Add config for password policy, audit and motd

This change basically takes a number of dashboard configuration
options which are managed via `ceph dasboard key value` and
exposes them as charm config options.

Change-Id: I92e0948e36f4156686c908f86b5e2398b504a742
This commit is contained in:
Liam Young 2021-08-12 10:01:27 +00:00
parent 765c7cfd58
commit 0c279b8ad5
3 changed files with 267 additions and 13 deletions

View File

@ -2,9 +2,79 @@
# See LICENSE file for licensing details.
options:
debug:
type: boolean
default: False
description: |
Control debug mode. It is recommended that debug be disabled in
production deployments.
public-hostname:
type: string
default:
description: |
The hostname or address of the public endpoints created for the
dashboard
enable-password-policy:
type: boolean
default: True
description: Enable password policy
password-policy-check-length:
type: boolean
default: True
description: |
Reject password if it is shorter then password-policy-min-length
password-policy-check-oldpwd:
type: boolean
default: True
description: Reject password if it matches previous password.
password-policy-check-username:
type: boolean
default: True
description: Reject password if username is included in password.
password-policy-check-exclusion-list:
type: boolean
default: True
description: Reject password if it contains a word from a forbidden list.
password-policy-check-complexity:
type: boolean
default: True
description: |
Check password meets a complexity score of password-policy-min-complexity.
See https://docs.ceph.com/en/latest/mgr/dashboard/#password-policy
password-policy-check-sequential-chars:
type: boolean
default: True
description: |
Reject password if it contains a sequence of sequential characters. e.g.
a password containing '123' or 'efg' would be rejected.
password-policy-check-repetitive-chars:
type: boolean
default: True
description: |
Reject password if password contains consecutive repeating charachters.
password-policy-min-length:
type: int
default: 8
description: Set minimum password length.
password-policy-min-complexity:
type: int
default: 10
description: |
Set minimum password complexity score.
See https://docs.ceph.com/en/latest/mgr/dashboard/#password-policy
audit-api-enabled:
type: boolean
default: False
description: |
Log requests made to the dashboard REST API to the Ceph audit log.
audit-api-log-payload:
type: boolean
default: True
description: |
Include payload in Ceph audit logs. audit-api-enabled must be set to True
to enable this.,
motd:
type: string
default: ""
description: |
Message of the day settings. Should be in the format "severity|expires|message". Set to "" to disable.

View File

@ -13,6 +13,8 @@ from ops.framework import StoredState
from ops.main import main
from ops.model import ActiveStatus, BlockedStatus, StatusBase
from ops.charm import ActionEvent
from typing import List, Union
import interface_tls_certificates.ca_client as ca_client
import re
import secrets
@ -24,6 +26,7 @@ import interface_dashboard
import interface_api_endpoints
import cryptography.hazmat.primitives.serialization as serialization
import charms_ceph.utils as ceph_utils
import charmhelpers.core.host as ch_host
from pathlib import Path
@ -44,6 +47,98 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm):
'/usr/local/share/ca-certificates/vault_ca_cert_dashboard.crt')
TLS_PORT = 8443
class CharmCephOption():
"""Manage a charm option to ceph command to manage that option"""
def __init__(self, charm_option_name, ceph_option_name,
min_version=None):
self.charm_option_name = charm_option_name
self.ceph_option_name = ceph_option_name
self.min_version = min_version
def is_supported(self) -> bool:
"""Is the option supported on this unit"""
if self.min_version:
return self.minimum_supported(self.min_version)
return True
def minimum_supported(self, supported_version: str) -> bool:
"""Check if installed Ceph release is >= to supported_version"""
return ch_host.cmp_pkgrevno('ceph-common', supported_version) < 1
def convert_option(self, value: Union[bool, str, int]) -> List[str]:
"""Convert a value to the corresponding value part of the ceph
dashboard command"""
return [str(value)]
def ceph_command(self, value: List[str]) -> List[str]:
"""Shell command to set option to desired value"""
cmd = ['ceph', 'dashboard', self.ceph_option_name]
cmd.extend(self.convert_option(value))
return cmd
class DebugOption(CharmCephOption):
def convert_option(self, value):
"""Convert charm True/False to enable/disable"""
if value:
return ['enable']
else:
return ['disable']
class MOTDOption(CharmCephOption):
def convert_option(self, value):
"""Split motd charm option into ['severity', 'time', 'message']"""
if value:
return value.split('|')
else:
return ['clear']
CHARM_TO_CEPH_OPTIONS = [
DebugOption('debug', 'debug'),
CharmCephOption(
'enable-password-policy',
'set-pwd-policy-enabled'),
CharmCephOption(
'password-policy-check-length',
'set-pwd-policy-check-length-enabled'),
CharmCephOption(
'password-policy-check-oldpwd',
'set-pwd-policy-check-oldpwd-enabled'),
CharmCephOption(
'password-policy-check-username',
'set-pwd-policy-check-username-enabled'),
CharmCephOption(
'password-policy-check-exclusion-list',
'set-pwd-policy-check-exclusion-list-enabled'),
CharmCephOption(
'password-policy-check-complexity',
'set-pwd-policy-check-complexity-enabled'),
CharmCephOption(
'password-policy-check-sequential-chars',
'set-pwd-policy-check-sequential-chars-enabled'),
CharmCephOption(
'password-policy-check-repetitive-chars',
'set-pwd-policy-check-repetitive-chars-enabled'),
CharmCephOption(
'password-policy-min-length',
'set-pwd-policy-min-length'),
CharmCephOption(
'password-policy-min-complexity',
'set-pwd-policy-min-complexity'),
CharmCephOption(
'audit-api-enabled',
'set-audit-api-enabled'),
CharmCephOption(
'audit-api-log-payload',
'set-audit-api-log-payload'),
MOTDOption(
'motd',
'motd',
min_version='15.2.14')
]
def __init__(self, *args) -> None:
"""Setup adapters and observers."""
super().__init__(*args)
@ -113,6 +208,33 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm):
ceph_utils.mgr_disable_dashboard()
ceph_utils.mgr_enable_dashboard()
def _run_cmd(self, cmd: List[str]) -> None:
"""Run command in subprocess
`cmd` The command to run
"""
try:
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
logging.exception("Command failed: {}".format(exc.output))
def _apply_ceph_config_from_charm_config(self) -> None:
"""Read charm config and apply settings to dashboard config"""
for option in self.CHARM_TO_CEPH_OPTIONS:
try:
value = self.config[option.charm_option_name]
except KeyError:
logging.error(
"Unknown charm option {}, skipping".format(
option.charm_option_name))
continue
if option.is_supported():
self._run_cmd(option.ceph_command(value))
else:
logging.warning(
"Skipping charm option {}, not supported".format(
option.charm_option_name))
def _configure_dashboard(self, _) -> None:
"""Configure dashboard"""
if not self.mon.mons_ready:
@ -120,6 +242,7 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm):
return
if self.unit.is_leader() and not ceph_utils.is_dashboard_enabled():
ceph_utils.mgr_enable_dashboard()
self._apply_ceph_config_from_charm_config()
ceph_utils.mgr_config_set(
'mgr/dashboard/{hostname}/server_addr'.format(
hostname=socket.gethostname()),

View File

@ -155,12 +155,19 @@ class TestCephDashboardCharmBase(CharmTestCase):
PATCHES = [
'ceph_utils',
'socket',
'subprocess'
'subprocess',
'ch_host',
]
def setUp(self):
super().setUp(charm, self.PATCHES)
self.harness = Harness(
self.harness = self.get_harness()
self.socket.gethostname.return_value = 'server1'
self.socket.getfqdn.return_value = 'server1.local'
def get_harness(self):
_harness = Harness(
_CephDashboardCharm,
)
@ -178,24 +185,78 @@ class TestCephDashboardCharmBase(CharmTestCase):
'egress-subnets': ['10.0.0.0/24']}
return network_data
self.harness._backend = _TestingOPSModelBackend(
self.harness._unit_name, self.harness._meta)
self.harness._model = model.Model(
self.harness._meta,
self.harness._backend)
self.harness._framework = framework.Framework(
_harness._backend = _TestingOPSModelBackend(
_harness._unit_name, _harness._meta)
_harness._model = model.Model(
_harness._meta,
_harness._backend)
_harness._framework = framework.Framework(
":memory:",
self.harness._charm_dir,
self.harness._meta,
self.harness._model)
_harness._charm_dir,
_harness._meta,
_harness._model)
# END Workaround
self.socket.gethostname.return_value = 'server1'
self.socket.getfqdn.return_value = 'server1.local'
return _harness
def test_init(self):
self.harness.begin()
self.assertFalse(self.harness.charm._stored.is_started)
def test_charm_config(self):
self.ceph_utils.is_dashboard_enabled.return_value = True
self.ch_host.cmp_pkgrevno.return_value = 0
basic_boolean = [
('enable-password-policy', 'set-pwd-policy-enabled'),
('password-policy-check-length',
'set-pwd-policy-check-length-enabled'),
('password-policy-check-oldpwd',
'set-pwd-policy-check-oldpwd-enabled'),
('password-policy-check-username',
'set-pwd-policy-check-username-enabled'),
('password-policy-check-exclusion-list',
'set-pwd-policy-check-exclusion-list-enabled'),
('password-policy-check-complexity',
'set-pwd-policy-check-complexity-enabled'),
('password-policy-check-sequential-chars',
'set-pwd-policy-check-sequential-chars-enabled'),
('password-policy-check-repetitive-chars',
'set-pwd-policy-check-repetitive-chars-enabled'),
('audit-api-enabled',
'set-audit-api-enabled'),
('audit-api-log-payload',
'set-audit-api-log-payload')]
expect = []
for charm_option, ceph_option in basic_boolean:
expect.append((charm_option, True, [ceph_option, 'True']))
expect.append((charm_option, False, [ceph_option, 'False']))
expect.extend([
('debug', True, ['debug', 'enable']),
('debug', False, ['debug', 'disable'])])
expect.extend([
('motd', 'warning|5w|enough is enough', ['motd', 'warning', '5w',
'enough is enough']),
('motd', '', ['motd', 'clear'])])
base_cmd = ['ceph', 'dashboard']
for charm_option, charm_value, expected_options in expect:
_harness = self.get_harness()
rel_id = _harness.add_relation('dashboard', 'ceph-mon')
_harness.add_relation_unit(
rel_id,
'ceph-mon/0')
_harness.update_relation_data(
rel_id,
'ceph-mon/0',
{
'mon-ready': 'True'})
_harness.begin()
expected_cmd = base_cmd + expected_options
self.subprocess.check_output.reset_mock()
_harness.update_config(
key_values={charm_option: charm_value})
self.subprocess.check_output.assert_called_once_with(
expected_cmd,
stderr=self.subprocess.STDOUT)
def test__on_ca_available(self):
rel_id = self.harness.add_relation('certificates', 'vault')
self.harness.begin()