From 66a167eb42d53f8e12a856c9f0b2d888e57b1742 Mon Sep 17 00:00:00 2001 From: Myles Penner Date: Fri, 31 May 2024 10:25:16 -0700 Subject: [PATCH] Add keystone audit middleware API logging This commit adds Keystone audit middleware API logging to the Glance charm in versions Yoga and newer to allow users to configure their environment for CADF compliance. This feature can be enabled/disabled and is set to 'disabled' by default to avoid bloat in log files. The logging output writes to /var/log/glance/glance-api.log. This commit builds on previous discussions: https://github.com/juju/charm-helpers/pull/808. Related-Pr: https://github.com/juju/charm-helpers/pull/893 func-test-pr: https://github.com/openstack-charmers/zaza-openstack-tests/pull/1212 Closes-Bug: 1856555 Change-Id: Ied08b56cf3c4fa30827d43a50ca7b552db0fa82b --- config.yaml | 5 ++ hooks/glance_utils.py | 19 +++++-- templates/yoga/api-paste.ini | 86 +++++++++++++++++++++++++++++ templates/yoga/api_audit_map.conf | 16 ++++++ templates/yoga/glance-api.conf | 89 +++++++++++++++++++++++++++++++ tests/tests.yaml | 3 ++ unit_tests/test_glance_utils.py | 27 ++++++++++ 7 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 templates/yoga/api-paste.ini create mode 100644 templates/yoga/api_audit_map.conf create mode 100644 templates/yoga/glance-api.conf diff --git a/config.yaml b/config.yaml index ef79421..05d90d5 100644 --- a/config.yaml +++ b/config.yaml @@ -12,6 +12,11 @@ options: default: False description: | Setting this to True will allow supporting services to log to syslog. + audit-middleware: + type: boolean + default: False + description: | + Enable Keystone auditing middleware for logging API calls. openstack-origin: type: string default: bobcat diff --git a/hooks/glance_utils.py b/hooks/glance_utils.py index 90e755a..8053cb8 100644 --- a/hooks/glance_utils.py +++ b/hooks/glance_utils.py @@ -121,13 +121,13 @@ CHARM = "glance" GLANCE_CONF_DIR = "/etc/glance" +GLANCE_AUDIT_MAP = "%s/api_audit_map.conf" % GLANCE_CONF_DIR GLANCE_REGISTRY_CONF = "%s/glance-registry.conf" % GLANCE_CONF_DIR GLANCE_API_CONF = "%s/glance-api.conf" % GLANCE_CONF_DIR GLANCE_SWIFT_CONF = "%s/glance-swift.conf" % GLANCE_CONF_DIR GLANCE_REGISTRY_PASTE = os.path.join(GLANCE_CONF_DIR, 'glance-registry-paste.ini') -GLANCE_API_PASTE = os.path.join(GLANCE_CONF_DIR, - 'glance-api-paste.ini') +GLANCE_API_PASTE = os.path.join(GLANCE_CONF_DIR, 'api-paste.ini') GLANCE_POLICY_FILE = os.path.join(GLANCE_CONF_DIR, "policy.json") # NOTE(ajkavanagh): from Ussuri, glance switched to policy-in-code; this is the # policy.yaml file (as there is not packaged policy.json or .yaml) that is used @@ -204,6 +204,7 @@ def ceph_config_file(): config_file=GLANCE_API_CONF), context.MemcacheContext(), glance_contexts.GlanceImageImportContext(), + context.KeystoneAuditMiddleware(service=CHARM), glance_contexts.ExternalS3Context()], 'services': ['glance-api'] }), @@ -218,6 +219,14 @@ def ceph_config_file(): 'hook_contexts': [], 'services': ['glance-api', 'glance-registry'] }), + (GLANCE_AUDIT_MAP, { + 'hook_contexts': [context.KeystoneAuditMiddleware(service=CHARM)], + 'services': ['glance-api'] + }), + (GLANCE_API_PASTE, { + 'hook_contexts': [context.KeystoneAuditMiddleware(service=CHARM)], + 'services': ['glance-api'] + }), (ceph_config_file(), { 'hook_contexts': [context.CephContext()], 'services': ['glance-api', 'glance-registry'] @@ -257,7 +266,9 @@ def register_configs(): confs = [GLANCE_REGISTRY_CONF, GLANCE_API_CONF, - HAPROXY_CONF] + HAPROXY_CONF, + GLANCE_API_PASTE, + GLANCE_AUDIT_MAP] if relation_ids('ceph'): mkdir(os.path.dirname(ceph_config_file())) @@ -403,6 +414,8 @@ def restart_map(): cmp_release = CompareOpenStackReleases(os_release('glance-common')) for f, ctxt in CONFIG_FILES.items(): + if f == GLANCE_AUDIT_MAP and cmp_release < 'yoga': + continue svcs = [] for svc in ctxt['services']: if cmp_release >= 'stein' and svc == 'glance-registry': diff --git a/templates/yoga/api-paste.ini b/templates/yoga/api-paste.ini new file mode 100644 index 0000000..dc8687b --- /dev/null +++ b/templates/yoga/api-paste.ini @@ -0,0 +1,86 @@ +# Use this pipeline for no auth or image caching - DEFAULT +[pipeline:glance-api] +pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler unauthenticated-context rootapp + +# Use this pipeline for image caching and no auth +[pipeline:glance-api-caching] +pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler unauthenticated-context cache rootapp + +# Use this pipeline for caching w/ management interface but no auth +[pipeline:glance-api-cachemanagement] +pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler unauthenticated-context cache cachemanage rootapp + +# Use this pipeline for keystone auth +[pipeline:glance-api-keystone] +{% if audit_middleware and service_name -%} +pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler authtoken audit context rootapp +{% else %} +pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler authtoken context rootapp +{% endif %} + +# Use this pipeline for keystone auth with image caching +[pipeline:glance-api-keystone+caching] +{% if audit_middleware and service_name -%} +pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler authtoken audit context cache rootapp +{% else %} +pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler authtoken context cache rootapp +{% endif %} + +# Use this pipeline for keystone auth with caching and cache management +[pipeline:glance-api-keystone+cachemanagement] +{% if audit_middleware and service_name -%} +pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler authtoken audit context cache cachemanage rootapp +{% else %} +pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler authtoken context cache cachemanage rootapp +{% endif %} + +[composite:rootapp] +paste.composite_factory = glance.api:root_app_factory +/: apiversions +/v2: apiv2app + +[app:apiversions] +paste.app_factory = glance.api.versions:create_resource + +[app:apiv2app] +paste.app_factory = glance.api.v2.router:API.factory + +[filter:healthcheck] +paste.filter_factory = oslo_middleware:Healthcheck.factory +backends = disable_by_file +disable_by_file_path = /etc/glance/healthcheck_disable + +[filter:versionnegotiation] +paste.filter_factory = glance.api.middleware.version_negotiation:VersionNegotiationFilter.factory + +[filter:cache] +paste.filter_factory = glance.api.middleware.cache:CacheFilter.factory + +[filter:cachemanage] +paste.filter_factory = glance.api.middleware.cache_manage:CacheManageFilter.factory + +[filter:context] +paste.filter_factory = glance.api.middleware.context:ContextMiddleware.factory + +[filter:unauthenticated-context] +paste.filter_factory = glance.api.middleware.context:UnauthenticatedContextMiddleware.factory + +[filter:authtoken] +paste.filter_factory = keystonemiddleware.auth_token:filter_factory +delay_auth_decision = true + +[filter:gzip] +paste.filter_factory = glance.api.middleware.gzip:GzipMiddleware.factory + +[filter:osprofiler] +paste.filter_factory = osprofiler.web:WsgiMiddleware.factory + +[filter:cors] +paste.filter_factory = oslo_middleware.cors:filter_factory +oslo_config_project = glance +oslo_config_program = glance-api + +[filter:http_proxy_to_wsgi] +paste.filter_factory = oslo_middleware:HTTPProxyToWSGI.factory + +{% include "section-filter-audit" %} \ No newline at end of file diff --git a/templates/yoga/api_audit_map.conf b/templates/yoga/api_audit_map.conf new file mode 100644 index 0000000..fc9e461 --- /dev/null +++ b/templates/yoga/api_audit_map.conf @@ -0,0 +1,16 @@ +[DEFAULT] +# default target endpoint type +# should match the endpoint type defined in service catalog +target_endpoint_type = None + +# possible end path of api requests +[path_keywords] +detail = None +file = None +images = image +members = member +tags = tag + +# map endpoint type defined in service catalog to CADF typeURI +[service_endpoints] +image = service/storage/image \ No newline at end of file diff --git a/templates/yoga/glance-api.conf b/templates/yoga/glance-api.conf new file mode 100644 index 0000000..6aed2b4 --- /dev/null +++ b/templates/yoga/glance-api.conf @@ -0,0 +1,89 @@ +[DEFAULT] +verbose = {{ verbose }} +use_syslog = {{ use_syslog }} +debug = {{ debug }} +workers = {{ workers }} +bind_host = {{ bind_host }} + +{% if ext -%} +bind_port = {{ ext }} +{% elif bind_port -%} +bind_port = {{ bind_port }} +{% else -%} +bind_port = 9292 +{% endif -%} + +{% if transport_url %} +transport_url = {{ transport_url }} +{% endif %} + +log_file = /var/log/glance/api.log +backlog = 4096 + +{% if expose_image_locations -%} +show_multiple_locations = {{ expose_image_locations }} +show_image_direct_url = {{ expose_image_locations }} +{% endif -%} + +{% if api_config_flags -%} +{% for key, value in api_config_flags.items() -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif -%} + +delayed_delete = False +scrub_time = 43200 +scrubber_datadir = /var/lib/glance/scrubber +image_cache_dir = /var/lib/glance/image-cache/ +db_enforce_mysql_charset = False + +{% if image_size_cap -%} +image_size_cap = {{ image_size_cap }} +{% endif -%} + +{% if enabled_backends %} +enabled_backends = {{ enabled_backends }} +{% endif %} + +[glance_store] +{% if default_store_backend %} +default_backend = {{ default_store_backend }} +{% endif %} + +[image_format] +disk_formats = {{ disk_formats }} +{% if container_formats -%} +container_formats = {{ container_formats }} +{% endif -%} + +{% include "section-keystone-authtoken-v3only" %} + +{% if auth_host -%} +[paste_deploy] +flavor = keystone +config_file = /etc/glance/api-paste.ini +{% endif %} + +[barbican] +auth_endpoint = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/v3 + +{% include "parts/section-database" %} + +{% include "section-oslo-messaging-rabbit" %} + +{% include "section-oslo-notifications" %} + +{% include "section-oslo-middleware" %} + +{% include "parts/section-storage" %} + +{% for name, cfg in enabled_backend_configs.items() %} +[{{name}}] +{% for key, val in cfg.items() -%} +{{ key }} = {{ val }} +{% endfor -%} +{% endfor%} + +{% include "parts/section-image-import" %} + +{% include "section-audit-middleware-notifications" %} \ No newline at end of file diff --git a/tests/tests.yaml b/tests/tests.yaml index 163abe2..f330fbd 100644 --- a/tests/tests.yaml +++ b/tests/tests.yaml @@ -26,6 +26,7 @@ tests: - zaza.openstack.charm_tests.glance.tests.GlanceCephRGWBackendTest - zaza.openstack.charm_tests.glance.tests.GlanceExternalS3Test - zaza.openstack.charm_tests.glance.tests.GlanceCinderBackendTest + - zaza.openstack.charm_tests.audit.tests.KeystoneAuditMiddlewareTest - zaza.openstack.charm_tests.policyd.tests.GlanceTests - zaza.openstack.charm_tests.ceph.tests.CheckPoolTypes - zaza.openstack.charm_tests.ceph.tests.BlueStoreCompressionCharmOperation @@ -35,6 +36,8 @@ tests: - zaza.openstack.charm_tests.policyd.tests.GlanceTests tests_options: + audit-middleware: + service: glance tempest: full_run: smoke: true diff --git a/unit_tests/test_glance_utils.py b/unit_tests/test_glance_utils.py index ee2de9e..c7e2c0e 100644 --- a/unit_tests/test_glance_utils.py +++ b/unit_tests/test_glance_utils.py @@ -159,6 +159,7 @@ def test_restart_map_rocky(self): (utils.GLANCE_API_CONF, ['glance-api']), (utils.GLANCE_SWIFT_CONF, ['glance-api']), (utils.GLANCE_POLICY_FILE, ['glance-api', 'glance-registry']), + (utils.GLANCE_API_PASTE, ['glance-api']), (utils.ceph_config_file(), ['glance-api', 'glance-registry']), (utils.HAPROXY_CONF, ['haproxy']), (utils.HTTPS_APACHE_CONF, ['apache2']), @@ -181,6 +182,31 @@ def test_restart_map_stein(self): (utils.GLANCE_API_CONF, ['glance-api']), (utils.GLANCE_SWIFT_CONF, ['glance-api']), (utils.GLANCE_POLICY_FILE, ['glance-api']), + (utils.GLANCE_API_PASTE, ['glance-api']), + (utils.ceph_config_file(), ['glance-api']), + (utils.HAPROXY_CONF, ['haproxy']), + (utils.HTTPS_APACHE_CONF, ['apache2']), + (utils.HTTPS_APACHE_24_CONF, ['apache2']), + (utils.APACHE_PORTS_CONF, ['apache2']), + (utils.MEMCACHED_CONF, ['memcached']), + ]) + self.assertEqual(ex_map, utils.restart_map()) + self.enable_memcache.return_value = False + del ex_map[utils.MEMCACHED_CONF] + self.assertEqual(ex_map, utils.restart_map()) + + def test_restart_map_yoga(self): + self.enable_memcache.return_value = True + self.config.side_effect = None + self.service_name.return_value = 'glance' + self.os_release.return_value = 'yoga' + + ex_map = OrderedDict([ + (utils.GLANCE_API_CONF, ['glance-api']), + (utils.GLANCE_SWIFT_CONF, ['glance-api']), + (utils.GLANCE_POLICY_FILE, ['glance-api']), + (utils.GLANCE_AUDIT_MAP, ['glance-api']), + (utils.GLANCE_API_PASTE, ['glance-api']), (utils.ceph_config_file(), ['glance-api']), (utils.HAPROXY_CONF, ['haproxy']), (utils.HTTPS_APACHE_CONF, ['apache2']), @@ -205,6 +231,7 @@ def test_restart_map_stein_ssl(self, isdir): (utils.GLANCE_API_CONF, ['glance-api']), (utils.GLANCE_SWIFT_CONF, ['glance-api']), (utils.GLANCE_POLICY_FILE, ['glance-api']), + (utils.GLANCE_API_PASTE, ['glance-api']), (utils.ceph_config_file(), ['glance-api']), (utils.HAPROXY_CONF, ['haproxy']), (utils.HTTPS_APACHE_CONF, ['apache2']),