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']),