Repository navigation
fix: use cached leadership in security logging, renew lease on success - #2778
tonyandrewmeyer merged 6 commits into
Conversation
Pass the existing leadership state in to the security logging, rather than running is-leader while reporting a failed hook command. Only store the refreshed leadership lease time when is-leader succeeds.
james-garner-canonical
left a comment
There was a problem hiding this comment.
Thanks, this looks good to me.
| if self._is_recursive.get(): | ||
| # Either `juju-log` hook command failed or there's a bug in ops. | ||
| return | ||
| is_leader = None |
There was a problem hiding this comment.
| is_leader = None | |
| # Save known leadership status at time of calling the hook command, | |
| # for use in security event logging if the hook command fails. | |
| is_leader = None |
| return | ||
| base_cmd = os.path.basename(cmd) | ||
| leadership = ' (as leader)' if self.is_leader() else '' | ||
| leadership = ' (as leader)' if is_leader else '' |
There was a problem hiding this comment.
I think this is a decent behaviour change -- we don't call is-leader on every authorization related error, which is a small win for performance, and arguably for correctness too on two fronts: 1) we avoid potentially failing in error handling on another hook command call, and 2) it's sensible to report the known leadership status at the time we called the hook command rather than afterwards.
Ops will have checked if we were the leader on events where we expect that to matter (so it will be cached in that case), and leadership doesn't matter for other permission errors (e.g. querying a secret or relation ID that we don't have access to).
WDYT @tonyandrewmeyer?
There was a problem hiding this comment.
If the lease has expired (or is_leader() wasn't called in this hook), is_leader is None and the log reads exactly the same as for a known non-leader. Since this is the security audit log, I think it would be better to distinguish those, maybe with something like (leadership unknown) for None?
There was a problem hiding this comment.
Added (leadership unknown) when there is no valid cached status.
| query = mock.Mock(side_effect=is_leader) | ||
| monkeypatch.setattr(hookcmds, 'is_leader', query) | ||
| assert backend.is_leader() is True | ||
| assert backend._leader_check_time == 100 |
There was a problem hiding this comment.
| assert backend._leader_check_time == 100 | |
| assert query.call_count == 1 | |
| assert backend._leader_check_time == 100 | |
| assert backend._leader_check_time != now |
| assert backend._leader_check_time == 100 | ||
| now = 130 | ||
| assert backend.is_leader() is True | ||
| assert query.call_count == 1 |
There was a problem hiding this comment.
| assert query.call_count == 1 | |
| assert query.call_count == 1 | |
| assert backend._leader_check_time == 100 | |
| assert backend._leader_check_time != now |
| now = 131 | ||
| assert backend.is_leader() is True | ||
| assert query.call_count == 2 | ||
| assert backend._leader_check_time == 131 |
There was a problem hiding this comment.
| assert backend._leader_check_time == 131 | |
| assert backend._leader_check_time == 131 | |
| assert backend._leader_check_time != now |
| 'cached,age,as_leader', | ||
| [ | ||
| (True, 0, True), | ||
| (True, 30, True), |
There was a problem hiding this comment.
This is quite funny on our part really isn't it, it's just asking for a leadership change out from under you ...
| return | ||
| base_cmd = os.path.basename(cmd) | ||
| leadership = ' (as leader)' if self.is_leader() else '' | ||
| leadership = ' (as leader)' if is_leader else '' |
There was a problem hiding this comment.
If the lease has expired (or is_leader() wasn't called in this hook), is_leader is None and the log reads exactly the same as for a known non-leader. Since this is the security audit log, I think it would be better to distinguish those, maybe with something like (leadership unknown) for None?
Move the lease check into a _cached_leadership() helper shared by is_leader() and _check_for_security_event(), so the two can't drift and juju_log() failures are covered too. Report "(leadership unknown)" in the security event when there is no valid cached status, and add a test for the is-leader authz failure that used to recurse.
|
Thanks Tony, all addressed in a53b128. One note on the history: my first cut of this PR had a shared helper like |
tonyandrewmeyer
left a comment
There was a problem hiding this comment.
Thanks! Could you please update the PR body? It gets used as the body of the squash commit, so it's important that it's up-to-date and accurate.
|
Updated the PR title and body to match the final change. |
Reporting a failed hook command as a security event called
is_leader(), which could run theis-leaderhook command while handling the original failure. Ifis-leaderitself failed with an authorisation error and no leadership status was cached, this recursed until aRecursionError.The security event now uses the leadership status that ops already has cached, through a new
_cached_leadership()helper thatis_leader()shares, so the lease rule is written once. This also covers failures injuju_log(). When there is no valid cached status, the event says "(leadership unknown)" rather than reading the same as a known non-leader.is_leader()also stored the lease time before runningis-leader, so a failed refresh kept the previous value with a fresh timestamp. The time is still taken before the query, and is now stored only once it succeeds.No exception message changes:
str(e)andargs[0]ofModelErrorare still Juju's stderr.