59
59
HookEvent ,
60
60
LeaderElectedEvent ,
61
61
RelationDepartedEvent ,
62
+ SecretChangedEvent ,
62
63
WorkloadEvent ,
63
64
)
64
65
from ops .model import (
68
69
MaintenanceStatus ,
69
70
ModelError ,
70
71
Relation ,
72
+ SecretNotFoundError ,
71
73
Unit ,
72
74
UnknownStatus ,
73
75
WaitingStatus ,
@@ -211,12 +213,12 @@ def __init__(self, *args):
211
213
self .framework .observe (self .on .leader_elected , self ._on_leader_elected )
212
214
self .framework .observe (self .on [PEER ].relation_changed , self ._on_peer_relation_changed )
213
215
self .framework .observe (self .on .secret_changed , self ._on_peer_relation_changed )
216
+ # add specific handler for updated system-user secrets
217
+ self .framework .observe (self .on .secret_changed , self ._on_secret_changed )
214
218
self .framework .observe (self .on [PEER ].relation_departed , self ._on_peer_relation_departed )
215
219
self .framework .observe (self .on .postgresql_pebble_ready , self ._on_postgresql_pebble_ready )
216
220
self .framework .observe (self .on .pgdata_storage_detaching , self ._on_pgdata_storage_detaching )
217
221
self .framework .observe (self .on .stop , self ._on_stop )
218
- self .framework .observe (self .on .get_password_action , self ._on_get_password )
219
- self .framework .observe (self .on .set_password_action , self ._on_set_password )
220
222
self .framework .observe (self .on .promote_to_primary_action , self ._on_promote_to_primary )
221
223
self .framework .observe (self .on .get_primary_action , self ._on_get_primary )
222
224
self .framework .observe (self .on .update_status , self ._on_update_status )
@@ -380,6 +382,25 @@ def remove_secret(self, scope: Scopes, key: str) -> None:
380
382
381
383
self .peer_relation_data (scope ).delete_relation_data (peers .id , [secret_key ])
382
384
385
+ def get_secret_from_id (self , secret_id : str ) -> dict [str , str ]:
386
+ """Resolve the given id of a Juju secret and return the content as a dict.
387
+
388
+ This method can be used to retrieve any secret, not just those used via the peer relation.
389
+ If the secret is not owned by the charm, it has to be granted access to it.
390
+
391
+ Args:
392
+ secret_id (str): The id of the secret.
393
+
394
+ Returns:
395
+ dict: The content of the secret.
396
+ """
397
+ try :
398
+ secret_content = self .model .get_secret (id = secret_id ).get_content (refresh = True )
399
+ except (SecretNotFoundError , ModelError ):
400
+ raise
401
+
402
+ return secret_content
403
+
383
404
@property
384
405
def is_cluster_initialised (self ) -> bool :
385
406
"""Returns whether the cluster is already initialised."""
@@ -662,6 +683,17 @@ def _on_peer_relation_changed(self, event: HookEvent) -> None: # noqa: C901
662
683
663
684
self .async_replication .handle_read_only_mode ()
664
685
686
+ def _on_secret_changed (self , event : SecretChangedEvent ) -> None :
687
+ """Handle the secret_changed event."""
688
+ if not self .unit .is_leader ():
689
+ return
690
+
691
+ if (admin_secret_id := self .config .system_users ) and admin_secret_id == event .secret .id :
692
+ try :
693
+ self ._update_admin_password (admin_secret_id )
694
+ except PostgreSQLUpdateUserPasswordError :
695
+ event .defer ()
696
+
665
697
def _on_config_changed (self , event ) -> None :
666
698
"""Handle configuration changes, like enabling plugins."""
667
699
if not self .is_cluster_initialised :
@@ -703,6 +735,12 @@ def _on_config_changed(self, event) -> None:
703
735
# Enable and/or disable the extensions.
704
736
self .enable_disable_extensions ()
705
737
738
+ if admin_secret_id := self .config .system_users :
739
+ try :
740
+ self ._update_admin_password (admin_secret_id )
741
+ except PostgreSQLUpdateUserPasswordError :
742
+ event .defer ()
743
+
706
744
def enable_disable_extensions (self , database : str | None = None ) -> None :
707
745
"""Enable/disable PostgreSQL extensions set through config options.
708
746
@@ -858,6 +896,17 @@ def _get_hostname_from_unit(self, member: str) -> str:
858
896
859
897
def _on_leader_elected (self , event : LeaderElectedEvent ) -> None :
860
898
"""Handle the leader-elected event."""
899
+ # consider configured system user passwords
900
+ system_user_passwords = {}
901
+ if admin_secret_id := self .config .system_users :
902
+ try :
903
+ system_user_passwords = self .get_secret_from_id (secret_id = admin_secret_id )
904
+ except (ModelError , SecretNotFoundError ) as e :
905
+ # only display the error but don't return to make sure all users have passwords
906
+ logger .error (f"Error setting internal passwords: { e } " )
907
+ self .unit .status = BlockedStatus ("Password setting for system users failed." )
908
+ event .defer ()
909
+
861
910
for password in {
862
911
USER_PASSWORD_KEY ,
863
912
REPLICATION_PASSWORD_KEY ,
@@ -866,7 +915,14 @@ def _on_leader_elected(self, event: LeaderElectedEvent) -> None:
866
915
PATRONI_PASSWORD_KEY ,
867
916
}:
868
917
if self .get_secret (APP_SCOPE , password ) is None :
869
- self .set_secret (APP_SCOPE , password , new_password ())
918
+ if password in system_user_passwords :
919
+ # use provided passwords for system-users if available
920
+ self .set_secret (APP_SCOPE , password , system_user_passwords [password ])
921
+ logger .info (f"Using configured password for { password } " )
922
+ else :
923
+ # generate a password for this user if not provided
924
+ self .set_secret (APP_SCOPE , password , new_password ())
925
+ logger .info (f"Generated new password for { password } " )
870
926
871
927
# Add this unit to the list of cluster members
872
928
# (the cluster should start with only this member).
@@ -1202,66 +1258,22 @@ def _has_non_restore_waiting_status(self) -> bool:
1202
1258
and not self .is_cluster_restoring_to_time
1203
1259
)
1204
1260
1205
- def _on_get_password (self , event : ActionEvent ) -> None :
1206
- """Returns the password for a user as an action response.
1207
-
1208
- If no user is provided, the password of the operator user is returned.
1209
- """
1210
- username = event .params .get ("username" , USER )
1211
- if username not in PASSWORD_USERS and self .is_ldap_enabled :
1212
- event .fail ("The action can be run only for system users when LDAP is enabled" )
1213
- return
1214
- if username not in PASSWORD_USERS :
1215
- event .fail (
1216
- f"The action can be run only for system users or Patroni:"
1217
- f" { ', ' .join (PASSWORD_USERS )} not { username } "
1218
- )
1219
- return
1220
-
1221
- event .set_results ({"password" : self .get_secret (APP_SCOPE , f"{ username } -password" )})
1222
-
1223
- def _on_set_password (self , event : ActionEvent ) -> None : # noqa: C901
1224
- """Set the password for the specified user."""
1225
- # Only leader can write the new password into peer relation.
1226
- if not self .unit .is_leader ():
1227
- event .fail ("The action can be run only on leader unit" )
1228
- return
1229
-
1230
- username = event .params .get ("username" , USER )
1231
- if username not in SYSTEM_USERS and self .is_ldap_enabled :
1232
- event .fail ("The action can be run only for system users when LDAP is enabled" )
1233
- return
1234
- if username not in SYSTEM_USERS :
1235
- event .fail (
1236
- f"The action can be run only for system users:"
1237
- f" { ', ' .join (SYSTEM_USERS )} not { username } "
1238
- )
1239
- return
1240
-
1241
- password = new_password ()
1242
- if "password" in event .params :
1243
- password = event .params ["password" ]
1244
-
1245
- if password == self .get_secret (APP_SCOPE , f"{ username } -password" ):
1246
- event .log ("The old and new passwords are equal." )
1247
- event .set_results ({"password" : password })
1248
- return
1249
-
1250
- # Ensure all members are ready before trying to reload Patroni
1251
- # configuration to avoid errors (like the API not responding in
1252
- # one instance because PostgreSQL and/or Patroni are not ready).
1261
+ def _update_admin_password (self , admin_secret_id : str ) -> None :
1262
+ """Check if the password of a system user was changed and update it in the database."""
1253
1263
if not self ._patroni .are_all_members_ready ():
1254
- event .fail (
1264
+ # Ensure all members are ready before reloading Patroni configuration to avoid errors
1265
+ # e.g. API not responding in one instance because PostgreSQL / Patroni are not ready
1266
+ raise PostgreSQLUpdateUserPasswordError (
1255
1267
"Failed changing the password: Not all members healthy or finished initial sync."
1256
1268
)
1257
- return
1258
1269
1270
+ # cross-cluster replication: extract the database host on which to update the passwords
1259
1271
replication_offer_relation = self .model .get_relation (REPLICATION_OFFER_RELATION )
1272
+ other_cluster_primary_ip = ""
1260
1273
if (
1261
1274
replication_offer_relation is not None
1262
1275
and not self .async_replication .is_primary_cluster ()
1263
1276
):
1264
- # Update the password in the other cluster PostgreSQL primary instance.
1265
1277
other_cluster_endpoints = self .async_replication .get_all_primary_cluster_endpoints ()
1266
1278
other_cluster_primary = self ._patroni .get_primary (
1267
1279
alternative_endpoints = other_cluster_endpoints
@@ -1271,37 +1283,51 @@ def _on_set_password(self, event: ActionEvent) -> None: # noqa: C901
1271
1283
for unit in replication_offer_relation .units
1272
1284
if unit .name .replace ("/" , "-" ) == other_cluster_primary
1273
1285
)
1274
- try :
1275
- self .postgresql .update_user_password (
1276
- username , password , database_host = other_cluster_primary_ip
1277
- )
1278
- except PostgreSQLUpdateUserPasswordError as e :
1279
- logger .exception (e )
1280
- event .fail ("Failed changing the password." )
1281
- return
1282
1286
elif self .model .get_relation (REPLICATION_CONSUMER_RELATION ) is not None :
1283
- event . fail (
1284
- "Failed changing the password: This action can be ran only in the cluster from the offer side."
1287
+ logger . error (
1288
+ "Failed changing the password: This can be ran only in the cluster from the offer side."
1285
1289
)
1290
+ self .unit .status = BlockedStatus ("Password update for system users failed." )
1286
1291
return
1287
- else :
1288
- # Update the password in this cluster PostgreSQL primary instance.
1289
- try :
1290
- self .postgresql .update_user_password (username , password )
1291
- except PostgreSQLUpdateUserPasswordError as e :
1292
- logger .exception (e )
1293
- event .fail ("Failed changing the password." )
1294
- return
1295
1292
1296
- # Update the password in the secret store.
1297
- self .set_secret (APP_SCOPE , f"{ username } -password" , password )
1293
+ try :
1294
+ # get the secret content and check each user configured there
1295
+ # only SYSTEM_USERS with changed passwords are processed, all others ignored
1296
+ updated_passwords = self .get_secret_from_id (secret_id = admin_secret_id )
1297
+ for user , password in list (updated_passwords .items ()):
1298
+ if user not in SYSTEM_USERS :
1299
+ logger .error (
1300
+ f"Can only update system users: { ', ' .join (SYSTEM_USERS )} not { user } "
1301
+ )
1302
+ updated_passwords .pop (user )
1303
+ continue
1304
+ if password == self .get_secret (APP_SCOPE , f"{ user } -password" ):
1305
+ updated_passwords .pop (user )
1306
+ except (ModelError , SecretNotFoundError ) as e :
1307
+ logger .error (f"Error updating internal passwords: { e } " )
1308
+ self .unit .status = BlockedStatus ("Password update for system users failed." )
1309
+ return
1310
+
1311
+ try :
1312
+ # perform the actual password update for the remaining users
1313
+ for user , password in updated_passwords .items ():
1314
+ logger .info (f"Updating password for user { user } " )
1315
+ self .postgresql .update_user_password (
1316
+ user ,
1317
+ password ,
1318
+ database_host = other_cluster_primary_ip if other_cluster_primary_ip else None ,
1319
+ )
1320
+ # Update the password in the secret store after updating it in the database
1321
+ self .set_secret (APP_SCOPE , f"{ user } -password" , password )
1322
+ except PostgreSQLUpdateUserPasswordError as e :
1323
+ logger .exception (e )
1324
+ self .unit .status = BlockedStatus ("Password update for system users failed." )
1325
+ return
1298
1326
1299
1327
# Update and reload Patroni configuration in this unit to use the new password.
1300
1328
# Other units Patroni configuration will be reloaded in the peer relation changed event.
1301
1329
self .update_config ()
1302
1330
1303
- event .set_results ({"password" : password })
1304
-
1305
1331
def _on_promote_to_primary (self , event : ActionEvent ) -> None :
1306
1332
if event .params .get ("scope" ) == "cluster" :
1307
1333
return self .async_replication .promote_to_primary (event )
0 commit comments