diff --git a/aodh/api/controllers/v2/alarm_rules/gnocchi.py b/aodh/api/controllers/v2/alarm_rules/gnocchi.py index 6576fc2bf..25663d2a0 100644 --- a/aodh/api/controllers/v2/alarm_rules/gnocchi.py +++ b/aodh/api/controllers/v2/alarm_rules/gnocchi.py @@ -18,6 +18,8 @@ import threading import cachetools from gnocchiclient import client from gnocchiclient import exceptions +from keystoneauth1 import exceptions as ka_exceptions +from oslo_config import cfg from oslo_serialization import jsonutils import pecan import wsme @@ -28,6 +30,14 @@ from aodh.api.controllers.v2 import utils as v2_utils from aodh import keystone_client +GNOCCHI_OPTS = [ + cfg.StrOpt('gnocchi_external_project_owner', + default="service", + help='Project name of resources creator in Gnocchi. ' + '(For example the Ceilometer project name'), +] + + class GnocchiUnavailable(Exception): code = 503 @@ -122,6 +132,20 @@ class AggregationMetricByResourcesLookupRule(AlarmGnocchiThresholdRule): 'resource_type']) return rule + cache = cachetools.TTLCache(maxsize=1, ttl=3600) + lock = threading.RLock() + + @staticmethod + @cachetools.cached(cache, lock=lock) + def get_external_project_owner(): + kc = keystone_client.get_client(pecan.request.cfg) + project_name = pecan.request.cfg.api.gnocchi_external_project_owner + try: + project = kc.projects.find(name=project_name) + return project.id + except ka_exceptions.NotFound: + return None + @classmethod def validate_alarm(cls, alarm): super(AggregationMetricByResourcesLookupRule, @@ -135,14 +159,27 @@ class AggregationMetricByResourcesLookupRule(AlarmGnocchiThresholdRule): except ValueError: raise wsme.exc.InvalidInput('rule/query', rule.query) + conf = pecan.request.cfg + # Scope the alarm to the project id if needed auth_project = v2_utils.get_auth_project(alarm.project_id) if auth_project: - query = {"and": [{"=": {"created_by_project_id": auth_project}}, - query]} + + perms_filter = {"=": {"created_by_project_id": auth_project}} + + external_project_owner = cls.get_external_project_owner() + if external_project_owner: + perms_filter = {"or": [ + perms_filter, + {"and": [ + {"=": {"created_by_project_id": + external_project_owner}}, + {"=": {"project_id": auth_project}}]} + ]} + + query = {"and": [perms_filter, query]} rule.query = jsonutils.dumps(query) - conf = pecan.request.cfg gnocchi_client = client.Client( '1', keystone_client.get_session(conf), interface=conf.service_credentials.interface, diff --git a/aodh/opts.py b/aodh/opts.py index dbc02f2d1..2f434e6fd 100644 --- a/aodh/opts.py +++ b/aodh/opts.py @@ -16,6 +16,7 @@ import itertools from keystoneauth1 import loading import aodh.api +import aodh.api.controllers.v2.alarm_rules.gnocchi import aodh.api.controllers.v2.alarms import aodh.coordination import aodh.evaluator @@ -42,6 +43,7 @@ def list_opts(): ('api', itertools.chain( aodh.api.OPTS, + aodh.api.controllers.v2.alarm_rules.gnocchi.GNOCCHI_OPTS, aodh.api.controllers.v2.alarms.ALARM_API_OPTS)), ('coordination', aodh.coordination.OPTS), ('database', aodh.storage.OPTS), diff --git a/aodh/tests/functional/api/v2/test_alarm_scenarios.py b/aodh/tests/functional/api/v2/test_alarm_scenarios.py index d89dd894b..9b801d7b1 100644 --- a/aodh/tests/functional/api/v2/test_alarm_scenarios.py +++ b/aodh/tests/functional/api/v2/test_alarm_scenarios.py @@ -2607,7 +2607,9 @@ class TestAlarmsRuleGnocchi(TestAlarmsBase): self.assertEqual(1, len(alarms)) self._verify_alarm(json, alarms[0]) - def test_post_gnocchi_aggregation_alarm_project_constraint(self): + @mock.patch('aodh.keystone_client.get_client') + def test_post_gnocchi_aggregation_alarm_project_constraint(self, + get_client): json = { 'enabled': False, 'name': 'project_constraint', @@ -2630,10 +2632,21 @@ class TestAlarmsRuleGnocchi(TestAlarmsBase): } } - expected_query = {"and": [{"=": {"created_by_project_id": - self.auth_headers['X-Project-Id']}}, - {"=": {"server_group": - "my_autoscaling_group"}}]} + expected_query = {"and": [ + {"or": [ + {"=": {"created_by_project_id": + self.auth_headers['X-Project-Id']}}, + {"and": [ + {"=": {"created_by_project_id": ""}}, + {"=": {"project_id": self.auth_headers['X-Project-Id']}} + ]}, + ]}, + {"=": {"server_group": "my_autoscaling_group"}}, + ]} + + ks_client = mock.Mock() + ks_client.projects.find.return_value = mock.Mock(id='') + get_client.return_value = ks_client with mock.patch('aodh.api.controllers.v2.alarm_rules.' 'gnocchi.client') as clientlib: diff --git a/releasenotes/notes/gnocchi-external-resource-owner-3fad253d30746b0d.yaml b/releasenotes/notes/gnocchi-external-resource-owner-3fad253d30746b0d.yaml new file mode 100644 index 000000000..45a2997a3 --- /dev/null +++ b/releasenotes/notes/gnocchi-external-resource-owner-3fad253d30746b0d.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + When an unprivileged user want to access to Gnocchi resources created by + Ceilometer, that doesn't work because the filter scope the Gnocchi query to + resource owner to the user. To fix we introduce a new configuration option + "gnocchi_external_project_owner" set by default to "service". The new + filter now allow two kind of Gnocchi resources: + * owned by the user project + * owned by "gnocchi_external_project_owner" and the orignal project_id of + the resource is the user project.