[CORE-15278] Add password_set_at field to the ScramCredential proto message (Security Admin API V2)#29328
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a password_set_at timestamp field to track when SCRAM credentials were created or last modified. The field is exposed through the admin API v2 as a read-only property, enabling clients to verify credential state changes and perform accurate reconciliation.
Changes:
- Extended the internal
scram_credentialstructure to include an optionalpassword_set_attimestamp field - Updated admin API v2 proto definitions and generated Python bindings to expose the timestamp field
- Modified credential creation and conversion logic to populate timestamps (current time for new credentials,
InfinitePastfor legacy credentials without timestamps) - Added comprehensive test coverage for timestamp behavior across creation, updates, propagation, and persistence
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| proto/redpanda/core/admin/v2/security.proto | Added password_set_at timestamp field to ScramCredential message |
| tests/rptest/clients/admin/proto/redpanda/core/admin/v2/security_pb2.py | Updated generated Python protobuf code with new timestamp field |
| tests/rptest/clients/admin/proto/redpanda/core/admin/v2/security_pb2.pyi | Updated Python type stubs for new timestamp field |
| src/v/security/scram_credential.h | Extended scram_credential class with optional password_set_at field and bumped serde version |
| src/v/security/scram_algorithm.h | Modified make_credentials to populate password_set_at with current timestamp |
| src/v/redpanda/admin/services/security.cc | Updated conversion function to set password_set_at from credential or InfinitePast for legacy credentials |
| tests/rptest/tests/scram_test.py | Added comprehensive test for password_set_at behavior including creation, updates, propagation, and persistence |
| src/v/security/tests/scram_algorithm_test.cc | Added test verifying make_credentials populates timestamp |
| src/v/security/tests/credential_store_test.cc | Added test for credential store handling of timestamps |
| src/v/redpanda/admin/services/tests/security_test.cc | Added tests verifying protobuf conversion handles timestamps correctly |
| src/v/security/tests/BUILD | Added model dependency for timestamp support in tests |
| # Sleep to ensure timestamp difference (at least 1 second) | ||
| time.sleep(1) |
There was a problem hiding this comment.
Hardcoded 1-second sleep can make tests unnecessarily slow and brittle. Consider using a minimal sleep (e.g., 0.01 seconds) combined with explicit timestamp capture before/after the update operation, or mock the timestamp for deterministic testing.
| # Sleep to ensure timestamp difference (at least 1 second) | |
| time.sleep(1) | |
| # Minimal sleep to allow timestamp to advance on coarse-grained clocks | |
| time.sleep(0.01) |
| password, security::scram_sha256::min_iterations); | ||
|
|
||
| // Clear the password_set_at to simulate a credential without a timestamp | ||
| auto& password_set_at = std::get<4>(security_cred.serde_fields()); |
There was a problem hiding this comment.
Using a magic number index (4) to access tuple elements is fragile and will break if the order of fields in serde_fields() changes. Consider using structured bindings with descriptive names or adding a named accessor method to scram_credential for setting password_set_at.
| auto& password_set_at = std::get<4>(security_cred.serde_fields()); | |
| auto& [salt, server_key, stored_key, iterations, password_set_at] | |
| = security_cred.serde_fields(); |
There was a problem hiding this comment.
agree with bot here
290b049 to
fe4cb01
Compare
|
Force push to fix unit test affected by the addition of the |
| pb_cred.set_password_set_at( | ||
| cred.password_set_at().has_value() | ||
| ? absl::FromChrono(model::to_time_point(cred.password_set_at().value())) | ||
| : absl::InfinitePast()); |
There was a problem hiding this comment.
I don't think you want infinite past, and this likely causes an error during serialization. You want absl::UnixEpoch
| assert updated_at >= before_update, ( | ||
| f"password_set_at {updated_at} should be >= {before_update}" | ||
| ) | ||
|
|
||
| assert updated_at > created_at, ( | ||
| f"password_set_at should be updated: {updated_at} < {created_at}" | ||
| ) |
There was a problem hiding this comment.
need to account for clock skew in CDT. Maybe let's just ensure it's within an hour or something of now?
| // Records when the password was last set | ||
| std::optional<model::timestamp> _password_set_at; |
There was a problem hiding this comment.
I'm not sure we need a std::optional here, instead I think we can depend on the value being timestamp::missing() if it's not provided.
| BOOST_REQUIRE_EQUAL(r1->password_set_at().has_value(), true); | ||
| BOOST_REQUIRE_EQUAL(r1->password_set_at().value(), now); |
There was a problem hiding this comment.
nit: YOu can probably just do the value comparison without having to check that it has a value.
| // Copy the server-generated password_set_at timestamp to the expected | ||
| // credential, since this field is set to the current time at creation and | ||
| // cannot be predetermined in the test | ||
| std::get<4>(creds_256.serde_fields()) = cred->password_set_at(); |
There was a problem hiding this comment.
The std::get<4> seems unstable to me... maybe expose a setter method?
There was a problem hiding this comment.
Also I wonder if timestamp needs to be part of the comparison at all... I think we do do other cred comparison and I feel comparing the timestamp is unnecessary (especially say when we update/create a user).
| password, security::scram_sha256::min_iterations); | ||
|
|
||
| // Clear the password_set_at to simulate a credential without a timestamp | ||
| auto& password_set_at = std::get<4>(security_cred.serde_fields()); |
There was a problem hiding this comment.
agree with bot here
| auto security_cred = security::scram_sha256::make_credentials( | ||
| password, security::scram_sha256::min_iterations); |
There was a problem hiding this comment.
I see what you're testing here - maybe add an optional parameter to make_credentials to take in a timestamp and if provided, use it
There was a problem hiding this comment.
Oh good point, that'd would make things easier
a7c5553 to
ed7196d
Compare
CI test resultstest results on build#79335test results on build#79419
|
Add a helper method to check if a timestamp has the missing sentinel value (-1). This provides a cleaner API than comparing against timestamp::missing().value() directly. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add optional password_set_at timestamp field to track when SCRAM credentials were last updated. This bumps the serde version from 0 to 1 while maintaining backward compatibility with compat_version 0.
Add unit tests to verify password_set_at field behavior in credential store, including credentials without timestamp (backward compatibility), credentials with timestamp, and timestamp updates. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Set password_set_at to the current timestamp when generating new SCRAM credentials to track when passwords are created.
Add assertions to verify that password_set_at is set when SCRAM credentials are created via the Kafka API.
Add test to verify that make_credentials properly sets the password_set_at timestamp field when creating SCRAM credentials. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Expose password_set_at timestamp in the admin API v2 ScramCredential message as an output-only field to allow clients to query when passwords were last updated.
Convert and set the password_set_at timestamp when converting internal scram_credential to protobuf ScramCredential for admin API v2 responses.
Add test coverage for the password_set_at field in convert_to_pb_scram_credential: - Verify that credentials created via make_credentials have password_set_at populated - Verify that credentials without timestamps (for backwards compatibility) correctly have no password_set_at field when converted to protobuf Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Regenerate Python protobuf bindings to include the new password_set_at field in ScramCredential message.
Add comprehensive integration test for password_set_at timestamp field in the v2 SCRAM credential API. The test verifies: - password_set_at is set when creating credentials - password_set_at is updated when passwords change - Updated timestamps are strictly greater than original timestamps - Credentials propagate with timestamps correctly across all nodes - Timestamps persist correctly across cluster restarts Also adds `check_credential_on_all_nodes()` helper method to verify credential propagation with optional `password_set_at` validation.
ed7196d to
eb6a474
Compare
|
Force push to address PR comments:
|
| bool operator==(const scram_credential& other) const { | ||
| return _salt == other._salt && _server_key == other._server_key | ||
| && _stored_key == other._stored_key | ||
| && _iterations == other._iterations | ||
| && _principal == other._principal; | ||
| } |
There was a problem hiding this comment.
The comment states that equality excludes password_set_at because it's "metadata rather than part of the credential's identity." However, this design choice may not be obvious to future maintainers and could lead to subtle bugs. Consider expanding the documentation to explain the specific use cases where this behavior is desired (e.g., comparing credentials for authentication purposes) and explicitly warning about scenarios where timestamp comparison might be needed separately.
| return std::tie( | ||
| _salt, _server_key, _stored_key, _iterations, _password_set_at); |
There was a problem hiding this comment.
The serde_fields() method includes _password_set_at for serialization, but _principal is explicitly excluded (as noted in the comment on line 78). This asymmetry between serialized fields and equality comparison could be confusing. Consider adding a comment explaining why _principal is excluded from serialization but included in equality, while _password_set_at is included in serialization but excluded from equality.
| pb_cred.set_password_set_at( | ||
| cred.password_set_at().is_missing() | ||
| ? absl::UnixEpoch() | ||
| : absl::FromChrono(model::to_time_point(cred.password_set_at()))); |
There was a problem hiding this comment.
The conversion logic maps missing timestamps to Unix epoch, which is consistent with the proto documentation. However, the choice of Unix epoch as the sentinel value for "unknown" is not documented here in the C++ code. Consider adding a brief comment explaining why Unix epoch was chosen over other options (e.g., setting the field to unset/null in protobuf) to help future maintainers understand this design decision.
rockwotj
left a comment
There was a problem hiding this comment.
LGTM, will let Mike do the final signoff
This change extends the internal scram_credential structure and admin API v2 to track when SCRAM passwords were last set. Existing credentials without timestamps are supported and will return the Unix epoch as the
password_set_atwhen queried.The admin API v2
/v1/security/scram_credentialsendpoints now include a read-onlypassword_set_attimestamp field in ScramCredential responses:CreateScramCredential- returns credential with password_set_at set to creation timeUpdateScramCredential- returns credential with password_set_at updated to modification timeGetScramCredential- returns credential with password_set_at (or the Unix epoch for old credentials)ListScramCredentials- returns credentials with password_set_at for each entryThe v1 admin API is unchanged and does not expose this field.
Resolves CORE-15278
Backports Required
Release Notes
Improvements
password_set_attimestamps, exposed via admin API v2. This enables clients like the Kubernetes operator to verify credential state changes have propagated and perform accurate reconciliation.