Skip to content

Commit

Permalink
Fix metric aggregates as non-admin
Browse files Browse the repository at this point in the history
When using the aggregates API with a metric ID, the RBAC policy
`project_id:%(resource.project_id)s"` fails to authenticate a
non-admin user that is part of the project the metric is also
part of via its resource relationship.

This patch fixes two issues preventing this from working:

* Set `details=True` when listing specified metrics to ensure
  the joined load with the `Metric` and `Resource` models is done.
* Add support for processing oslo.db SQLAlchemy models in
  `gnocchi.rest.api.flatten_dict_to_keypairs` to make sure nested
  attributes are added to the list of targets passed to the
  policy enforcer.
  • Loading branch information
Callum027 committed Feb 11, 2025
1 parent b28f215 commit 39ba9fc
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 2 deletions.
3 changes: 2 additions & 1 deletion gnocchi/rest/aggregates/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,8 @@ def post(self, start=None, stop=None, granularity=None,
"detail": references})

metrics = pecan.request.indexer.list_metrics(
attribute_filter={"in": {"id": metric_ids}})
attribute_filter={"in": {"id": metric_ids}},
details=True)
missing_metric_ids = (set(metric_ids)
- set(str(m.id) for m in metrics))
if missing_metric_ids:
Expand Down
13 changes: 12 additions & 1 deletion gnocchi/rest/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import operator
import uuid

from collections import abc
import jsonpatch
from oslo_utils import strutils
import pecan
Expand Down Expand Up @@ -54,10 +55,20 @@
except ImportError:
PROMETHEUS_SUPPORTED = False

try:
from oslo_db.sqlalchemy import models as sqlalchemy_models
SQLALCHEMY_SUPPORTED = True
except ImportError:
SQLALCHEMY_SUPPORTED = False


ATTRGETTER_GRANULARITY = operator.attrgetter("granularity")
LOG = logging.getLogger(__name__)

FLATTEN_DICT_TYPES = ((abc.Mapping, sqlalchemy_models.ModelBase)
if SQLALCHEMY_SUPPORTED
else (abc.Mapping,))


def arg_to_list(value):
if isinstance(value, list):
Expand Down Expand Up @@ -91,7 +102,7 @@ def flatten_dict_to_keypairs(d, separator=':'):
:param separator: symbol between names
"""
for name, value in sorted(d.items()):
if isinstance(value, dict):
if isinstance(value, FLATTEN_DICT_TYPES):
for subname, subvalue in flatten_dict_to_keypairs(value,
separator):
yield ('%s%s%s' % (name, separator, subname), subvalue)
Expand Down
52 changes: 52 additions & 0 deletions gnocchi/tests/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,58 @@ def test_search_resources_invalid_query(self):
)


class AggregatesTest(RestTest):
def test_get_metric_aggregates_with_another_user(self):
r = self.app.post_json(
"/v1/metric",
params={"archive_policy_name": "medium"},
status=201)
metric_id = r.json['id']
self.app.post_json(
f"/v1/metric/{metric_id}/measures",
params=[{"timestamp": "2013-01-01 12:00:01",
"value": 8},
{"timestamp": "2013-01-01 12:00:02",
"value": 16}])
with self.app.use_another_user():
self.app.post_json(
"/v1/aggregates",
params={"operations": ["metric", metric_id, "mean"]},
status=403)

def test_get_metric_aggregates_with_another_user_allowed(self):
rid = str(uuid.uuid4())
r = self.app.post_json(
"/v1/resource/generic",
params={
"id": rid,
"project_id": TestingApp.PROJECT_ID_2,
"metrics": {
"disk": {"archive_policy_name": "low"},
}
})
metric_id = r.json['metrics']['disk']
self.app.post_json(
f"/v1/metric/{metric_id}/measures",
params=[{"timestamp": "2013-01-01 12:00:01",
"value": 8},
{"timestamp": "2013-01-01 12:00:02",
"value": 16}])
with self.app.use_another_user():
r = self.app.post_json(
"/v1/aggregates",
params={"operations": ["metric", metric_id, "mean"]},
status=200)
aggregates = r.json
self.assertIn(metric_id, aggregates["measures"])
measures = aggregates["measures"][metric_id]
self.assertIn("mean", measures)
self.assertEqual([["2013-01-01T00:00:00+00:00", 86400, 12],
["2013-01-01T12:00:00+00:00", 3600, 12],
["2013-01-01T12:00:00+00:00", 300, 12]],
measures["mean"])


class QueryStringSearchAttrFilterTest(tests_base.TestCase):
def _do_test(self, expr, expected):
req = api.QueryStringSearchAttrFilter._parse(expr)
Expand Down

0 comments on commit 39ba9fc

Please sign in to comment.