-
Notifications
You must be signed in to change notification settings - Fork 574
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
Icinga DB: Config no_user_modify, Extract ValidateCertPath, and Support Redis username authentication #10102
base: master
Are you sure you want to change the base?
Conversation
As seen in the recent GHA run for #10102, CentOS is now dysfunctional. > Could not retrieve mirrorlist http://mirrorlist.centos.org/?release=7&arch=x86_64&repo=os&infra=container error was > 14: curl#6 - "Could not resolve host: mirrorlist.centos.org; Unknown error" > $ host mirrorlist.centos.org > Host mirrorlist.centos.org not found: 3(NXDOMAIN) Since CentOS Linux 7 has reached its end of life at June 30 together with RHEL7's end of maintenance, there will be no further updates. https://blog.centos.org/2023/04/end-dates-are-coming-for-centos-stream-8-and-centos-linux-7/
As seen in the recent GHA run for #10102, the two Windows Actions have failed. The output log contains: > DEBUG: 27+ >>>> ctest.exe -C "${env:CMAKE_BUILD_TYPE}" -T test -O $env:ICINGA2_BUILDPATH/Test.xml > --output-on-failure --log_level=all > CMake Error: Unknown argument: --log_level=all > CMake Error: Run 'ctest --help' for all supported options. After consulting ctest(1), older versions included, I have never found a mention of the "--log_level" flag. Since the useful "--output-on-failure" flag is already set, which will "[o]utput anything outputted by the test program if the test should fail", I do not see any further reason for more logging information. This flag was introduced in 7665143, but I have not found any reasoning for the flag in particular.
As seen in the recent GHA run for #10102, the two Windows Actions have failed. The output log contains: > DEBUG: 27+ >>>> ctest.exe -C "${env:CMAKE_BUILD_TYPE}" -T test -O $env:ICINGA2_BUILDPATH/Test.xml > --output-on-failure --log_level=all > CMake Error: Unknown argument: --log_level=all > CMake Error: Run 'ctest --help' for all supported options. After consulting ctest(1), older versions included, I have never found a mention of the "--log_level" flag. Since the useful "--output-on-failure" flag is already set, which will "[o]utput anything outputted by the test program if the test should fail", I do not see any further reason for more logging information. This flag was introduced in 7665143, but I have not found any reasoning for the flag in particular.
acb8c79
to
bc5e7ec
Compare
Rebased after the last two CI fixes were merged. |
As seen in the recent GHA run for #10102, the two Windows Actions have failed. The output log contains: > DEBUG: 27+ >>>> ctest.exe -C "${env:CMAKE_BUILD_TYPE}" -T test -O $env:ICINGA2_BUILDPATH/Test.xml > --output-on-failure --log_level=all > CMake Error: Unknown argument: --log_level=all > CMake Error: Run 'ctest --help' for all supported options. After consulting ctest(1), older versions included, I have never found a mention of the "--log_level" flag. Since the useful "--output-on-failure" flag is already set, which will "[o]utput anything outputted by the test program if the test should fail", I do not see any further reason for more logging information. This flag was introduced in 7665143, but I have not found any reasoning for the flag in particular.
As seen in the recent GHA run for #10102, the two Windows Actions have failed. The output log contains: > DEBUG: 27+ >>>> ctest.exe -C "${env:CMAKE_BUILD_TYPE}" -T test -O $env:ICINGA2_BUILDPATH/Test.xml > --output-on-failure --log_level=all > CMake Error: Unknown argument: --log_level=all > CMake Error: Run 'ctest --help' for all supported options. After consulting ctest(1), older versions included, I have never found a mention of the "--log_level" flag. Since the useful "--output-on-failure" flag is already set, which will "[o]utput anything outputted by the test program if the test should fail", I do not see any further reason for more logging information. This flag was introduced in 7665143, but I have not found any reasoning for the flag in particular.
lib/icingadb/icingadb.cpp
Outdated
if (!GetUsername().IsEmpty() && GetPassword().IsEmpty()) { | ||
BOOST_THROW_EXCEPTION(ValidationError(this, std::vector<String>(), "Validation failed: If username is set, a password is required as well.")); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I don't think this is the right place to perform such a constraint and even the existing certificate path validation. The IcingaDB::Validate()
method should validate the object as a whole, initialising the environment ID for example, not such a specific attribute validity check. Instead, you can put this in its own method by overriding the IcingaDB::ValidateUsername()
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like this for example:
icinga2/lib/icingadb/icingadb.cpp
Lines 217 to 224 in 8beb0b7
void IcingaDB::ValidateConnectTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils) | |
{ | |
ObjectImpl<IcingaDB>::ValidateConnectTimeout(lvalue, utils); | |
if (lvalue() <= 0) { | |
BOOST_THROW_EXCEPTION(ValidationError(this, { "connect_timeout" }, "Value must be greater than 0.")); | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead, you can put this in its own method by overriding the
IcingaDB::ValidateUsername()
method.
Aren't those for checking the values of individual fields in isolation?
The
IcingaDB::Validate()
method should validate the object as a whole, initialising the environment ID for example
Initializing something in a validate method sounds more like a hack to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought this was the right place due to the fact that the TLS verification just follows. But I can change it to a more specific validation method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't those for checking the values of individual fields in isolation?
As far as I'm concerned, ValidateUsername()
should fail as well if this constraint isn't met, but I see that a few other types also perform this kind of validation in the base method, so just as you like it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this would even work: Would GetPassword()
within IcingaDB::ValidateUsername()
even return a value yet? Would it depend on the order the attributes are set within the object definition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this would even work: Would
GetPassword()
withinIcingaDB::ValidateUsername()
even return a value yet?
lvalue
is literally a wrapper around a plain GetFoo()
method call 😅.
if (2 & types)
ValidateUsername(Lazy<String>([this]() { return GetUsername(); }), utils);
Would it depend on the order the attributes are set within the object definition?
No, the attributes are all initialised at the validation stage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it turns out, the individual validation methods are also called when modifying the attributes via the API:
icinga2/lib/base/configobject.cpp
Lines 193 to 194 in 3ffec77
ModAttrValidationUtils utils; | |
ValidateField(fid, Lazy<Value>{newValue}, utils); |
Since the password
field is set to no_user_modify
, meaning that it isn't modifiable via the API but with the current state of this PR, that sanity check wouldn't actually be triggered when changing/setting the username
via the API.
However, looking at the current Icinga DB code, it doesn't seem to make sense to me to make the config attributes runtime modifiable at all, as once Icinga DB is started, it never uses any of those, i.e. you can e.g. change the Redis port at runtime, but Icinga DB won't notice that change. Therefore, I would just mark the username
to no_user_modify
as well to disallow changes at runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am currently at reworking this, thus I would include your comment as well. Thanks for taking another look, @yhabteab.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I force pushed some changes according to @yhabteab's comments, which can be verified via the API as follows:
- Trying to create an IcingaDB object with a username but without a password:
$ curl -k -s -S -u root:icinga -H 'Accept: application/json' -X PUT 'https://localhost:5665/v1/objects/IcingaDBs/test' -d '{"attrs": {"username": "foo"}, "pretty": true}'
{
"results": [
{
"code": 500,
"errors": [
"Error: Validation failed for object 'test' of type 'IcingaDB'; Attribute 'username': If a username is given, a password is also required. However, a password can exist on its own.\nLocation: in /var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf: 2:2-2:17\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(1): object IcingaDB \"test\" {\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(2): username = \"foo\"\n ^^^^^^^^^^^^^^^^\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(3): version = 1726755773.422595\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(4): zone = \"master\"\n"
],
"status": "Object could not be created."
}
]
}
- Trying to create an IcingaDB object with a faulty user certificate config (only
cert_path
being set):
$ curl -k -s -S -u root:icinga -H 'Accept: application/json' -X PUT 'https://localhost:5665/v1/objects/IcingaDBs/test' -d '{"attrs": {"enable_tls": true, "cert_path": "/foo"}, "pretty": true}'
{
"results": [
{
"code": 500,
"errors": [
"Error: Validation failed for object 'test' of type 'IcingaDB'; Attribute 'cert_path': Either both a client certificate (cert_path) and its private key (key_path) or none of them must be given.\nLocation: in /var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf: 2:2-2:19\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(1): object IcingaDB \"test\" {\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(2): cert_path = \"/foo\"\n ^^^^^^^^^^^^^^^^^^\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(3): enable_tls = true\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(4): version = 1726755898.696290\n"
],
"status": "Object could not be created."
}
]
}
- Trying to create an IcingaDB object with a faulty user certificate config (only
key_path
being set):
$ curl -k -s -S -u root:icinga -H 'Accept: application/json' -X PUT 'https://localhost:5665/v1/objects/IcingaDBs/test' -d '{"attrs": {"enable_tls": true, "key_path": "/foo"}, "pretty": true}'
{
"results": [
{
"code": 500,
"errors": [
"Error: Validation failed for object 'test' of type 'IcingaDB'; Attribute 'cert_path': Either both a client certificate (cert_path) and its private key (key_path) or none of them must be given.\nLocation: in /var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf: 1:0-1:21\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(1): object IcingaDB \"test\" {\n ^^^^^^^^^^^^^^^^^^^^^^\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(2): enable_tls = true\n/var/lib/icinga2/api/packages/_api/c9ef1947-e1ff-47f1-9afa-385ad39c88a2/conf.d/icingadbs/test.conf(3): key_path = \"/foo\"\n"
],
"status": "Object could not be created."
}
]
}
- Successfully creating an IcingaDB object with username and password:
$ curl -k -s -S -u root:icinga -H 'Accept: application/json' -X PUT 'https://localhost:5665/v1/objects/IcingaDBs/test' -d '{"attrs": {"username": "foo", "password": "bar"}, "pretty": true}'
{
"results": [
{
"code": 200,
"status": "Object was created"
}
]
}
- Trying to change the username of the just created IcingaDB object, which should fail now:
$ curl -k -s -S -u root:icinga -H 'Accept: application/json' -X POST 'https://localhost:5665/v1/objects/IcingaDBs/test' -d '{"attrs": {"username": "change"}, "pretty": true}'
{
"results": [
{
"code": 500,
"name": "test",
"status": "Attribute 'username' could not be set: Error: Attribute cannot be modified.\n",
"type": "IcingaDB"
}
]
}
Each configuration field of an IcingaDB Object was marked with no_user_modify as modifications via the API would not result in an actual change. While the Object would be updated, the internal Redis connection would not be restarted, resulting in an unexpected behavior. The validation for cert_path and key_path was extracted from the generic Validate method to an own ValidateCertPath method. The missing db_index was added to the documentation.
bc5e7ec
to
55b31a9
Compare
The Redis ACL system was introduced with Redis 6.0. It introduced users with precisely granular permissions. This change allows Icinga 2 to use the Icinga DB feature against a Redis with an ACL user. This was reflected in the documentation, next to the already implemented, but undocumented Redis database. Closes #9536.
55b31a9
to
34d4a42
Compare
if (GetEnableTls() && GetCertPath().IsEmpty() != GetKeyPath().IsEmpty()) { | ||
BOOST_THROW_EXCEPTION(ValidationError(this, std::vector<String>(), "Validation failed: Either both a client certificate (cert_path) and its private key (key_path) or none of them must be given.")); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead, you can put this in its own method by overriding the
IcingaDB::ValidateUsername()
method.Aren't those for checking the values of individual fields in isolation?
🎵That is true, hackers, that is true..🎵
Especially here, three attrs are involved!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are referring to @julianbrost's question from above. @yhabteab wrote in a subsequent comment that this is not the case and one can reference other fields there as well.
I thought the consensus was to move the combined checks to their own methods.
But since I am just reading the Free Software Song out of your comment, please feel free to elaborate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no doubt that it works to validate the combination of three attributes in a validator of a single attribute. I just think the referenced code belongs where it was – in the validator of the object as whole.
Same for the username-password thing below. Especially if you can't change attributes at runtime, why to validate the constraints this way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Especially if you can't change attributes at runtime, why to validate the constraints this way?
Honestly, I don't quite get it! The ValidateAttributeFoo exists to exactly validate the attribute foo, and that's exactly what this PR does. Forget whether the attribute is mutable or not for now, why don't you want to perform such checks in the individual validators? But instead you want to decide where to perform the checks based on whether they are runtime modifiable or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, forget about runtime modification.
Validation methods named after fields are "for checking the values of individual fields in isolation". What's done here is validation of a combination of them, which belongs in the central method.
What you're gonna do here, admittedly, works (lvalue is just a Get(), etc.) – but what for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But you are still against using field-specific validation methods to perform validations of multiple fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TL;DR: Exactly! They simply don't belong there. I, as a dev, would expect e.g the referenced code (TLS) where it was before. That this works at all is rather by accident IMAO.
<walloftext reading="optional">Sure, there's no_user_modify now. But it has nothing to do with the rest. And, one nice day, we could decide to just let users modify them (for the next reload) or even make them working on the next connection (#10102 (comment)). Then no_user_modify would disappear, but the validation of the combination of three attrs would be still pinned to one of them. So I could bypass the validator by choosing an attribute w/o the validator. Or, if that's too hypothetical for you, imagine instead that no_user_modify and username were two separate PRs – not depending on each other of course. Same validation weakness.
</walloftext>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either you or I is messing something up. In my understanding, there is no direct link between the validation methods and the no_user_modify
attribute. The validation methods are being called to verify the state on creation and on further changes. The no_user_modify
attribute, however, just forbids updating the field during runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, no_user_modify just prevents runtime-updating attrs which, if not prevented, would also validate individual new values. (ConfigObject#ModifyAttribute() calls ValidateField()!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me try to summarize where all of this is coming from.
First of all, runtime object modification is quite naive: if you send a request modifying multiple attributes of an object, they are just set one after another in a loop:
icinga2/lib/remote/modifyobjecthandler.cpp
Lines 141 to 144 in b6b1506
for (const Dictionary::Pair& kv : attrs) { | |
key = kv.first; | |
obj->ModifyAttribute(kv.first, kv.second); | |
} |
ConfigObject::ModifyAttribute()
just calls ValidateField()
for each field individually with the new value before actually modifying it:
icinga2/lib/base/configobject.cpp
Line 194 in b6b1506
ValidateField(fid, Lazy<Value>{newValue}, utils); |
Note, that there is no call to the whole-object Validate()
method, so the individual ValideteXYZ()
methods that's all that's called for runtime objects. That's probably why this PR got so focused on no_user_modify
: for the initial config loading, functionality-wise it makes no difference whether the check is performed in Validate()
or ValidateXYZ()
, as both are called at the same time anyways.
So I believe that's why @yhabteab suggested to put the validation code into ValidateUsername()
: that way, the validation would still be run for runtime updates. However, if that's the intention, to do it properly, you'd also have to perform the very same check inside ValidatePassword()
as updating that field could also introduce a violation of constraint over both username and password. And even when this would be done, the IcingaDB
object type is a great example showing a new problem this would introduce: either both or none of cert_path
and key_path
can be set, so with the code updating attribute by attribute, you can't go between both or none set, you could only update already set paths to new paths (well, actually the way the check currently works, you could disable TLS first and do the changes).
Thus, no_user_modify
is basically the workaround for "we can't properly validate multi-attribute constraints for runtime updates".
I have a slight tendency towards just keeping the multi-attribute checks in Validate()
and prevent violating these constraints by forbidding runtime updates on the involved attributes. At least that was my intuition where these checks should be and while I think I understood the idea why they could be placed in ValidateXYZ()
, that seems to either introduce some strange asymmetry (why check the combination of username and password only in ValidateUsername()
but not ValidatePassword()
?) or duplication of the check for questionable benefit, i.e. removing no_user_modify
still sounds questionable, even when only taking the validation into account (ignoring that the Icinga DB feature would ignore the new values at runtime anyways).
|
||
if (!lvalue().IsEmpty() && GetPassword().IsEmpty()) { | ||
BOOST_THROW_EXCEPTION(ValidationError(this, { "username" }, | ||
"Redis password must be set, if username is provided.")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here there are only two. But still I could update (w/o no_user_modify) any of them causing this check to fail:
- Set username while there's no password
- Unset password while there's a username
If you insist, put this in ValidatePassword() at least IMAO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But still I could update (w/o no_user_modify) any of them causing this check to fail:
I'm having a hard time understanding what you're trying to imply as to what the problem is, first of all, what does w/o no_user_modify
mean? Did you remove the no_user_modify
restriction? The password
attribute is never supposed to be changed at runtime, nor is username
. So, if you remove this restriction, what do you expect to happen then?
Icinga DB: Config no_user_modify, Extract ValidateCertPath
Each configuration field of an IcingaDB Object was marked with no_user_modify as modifications via the API would not result in an actual change. While the Object would be updated, the internal Redis connection would not be restarted, resulting in an unexpected behavior.
The validation for cert_path and key_path was extracted from the generic Validate method to an own ValidateCertPath method.
The missing db_index was added to the documentation.
Icinga DB: Support Redis username authentication
The Redis ACL system was introduced with Redis 6.0. It introduced users with precisely granular permissions. This change allows Icinga 2 to use the Icinga DB feature against a Redis with an ACL user.
This was reflected in the documentation, next to the already implemented, but undocumented Redis database.
Closes #9536.