Skip to content

Commit

Permalink
change admin user to be named operator (#147)
Browse files Browse the repository at this point in the history
* change admin user to be named operator

* lint + unit test correction + fetch lib

* Update actions.yaml

Co-authored-by: Mykola Marzhan <[email protected]>
  • Loading branch information
MiaAltieri and delgod authored Jan 12, 2023
1 parent 8f122da commit 382e91d
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 59 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This operator charm deploys and operates MongoDB on physical or virtual machines
auto-delete - `boolean`; When a relation is removed, auto-delete ensures that any relevant databases
associated with the relation are also removed. Set with `juju config mongodb auto-delete=<bool>`.

admin-password - `string`; The password for the database admin. Set with `juju run-action mongodb/leader set-password --wait`
operator password - `string`; The password for the database admin (named "operator"). Set with `juju run-action mongodb/leader set-password --wait`

tls external key - `string`; TLS external key for encryption outside the cluster. Set with `juju run-action mongodb/0 set-tls-private-key "external-key=$(base64 -w0 external-key-0.pem)" --wait`

Expand Down Expand Up @@ -132,13 +132,13 @@ juju remove-relation mongodb tls-certificates-operator
Note: The TLS settings here are for self-signed-certificates which are not recommended for production clusters, the `tls-certificates-operator` charm offers a variety of configurations, read more on the TLS charm [here](https://charmhub.io/tls-certificates-operator)

### Password rotation
#### Internal admin user
The admin user is used internally by the Charmed MongoDB Operator, the `set-password` action can be used to rotate its password.
#### Internal operator user
The operator user is used internally by the Charmed MongoDB Operator, the `set-password` action can be used to rotate its password.
```shell
# to set a specific password for the admin user
# to set a specific password for the operator user
juju run-action mongodb/leader set-password password=<password> --wait

# to randomly generate a password for the admin user
# to randomly generate a password for the operator user
juju run-action mongodb/leader set-password --wait
```

Expand Down
9 changes: 9 additions & 0 deletions actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@ get-primary:
get-password:
description: Change the admin user's password, which is used by charm.
It is for internal charm users and SHOULD NOT be used by applications.
params:
username:
type: string
description: The username, the default value 'operator'.
Possible values - operator, backup.

set-password:
description: Change the admin user's password, which is used by charm.
It is for internal charm users and SHOULD NOT be used by applications.
params:
username:
type: string
description: The username, the default value 'operator'.
Possible values - operator, backup.
password:
type: string
description: The password will be auto-generated if this option is not specified.
Expand Down
2 changes: 1 addition & 1 deletion lib/charms/mongodb/v0/mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
logger = logging.getLogger(__name__)

# List of system usernames needed for correct work on the charm.
CHARM_USERS = ["operator"]
CHARM_USERS = ["operator", "backup"]


@dataclass
Expand Down
190 changes: 165 additions & 25 deletions lib/charms/tls_certificates_interface/v1/tls_certificates.py

Large diffs are not rendered by default.

33 changes: 25 additions & 8 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
get_create_user_cmd,
)
from charms.mongodb.v0.mongodb import (
CHARM_USERS,
MongoDBConfiguration,
MongoDBConnection,
NotReadyError,
Expand Down Expand Up @@ -108,8 +109,8 @@ def _generate_passwords(self) -> None:
The same keyFile and admin password on all members needed, hence it is generated once and
share between members via the app data.
"""
if not self.get_secret("app", "password"):
self.set_secret("app", "password", generate_password())
if not self.get_secret("app", "operator-password"):
self.set_secret("app", "operator-password", generate_password())

if not self.get_secret("app", "keyfile"):
self.set_secret("app", "keyfile", generate_keyfile())
Expand Down Expand Up @@ -408,7 +409,15 @@ def _on_get_primary_action(self, event: ops.charm.ActionEvent):

def _on_get_password(self, event: ops.charm.ActionEvent) -> None:
"""Returns the password for the user as an action response."""
event.set_results({"admin-password": self.get_secret("app", "password")})
username = event.params.get("username", "operator")
if username not in CHARM_USERS:
event.fail(
f"The action can be run only for users used by the charm:"
f" {', '.join(CHARM_USERS)} not {username}"
)
return

event.set_results({f"{username}-password": self.get_secret("app", f"{username}-password")})

def _on_set_password(self, event: ops.charm.ActionEvent) -> None:
"""Set the password for the admin user."""
Expand All @@ -417,13 +426,21 @@ def _on_set_password(self, event: ops.charm.ActionEvent) -> None:
event.fail("The action can be run only on leader unit.")
return

username = event.params.get("username", "operator")
if username not in CHARM_USERS:
event.fail(
f"The action can be run only for users used by the charm:"
f" {', '.join(CHARM_USERS)} not {username}"
)
return

new_password = generate_password()
if "password" in event.params:
new_password = event.params["password"]

with MongoDBConnection(self.mongodb_config) as mongo:
try:
mongo.set_user_password("admin", new_password)
mongo.set_user_password(username, new_password)
except NotReadyError:
event.fail(
"Failed changing the password: Not all members healthy or finished initial sync."
Expand All @@ -433,8 +450,8 @@ def _on_set_password(self, event: ops.charm.ActionEvent) -> None:
event.fail(f"Failed changing the password: {e}")
return

self.set_secret("app", "password", new_password)
event.set_results({"admin-password": self.get_secret("app", "password")})
self.set_secret("app", f"{username}-password", new_password)
event.set_results({f"{username}-password": new_password})

def _open_port_tcp(self, port: int) -> None:
"""Open the given port.
Expand Down Expand Up @@ -696,8 +713,8 @@ def mongodb_config(self) -> MongoDBConfiguration:
return MongoDBConfiguration(
replset=self.app.name,
database="admin",
username="admin",
password=self.get_secret("app", "password"),
username="operator",
password=self.get_secret("app", "operator-password"),
hosts=set(self._unit_ips),
roles={"default"},
tls_external=external_ca is not None,
Expand Down
16 changes: 8 additions & 8 deletions tests/integration/ha_tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def replica_set_client(replica_ips: List[str], password: str, app=APP_NAME) -> M
hosts = ["{}:{}".format(replica_ip, PORT) for replica_ip in replica_ips]
hosts = ",".join(hosts)

replica_set_uri = f"mongodb://admin:" f"{password}@" f"{hosts}/admin?replicaSet={app}"
replica_set_uri = f"mongodb://operator:" f"{password}@" f"{hosts}/admin?replicaSet={app}"
return MongoClient(replica_set_uri)


Expand Down Expand Up @@ -91,7 +91,7 @@ def unit_uri(ip_address: str, password, app=APP_NAME) -> str:
password: password of database.
app: name of application which has the cluster.
"""
return f"mongodb://admin:" f"{password}@" f"{ip_address}:{PORT}/admin?replicaSet={app}"
return f"mongodb://operator:" f"{password}@" f"{ip_address}:{PORT}/admin?replicaSet={app}"


async def get_password(ops_test: OpsTest, app, down_unit=None) -> str:
Expand All @@ -108,7 +108,7 @@ async def get_password(ops_test: OpsTest, app, down_unit=None) -> str:

action = await ops_test.model.units.get(f"{app}/{unit_id}").run_action("get-password")
action = await action.wait()
return action.results["admin-password"]
return action.results["operator-password"]


async def fetch_primary(
Expand Down Expand Up @@ -250,7 +250,7 @@ async def clear_db_writes(ops_test: OpsTest) -> bool:
password = await get_password(ops_test, app)
hosts = [unit.public_address for unit in ops_test.model.applications[app].units]
hosts = ",".join(hosts)
connection_string = f"mongodb://admin:{password}@{hosts}/admin?replicaSet={app}"
connection_string = f"mongodb://operator:{password}@{hosts}/admin?replicaSet={app}"

client = MongoClient(connection_string)
db = client["new-db"]
Expand All @@ -275,7 +275,7 @@ async def start_continous_writes(ops_test: OpsTest, starting_number: int) -> Non
password = await get_password(ops_test, app)
hosts = [unit.public_address for unit in ops_test.model.applications[app].units]
hosts = ",".join(hosts)
connection_string = f"mongodb://admin:{password}@{hosts}/admin?replicaSet={app}"
connection_string = f"mongodb://operator:{password}@{hosts}/admin?replicaSet={app}"

# run continuous writes in the background.
subprocess.Popen(
Expand Down Expand Up @@ -303,7 +303,7 @@ async def stop_continous_writes(ops_test: OpsTest, down_unit=None) -> int:
password = await get_password(ops_test, app, down_unit)
hosts = [unit.public_address for unit in ops_test.model.applications[app].units]
hosts = ",".join(hosts)
connection_string = f"mongodb://admin:{password}@{hosts}/admin?replicaSet={app}"
connection_string = f"mongodb://operator:{password}@{hosts}/admin?replicaSet={app}"

client = MongoClient(connection_string)
db = client["new-db"]
Expand All @@ -321,7 +321,7 @@ async def count_writes(ops_test: OpsTest, down_unit=None) -> int:
password = await get_password(ops_test, app, down_unit)
hosts = [unit.public_address for unit in ops_test.model.applications[app].units]
hosts = ",".join(hosts)
connection_string = f"mongodb://admin:{password}@{hosts}/admin?replicaSet={app}"
connection_string = f"mongodb://operator:{password}@{hosts}/admin?replicaSet={app}"

client = MongoClient(connection_string)
db = client["new-db"]
Expand All @@ -338,7 +338,7 @@ async def secondary_up_to_date(ops_test: OpsTest, unit_ip, expected_writes) -> b
"""
app = await app_name(ops_test)
password = await get_password(ops_test, app)
connection_string = f"mongodb://admin:{password}@{unit_ip}:{PORT}/admin?"
connection_string = f"mongodb://operator:{password}@{unit_ip}:{PORT}/admin?"
client = MongoClient(connection_string, directConnection=True)

try:
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def unit_uri(ip_address: str, password, app=APP_NAME) -> str:
password: password of database.
app: name of application which has the cluster.
"""
return f"mongodb://admin:" f"{password}@" f"{ip_address}:{PORT}/admin?replicaSet={app}"
return f"mongodb://operator:" f"{password}@" f"{ip_address}:{PORT}/admin?replicaSet={app}"


async def get_password(ops_test: OpsTest, app=APP_NAME) -> str:
Expand All @@ -38,7 +38,7 @@ async def get_password(ops_test: OpsTest, app=APP_NAME) -> str:

action = await ops_test.model.units.get(f"{app}/{unit_id}").run_action("get-password")
action = await action.wait()
return action.results["admin-password"]
return action.results["operator-password"]


@retry(
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ async def test_set_password_action(ops_test: OpsTest) -> None:
unit = await find_unit(ops_test, leader=True)
action = await unit.run_action("set-password")
action = await action.wait()
new_password = action.results["admin-password"]
new_password = action.results["operator-password"]
assert new_password != old_password
new_password_reported = await get_password(ops_test)
assert new_password == new_password_reported
Expand All @@ -152,7 +152,7 @@ async def test_set_password_action(ops_test: OpsTest) -> None:
old_password = await get_password(ops_test)
action = await unit.run_action("set-password", **{"password": "safe_pass"})
action = await action.wait()
new_password = action.results["admin-password"]
new_password = action.results["operator-password"]
assert new_password != old_password
new_password_reported = await get_password(ops_test)
assert "safe_pass" == new_password_reported
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/tls_tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async def mongo_tls_command(ops_test: OpsTest) -> str:
replica_set_hosts = [unit.public_address for unit in ops_test.model.applications[app].units]
password = await get_password(ops_test, app)
hosts = ",".join(replica_set_hosts)
replica_set_uri = f"mongodb://admin:" f"{password}@" f"{hosts}/admin?replicaSet={app}"
replica_set_uri = f"mongodb://operator:" f"{password}@" f"{hosts}/admin?replicaSet={app}"

return (
f"mongo '{replica_set_uri}' --eval 'rs.status()'"
Expand Down
14 changes: 7 additions & 7 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,15 +631,15 @@ def test_start_init_user_after_second_call(self, run, config):
def test_set_password(self, connection):
"""Tests that a new admin password is generated and is returned to the user."""
self.harness.set_leader(True)
original_password = self.harness.charm.app_peer_data["password"]
original_password = self.harness.charm.app_peer_data["operator-password"]
action_event = mock.Mock()
action_event.params = {}
self.harness.charm._on_set_password(action_event)
new_password = self.harness.charm.app_peer_data["password"]
new_password = self.harness.charm.app_peer_data["operator-password"]

# verify app data is updated and results are reported to user
self.assertNotEqual(original_password, new_password)
action_event.set_results.assert_called_with({"admin-password": new_password})
action_event.set_results.assert_called_with({"operator-password": new_password})

@patch_network_get(private_address="1.1.1.1")
@patch("charm.MongoDBConnection")
Expand All @@ -649,18 +649,18 @@ def test_set_password_provided(self, connection):
action_event = mock.Mock()
action_event.params = {"password": "canonical123"}
self.harness.charm._on_set_password(action_event)
new_password = self.harness.charm.app_peer_data["password"]
new_password = self.harness.charm.app_peer_data["operator-password"]

# verify app data is updated and results are reported to user
self.assertEqual("canonical123", new_password)
action_event.set_results.assert_called_with({"admin-password": "canonical123"})
action_event.set_results.assert_called_with({"operator-password": "canonical123"})

@patch_network_get(private_address="1.1.1.1")
@patch("charm.MongoDBConnection")
def test_set_password_failure(self, connection):
"""Tests failure to reset password does not update app data and failure is reported."""
self.harness.set_leader(True)
original_password = self.harness.charm.app_peer_data["password"]
original_password = self.harness.charm.app_peer_data["operator-password"]
action_event = mock.Mock()
action_event.params = {}

Expand All @@ -669,7 +669,7 @@ def test_set_password_failure(self, connection):
exception
)
self.harness.charm._on_set_password(action_event)
current_password = self.harness.charm.app_peer_data["password"]
current_password = self.harness.charm.app_peer_data["operator-password"]

# verify passwords are not updated.
self.assertEqual(current_password, original_password)
Expand Down

0 comments on commit 382e91d

Please sign in to comment.