Skip to content

Commit

Permalink
Merge pull request #27 from canonical/tls-sanity-checks
Browse files Browse the repository at this point in the history
[DPE-3753] Tls sanity checks
  • Loading branch information
Mehdi-Bendriss authored Mar 26, 2024
2 parents e08b6ea + 3c75d3d commit dd945b6
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 18 deletions.
27 changes: 27 additions & 0 deletions icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 109 additions & 4 deletions lib/charms/mongodb/v0/config_server_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@
DatabaseProvides,
DatabaseRequires,
)
from charms.mongodb.v1.helpers import add_args_to_env, get_mongos_args
from charms.mongodb.v1.mongos import MongosConnection
from ops.charm import CharmBase, EventBase, RelationBrokenEvent
from ops.framework import Object
from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus
from ops.model import (
ActiveStatus,
BlockedStatus,
MaintenanceStatus,
StatusBase,
WaitingStatus,
)

from typing import Optional
from config import Config

logger = logging.getLogger(__name__)
Expand All @@ -38,7 +42,7 @@

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 8
LIBPATCH = 10


class ClusterProvider(Object):
Expand Down Expand Up @@ -232,6 +236,10 @@ def _on_database_created(self, event) -> None:

def _on_relation_changed(self, event) -> None:
"""Starts/restarts monogs with config server information."""
if not self.pass_hook_checks(event):
logger.info("pre-hook checks did not pass, not executing event")
return

key_file_contents = self.database_requires.fetch_relation_field(
event.relation.id, KEYFILE_KEY
)
Expand Down Expand Up @@ -288,6 +296,34 @@ def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
self.charm.remove_connection_info()

# BEGIN: helper functions
def pass_hook_checks(self, event):
"""Runs the pre-hooks checks for ClusterRequirer, returns True if all pass."""
if self.is_mongos_tls_missing():
logger.info(
"Deferring %s. Config-server uses TLS, but mongos does not. Please synchronise encryption methods.",
str(type(event)),
)
event.defer()
return False

if self.is_config_server_tls_missing():
logger.info(
"Deferring %s. mongos uses TLS, but config-server does not. Please synchronise encryption methods.",
str(type(event)),
)
event.defer()
return False

if not self.is_ca_compatible():
logger.info(
"Deferring %s. mongos is integrated to a different CA than the config server. Please use the same CA for all cluster components.",
str(type(event)),
)

event.defer()
return False

return True

def is_mongos_running(self) -> bool:
"""Returns true if mongos service is running."""
Expand Down Expand Up @@ -328,6 +364,22 @@ def update_keyfile(self, key_file_contents: str) -> bool:

return True

def get_tls_statuses(self) -> Optional[StatusBase]:
"""Returns statuses relevant to TLS."""
if self.is_mongos_tls_missing():
return BlockedStatus("mongos requires TLS to be enabled.")

if self.is_config_server_tls_missing():
return BlockedStatus("mongos has TLS enabled, but config-server does not.")

if not self.is_ca_compatible():
logger.error(
"mongos is integrated to a different CA than the config server. Please use the same CA for all cluster components."
)
return BlockedStatus("mongos CA and Config-Server CA don't match.")

return

def get_config_server_name(self) -> Optional[str]:
"""Returns the name of the Juju Application that mongos is using as a config server."""
if not self.model.get_relation(self.relation_name):
Expand All @@ -336,4 +388,57 @@ def get_config_server_name(self) -> Optional[str]:
# metadata.yaml prevents having multiple config servers
return self.model.get_relation(self.relation_name).app.name

def is_ca_compatible(self) -> bool:
"""Returns true if both the mongos and the config server use the same CA."""
config_server_relation = self.charm.model.get_relation(self.relation_name)
# base-case: nothing to compare
if not config_server_relation:
return True

config_server_tls_ca = self.database_requires.fetch_relation_field(
config_server_relation.id, INT_TLS_CA_KEY
)

mongos_tls_ca = self.charm.tls.get_tls_secret(
internal=True, label_name=Config.TLS.SECRET_CA_LABEL
)

# base-case: missing one or more CA's to compare
if not config_server_tls_ca and not mongos_tls_ca:
return True

return config_server_tls_ca == mongos_tls_ca

def is_mongos_tls_missing(self) -> bool:
"""Returns true if the config-server has TLS enabled but mongos does not."""
config_server_relation = self.charm.model.get_relation(self.relation_name)
if not config_server_relation:
return False

mongos_has_tls = self.charm.model.get_relation(Config.TLS.TLS_PEER_RELATION) is not None
config_server_has_tls = (
self.database_requires.fetch_relation_field(config_server_relation.id, INT_TLS_CA_KEY)
is not None
)
if config_server_has_tls and not mongos_has_tls:
return True

return False

def is_config_server_tls_missing(self) -> bool:
"""Returns true if the mongos has TLS enabled but the config-server does not."""
config_server_relation = self.charm.model.get_relation(self.relation_name)
if not config_server_relation:
return False

mongos_has_tls = self.charm.model.get_relation(Config.TLS.TLS_PEER_RELATION) is not None
config_server_has_tls = (
self.database_requires.fetch_relation_field(config_server_relation.id, INT_TLS_CA_KEY)
is not None
)
if not config_server_has_tls and mongos_has_tls:
return True

return False

# END: helper functions
17 changes: 14 additions & 3 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@
from config import Config

import ops
from ops.model import BlockedStatus, MaintenanceStatus, WaitingStatus, Relation
from ops.model import (
BlockedStatus,
MaintenanceStatus,
WaitingStatus,
Relation,
ActiveStatus,
)
from ops.charm import InstallEvent, StartEvent, RelationDepartedEvent

import logging
Expand Down Expand Up @@ -105,12 +111,18 @@ def _on_update_status(self, _):
self.unit.status = BlockedStatus("Missing relation to config-server.")
return

# restart on high loaded databases can be very slow (e.g. up to 10-20 minutes).
if self.cluster.get_tls_statuses():
self.unit.status = self.cluster.get_tls_statuses()
return

# restart on high loaded databases can be very slow (e.g. up to 10-20 minutes).
if not self.cluster.is_mongos_running():
logger.info("mongos has not started yet")
self.unit.status = WaitingStatus("Waiting for mongos to start.")
return

self.unit.status = ActiveStatus()

# END: hook functions

# BEGIN: helper functions
Expand Down Expand Up @@ -267,7 +279,6 @@ def restart_charm_services(self) -> None:
self.start_mongos_service()

def update_mongos_args(self, config_server_db: Optional[str] = None):
"""Updates the starting arguments for the mongos daemon."""
config_server_db = config_server_db or self.config_server_db
if config_server_db is None:
logger.error("cannot start mongos without a config_server_db")
Expand Down
8 changes: 5 additions & 3 deletions tests/integration/tls_tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,19 @@ async def check_mongos_tls_enabled(ops_test: OpsTest) -> None:
await check_tls(ops_test, unit, enabled=True)


async def toggle_tls_mongos(ops_test: OpsTest, enable: bool) -> None:
async def toggle_tls_mongos(
ops_test: OpsTest, enable: bool, certs_app_name: str = CERTS_APP_NAME
) -> None:
"""Toggles TLS on mongos application to the specified enabled state."""
if enable:
await ops_test.model.integrate(
f"{MONGOS_APP_NAME}:{CERT_REL_NAME}",
f"{CERTS_APP_NAME}:{CERT_REL_NAME}",
f"{certs_app_name}:{CERT_REL_NAME}",
)
else:
await ops_test.model.applications[MONGOS_APP_NAME].remove_relation(
f"{MONGOS_APP_NAME}:{CERT_REL_NAME}",
f"{CERTS_APP_NAME}:{CERT_REL_NAME}",
f"{certs_app_name}:{CERT_REL_NAME}",
)


Expand Down
76 changes: 71 additions & 5 deletions tests/integration/tls_tests/test_tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
MONGODB_CHARM_NAME = "mongodb"
CONFIG_SERVER_APP_NAME = "config-server"
SHARD_APP_NAME = "shard"
CLUSTER_COMPONENTS = [MONGOS_APP_NAME, CONFIG_SERVER_APP_NAME, SHARD_APP_NAME]
CLUSTER_COMPONENTS = [CONFIG_SERVER_APP_NAME, SHARD_APP_NAME]
CERT_REL_NAME = "certificates"
SHARD_REL_NAME = "sharding"
CLUSTER_REL_NAME = "cluster"
CONFIG_SERVER_REL_NAME = "config-server"
CERTS_APP_NAME = "self-signed-certificates"
DIFFERENT_CERTS_APP_NAME = "self-signed-certificates-separate"
TIMEOUT = 15 * 60


Expand All @@ -39,20 +40,42 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None:
@pytest.mark.abort_on_fail
async def test_mongos_tls_enabled(ops_test: OpsTest) -> None:
"""Tests that mongos charm can enable TLS."""
# await integrate_cluster_with_tls(ops_test)
# await integrate_mongos_with_tls(ops_test)

await ops_test.model.wait_for_idle(
apps=[MONGOS_APP_NAME],
idle_period=20,
timeout=TIMEOUT,
raise_on_blocked=False,
status="blocked",
)

mongos_unit = ops_test.model.applications[MONGOS_APP_NAME].units[0]
assert (
mongos_unit.workload_status_message
== "mongos has TLS enabled, but config-server does not."
), "mongos fails to report TLS inconsistencies."

await integrate_cluster_with_tls(ops_test)

await check_mongos_tls_enabled(ops_test)


@pytest.mark.skip("Wait until TLS sanity check functionality is implemented")
@pytest.mark.skip("Wait new MongoDB charm is published.")
@pytest.mark.group(1)
@pytest.mark.abort_on_fail
async def test_mongos_tls_disabled(ops_test: OpsTest) -> None:
"""Tests that mongos charm can disable TLS."""
await toggle_tls_mongos(ops_test, enable=False)
await check_mongos_tls_disabled(ops_test)

mongos_unit = ops_test.model.applications[MONGOS_APP_NAME].units[0]
assert (
mongos_unit.workload_status_message == "mongos requires TLS to be enabled."
), "mongos fails to report TLS inconsistencies."


@pytest.mark.skip("Wait until TLS sanity check functionality is implemented")
@pytest.mark.skip("Wait new MongoDB charm is published.")
@pytest.mark.group(1)
@pytest.mark.abort_on_fail
async def test_tls_reenabled(ops_test: OpsTest) -> None:
Expand All @@ -61,6 +84,41 @@ async def test_tls_reenabled(ops_test: OpsTest) -> None:
await check_mongos_tls_enabled(ops_test)


@pytest.mark.skip("Wait new MongoDB charm is published.")
@pytest.mark.group(1)
@pytest.mark.abort_on_fail
async def test_mongos_tls_ca_mismatch(ops_test: OpsTest) -> None:
"""Tests that mongos charm can disable TLS."""
await toggle_tls_mongos(ops_test, enable=False)
await ops_test.model.deploy(
CERTS_APP_NAME, application_name=DIFFERENT_CERTS_APP_NAME, channel="stable"
)
await ops_test.model.wait_for_idle(
apps=[DIFFERENT_CERTS_APP_NAME],
idle_period=10,
raise_on_blocked=False,
status="active",
timeout=TIMEOUT,
)

await toggle_tls_mongos(
ops_test, enable=True, certs_app_name=DIFFERENT_CERTS_APP_NAME
)

await ops_test.model.wait_for_idle(
apps=[MONGOS_APP_NAME],
idle_period=20,
raise_on_blocked=False,
timeout=TIMEOUT,
)

mongos_unit = ops_test.model.applications[MONGOS_APP_NAME].units[0]
assert (
mongos_unit.workload_status_message
== "mongos CA and Config-Server CA don't match."
), "mongos fails to report mismatch in CA."


async def deploy_cluster(ops_test: OpsTest) -> None:
"""Deploys the necessary cluster components"""
application_charm = await ops_test.build_charm("tests/integration/application")
Expand Down Expand Up @@ -153,6 +211,14 @@ async def deploy_tls(ops_test: OpsTest) -> None:
)


async def integrate_mongos_with_tls(ops_test: OpsTest) -> None:
"""Integrate mongos to the TLS interface."""
await ops_test.model.integrate(
f"{MONGOS_APP_NAME}:{CERT_REL_NAME}",
f"{CERTS_APP_NAME}:{CERT_REL_NAME}",
)


async def integrate_cluster_with_tls(ops_test: OpsTest) -> None:
"""Integrate cluster components to the TLS interface."""
for cluster_component in CLUSTER_COMPONENTS:
Expand All @@ -162,7 +228,7 @@ async def integrate_cluster_with_tls(ops_test: OpsTest) -> None:
)

await ops_test.model.wait_for_idle(
apps=[CLUSTER_COMPONENTS],
apps=CLUSTER_COMPONENTS,
idle_period=20,
timeout=TIMEOUT,
raise_on_blocked=False,
Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def test_status_shows_mongos_waiting(self):
"""Tests when mongos accurately reports waiting status."""
cluster_mock = mock.Mock()
cluster_mock.is_mongos_running.return_value = False
cluster_mock.get_tls_statuses.return_value = None
self.harness.charm.cluster = cluster_mock

# A running config server is a requirement to start for mongos
Expand Down
Loading

0 comments on commit dd945b6

Please sign in to comment.