Fix Issue 227: Generate new PostgreSQL password for role after restoration#348
Conversation
…ange Signed-off-by: Jevgeni Fenko <jevgeni.fenko@gmail.com>
… prevents unwanted generation due to any spec change. Signed-off-by: Jevgeni Fenko <jevgeni.fenko@gmail.com>
Signed-off-by: Jevgeni Fenko <jevgeni.fenko@gmail.com>
…postgreSQL-password-for-role-after-restoration
9879996 to
223b9ed
Compare
…abases and roles restoration Signed-off-by: Jevgeni Fenko <jevgeni.fenko@gmail.com>
223b9ed to
0d91262
Compare
Signed-off-by: Jevgeni Fenko <jevgeni.fenko@gmail.com>
|
@JevgeniF thanks for your contribution! I like the idea, wondering if it can be extended to become a sort of either:
I did some related work in another provider for a User resource in provider-clickhousedbops |
|
Dear @fernandezcuesta, Your idea to bring BYOP to this project is great, but I’m not sure it should be solved within the context of this specific fix. |
There was a problem hiding this comment.
Alternative implementation, considering that:
- passwordSecretRef implements BYOP (and this trigger should have no effect)
- Set a timestamp indicating when you want the password to be reset, so it somehow changes the logic of "reset the password"
(missing: update in role_types to add PasswordRotationTrigger in roleparameters and LastPasswordChange in RoleObservation + tests)
| if pw == "" { | ||
| pw, err = password.Generate() | ||
| if err != nil { | ||
| return managed.ExternalUpdate{}, err | ||
| } | ||
| } |
There was a problem hiding this comment.
| if pw == "" { | |
| pw, err = password.Generate() | |
| if err != nil { | |
| return managed.ExternalUpdate{}, err | |
| } | |
| } |
There was a problem hiding this comment.
Returned it back, because now - i.e., when we need to reset a password in the non-BYOP case - c.getPassword returns "", true, nil, and we need to generate the password before the ALTER ROLE ... PASSWORD ... query in the database."
| func (c *external) shouldResetPassword(ctx context.Context, role *v1alpha1.Role) (bool, error) { | ||
| if role.Spec.ForProvider.PasswordReset == nil || !*role.Spec.ForProvider.PasswordReset { | ||
| return false, nil | ||
| } | ||
| if role.Spec.WriteConnectionSecretToReference == nil { | ||
| return newPwd, false, nil | ||
| return false, nil | ||
| } | ||
|
|
||
| nn = types.NamespacedName{ | ||
| nn := types.NamespacedName{ | ||
| Name: role.Spec.WriteConnectionSecretToReference.Name, | ||
| Namespace: role.Spec.WriteConnectionSecretToReference.Namespace, | ||
| } | ||
| s = &corev1.Secret{} | ||
| // the output secret may not exist yet, so we can skip returning an | ||
| // error if the error is NotFound | ||
| s := &corev1.Secret{} | ||
| if err := c.kube.Get(ctx, nn, s); resource.IgnoreNotFound(err) != nil { | ||
| return "", false, err | ||
| return false, err | ||
| } | ||
| // if newPwd was set to some value, compare value in output secret with | ||
| // newPwd | ||
| changed = newPwd != "" && newPwd != string(s.Data[xpv1.ResourceCredentialsSecretPasswordKey]) | ||
|
|
||
| return newPwd, changed, nil | ||
| return len(s.Data[xpv1.ResourceCredentialsSecretPasswordKey]) == 0, nil | ||
| } |
There was a problem hiding this comment.
| func (c *external) shouldResetPassword(ctx context.Context, role *v1alpha1.Role) (bool, error) { | |
| if role.Spec.ForProvider.PasswordReset == nil || !*role.Spec.ForProvider.PasswordReset { | |
| return false, nil | |
| } | |
| if role.Spec.WriteConnectionSecretToReference == nil { | |
| return newPwd, false, nil | |
| return false, nil | |
| } | |
| nn = types.NamespacedName{ | |
| nn := types.NamespacedName{ | |
| Name: role.Spec.WriteConnectionSecretToReference.Name, | |
| Namespace: role.Spec.WriteConnectionSecretToReference.Namespace, | |
| } | |
| s = &corev1.Secret{} | |
| // the output secret may not exist yet, so we can skip returning an | |
| // error if the error is NotFound | |
| s := &corev1.Secret{} | |
| if err := c.kube.Get(ctx, nn, s); resource.IgnoreNotFound(err) != nil { | |
| return "", false, err | |
| return false, err | |
| } | |
| // if newPwd was set to some value, compare value in output secret with | |
| // newPwd | |
| changed = newPwd != "" && newPwd != string(s.Data[xpv1.ResourceCredentialsSecretPasswordKey]) | |
| return newPwd, changed, nil | |
| return len(s.Data[xpv1.ResourceCredentialsSecretPasswordKey]) == 0, nil | |
| } | |
| func (c *external) shouldResetPassword(ctx context.Context, role *v1alpha1.Role) bool { | |
| if role.Spec.ForProvider.PasswordSecretRef != nil { | |
| return false | |
| } | |
| trigger := role.Spec.ForProvider.PasswordReset | |
| if trigger == nil { | |
| return false | |
| } | |
| last := role.Status.AtProvider.LastPasswordChange | |
| if last == nil { | |
| return true | |
| } | |
| return trigger.After(last.Time) | |
| } |
There was a problem hiding this comment.
Kept PasswordReset as *bool, because it should fire once when LastPasswordChange is nil (meaning the provider has never set a password for this role), not on a schedule (as in our issue, when we restore database with roles, but provider doesn't generate password).
For time-based rotation I added a separate PasswordRotationTrigger *metav1.Time field, which does use trigger.After(last.Time) in shouldResetPassword. So both use cases are covered:
-- PasswordReset: true - fires once when LastPasswordChange == nil
-- PasswordRotationTrigger - fires whenever the trigger time is after LastPasswordChange
2d641ee to
92091c4
Compare
Signed-off-by: Jevgeni Fenko <jevgeni.fenko@gmail.com>
|
@fernandezcuesta maybe I got your idea wrong and made wrong implementation. Please look again and build and test if BYOP and password rotation works as you wanted. New field added to Both A new
|
|
Hey, thanks for considering it and your explanation. I'm wondering if both fields aren't a bit redundant. I mean, with a timestamp-alike field, can't you satisfy the scenario in which you lost the password and want to have it immediately recreated? In the case of a "past" time, wouldn't it act similar to PasswordReset? |
|
@fernandezcuesta you are totally right. I was overthinking and left our simple (legacy) logic in parallel with your solution, but To sum up...
Am I right? Updated logic for non-BYOP ( For new role creation:
For restoration scenarios:
Looks simplier and works in any scenario: |
|
Yes I guess that's correct |
|
@fernandezcuesta OK. Then I push updated version tomorrow. Thank you! |
Password reset logic built on Role status LastPasswordChange and secret check; Password rotation logic for non BYOD cases remains on PasswordRotationTrigger Signed-off-by: Jevgeni Fenko <jevgeni.fenko@gmail.com>
|
Some comments. Current flows: Role restoration (non-BYOP): Password rotation (non-BYOP): New inux_amd64 test package: |
Description of your changes
Fixes #227: The change adds an optional boolean field,
passwordReset, to the Role spec. This field changes how the provider behaves when it encounters an empty Secret resource after a restoration:If
passwordResetis true and the Secret is empty, the provider generates a new password for the role and populates the Secret.If
passwordResetis false or omitted, the provider acts as usual and leaves the role's password unchanged (meaning the Secret may simply stay empty).I have:
make reviewableto ensure this PR is ready for review.How has this code been tested
Unit tests written to test passwordResetToken work.
Manual test in AWS cluster.
A test linux_amd64 package for you reference can be found in Docker Hub:
docker pull entigolabs/provider-sql:testPackage digest:
sha256:6b41c2bc13fc23ecd13f9461d3beeb3d38af3990c6d3c1b75167ad0260382d05