Skip to content

Commit 3163512

Browse files
committed
clear the password of a NOLOGIN role that still holds one
`buildAlterRole` added `PASSWORD NULL` only on the transition from `LOGIN` to `NOLOGIN`. A role that was already `NOLOGIN` but still held a password kept it. `pg_roles` masks `rolpassword` with a constant, so the operator cannot tell whether such a role has a password. It therefore always clears the password when the spec expects no login. This costs nothing in the steady state. `RoleReconciler` calls `alterRole` only when the login state, the flags, or the password differ from the spec.
1 parent 70ec3a9 commit 3163512

2 files changed

Lines changed: 52 additions & 3 deletions

File tree

operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleService.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,12 +374,17 @@ private static Optional<Query> buildAlterRole(
374374
}
375375

376376
// Password handling
377-
// - if the role loses LOGIN, remove the password
377+
// - if no login is expected, remove the password
378378
// - if LOGIN and the password changed, set the new password
379-
if (!loginExpected && currentCanLogin) {
379+
//
380+
// `PASSWORD NULL` is not conditional on the current state. `pg_roles` masks `rolpassword`
381+
// with a constant, so the operator cannot tell whether a `NOLOGIN` role still holds a
382+
// password. The reconciler calls this method only when the role differs from the spec,
383+
// so the statement does not run on every reconcile.
384+
if (!loginExpected) {
380385
options.add(keyword(RoleFlag.PASSWORD.flag()));
381386
options.add(keyword("NULL"));
382-
} else if (loginExpected && changePassword) {
387+
} else if (changePassword) {
383388
options.add(keyword(RoleFlag.PASSWORD.flag()));
384389
options.add(val(password));
385390
}

operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
import static org.assertj.core.api.Assertions.assertThat;
3737
import static org.awaitility.Awaitility.await;
3838
import static org.jooq.impl.DSL.inline;
39+
import static org.jooq.impl.DSL.query;
3940
import static org.jooq.impl.DSL.role;
41+
import static org.jooq.impl.DSL.val;
4042

4143
@QuarkusTest
4244
@RequiredArgsConstructor
@@ -196,6 +198,48 @@ void toggleRoleLogin_updatesCorrectly() {
196198
assertThat(getRoleFlagValue(dsl, roleName, PG_ROLES.ROLCANLOGIN)).isFalse();
197199
}
198200

201+
@Test
202+
@DisplayName("When a NOLOGIN Role still holds a password, the next update should clear it")
203+
void noLoginRole_withLeftoverPassword_clearsPassword() {
204+
// given: a reconciled NOLOGIN role
205+
var clusterConnection = given.one()
206+
.clusterConnection()
207+
.withName("test-connection-role-leftover-password")
208+
.returnFirst();
209+
210+
var roleName = "test-role-leftover-password";
211+
212+
var role = given.one()
213+
.role()
214+
.withName(roleName)
215+
.withClusterConnectionName(clusterConnection.getMetadata().getName())
216+
.returnFirst();
217+
218+
var dsl = postgreSQLContextFactory.getDSLContext(clusterConnection);
219+
220+
assertThat(getRoleFlagValue(dsl, roleName, PG_ROLES.ROLCANLOGIN)).isFalse();
221+
222+
// and: somebody sets a password directly in PostgreSQL.
223+
// The role keeps NOLOGIN, so the login state still matches the spec.
224+
// `pg_roles` masks `rolpassword`, so the operator cannot see that password.
225+
dsl.execute(query(
226+
"alter role {0} with password {1}",
227+
role(roleName),
228+
val("leftover-password")
229+
));
230+
231+
assertThat(PostgreSQLPasswordVerifier.storedVerifier(dsl, roleName)).isNotNull();
232+
233+
// when: an unrelated flag changes, so the operator alters the role
234+
role.getSpec().getFlags().setCreatedb(true);
235+
236+
applyRole(role);
237+
238+
// then: the operator cleared the leftover password
239+
assertThat(getRoleFlagValue(dsl, roleName, PG_ROLES.ROLCREATEDB)).isTrue();
240+
assertThat(PostgreSQLPasswordVerifier.storedVerifier(dsl, roleName)).isNull();
241+
}
242+
199243
@Test
200244
@DisplayName("When a Role references a missing ClusterConnection, status should be PENDING with a helpful message")
201245
void createRole_withMissingClusterConnection_setsPending() {

0 commit comments

Comments
 (0)