rest: introduce auth_helper to filter resources

This introduce a new plugin system called auth_helper, that can be used
to decide how to filter resources depending on the authentication done.

That does not change the current filtering rules applied with Keystone
authentication, but it removes the artificial filtering that was done
when no authentication method was configured.

Change-Id: I8b6834a10812f16aed808d3a219be9fd86214f4e
This commit is contained in:
Julien Danjou 2016-10-20 15:24:16 +02:00
parent 98a49616c2
commit da0498e3f6
10 changed files with 93 additions and 54 deletions

View File

@ -58,8 +58,8 @@ Gnocchi provides these indexer drivers:
Configuring authentication
-----------------------------
The API server supports different authentication methods. `OpenStack Keystone`_
is supported but by default, no authentication is enabled. If you successfully
The API server supports different authentication methods: `noauth` (the
default) or `keystone` to use `OpenStack Keystone`_. If you successfully
installed the `keystone` flavor using `pip` (see :ref:`installation`), you can
set `api.auth_mode` to `keystone` to enable Keystone authentication.

View File

@ -18,6 +18,7 @@ import uuid
from oslo_config import cfg
from oslo_middleware import cors
from stevedore import extension
import gnocchi.archive_policy
import gnocchi.indexer
@ -69,6 +70,9 @@ def list_opts():
"rest", "api-paste.ini")),
help='Path to API Paste configuration.'),
cfg.StrOpt('auth_mode',
default="noauth",
choices=extension.ExtensionManager(
"gnocchi.rest.auth_helper").names(),
help='Authentication mode to use.'),
cfg.IntOpt('max_limit',
default=1000,

View File

@ -90,44 +90,6 @@ def enforce(rule, target):
abort(403)
def _get_list_resource_policy_filter(rule, resource_type, user, project):
try:
# Check if the policy allows the user to list any resource
enforce(rule, {
"resource_type": resource_type,
})
except webob.exc.HTTPForbidden:
policy_filter = []
try:
# Check if the policy allows the user to list resources linked
# to their project
enforce(rule, {
"resource_type": resource_type,
"project_id": project,
})
except webob.exc.HTTPForbidden:
pass
else:
policy_filter.append({"=": {"project_id": project}})
try:
# Check if the policy allows the user to list resources linked
# to their created_by_project
enforce(rule, {
"resource_type": resource_type,
"created_by_project_id": project,
})
except webob.exc.HTTPForbidden:
pass
else:
policy_filter.append({"=": {"created_by_project_id": project}})
if not policy_filter:
# We need to have at least one policy filter in place
abort(403, "Insufficient privileges")
return {"or": policy_filter}
def set_resp_location_hdr(location):
location = '%s%s' % (pecan.request.script_name, location)
# NOTE(sileht): according the pep-3333 the headers must be
@ -1035,9 +997,8 @@ class ResourcesController(rest.RestController):
history = get_history(kwargs)
pagination_opts = get_pagination_options(
kwargs, RESOURCE_DEFAULT_PAGINATION)
user, project = get_user_and_project()
policy_filter = _get_list_resource_policy_filter(
"list resource", self._resource_type, user, project)
policy_filter = pecan.request.auth_helper.get_resource_policy_filter(
"list resource", self._resource_type)
try:
# FIXME(sileht): next API version should returns
@ -1065,9 +1026,8 @@ class ResourcesController(rest.RestController):
abort(400, "caution: the query can not be empty, or it will \
delete entire database")
user, project = get_user_and_project()
policy_filter = _get_list_resource_policy_filter(
"delete resources", self._resource_type, user, project)
policy_filter = pecan.request.auth_helper.get_resource_policy_filter(
"delete resources", self._resource_type)
if policy_filter:
attr_filter = {"and": [policy_filter, attr_filter]}
@ -1238,9 +1198,8 @@ class SearchResourceTypeController(rest.RestController):
pagination_opts = get_pagination_options(
kwargs, RESOURCE_DEFAULT_PAGINATION)
user, project = get_user_and_project()
policy_filter = _get_list_resource_policy_filter(
"search resource", self._resource_type, user, project)
policy_filter = pecan.request.auth_helper.get_resource_policy_filter(
"search resource", self._resource_type)
if policy_filter:
if attr_filter:
attr_filter = {"and": [

View File

@ -1,4 +1,4 @@
[composite:gnocchi]
[composite:gnocchi+noauth]
use = egg:Paste#urlmap
/ = gnocchiversions_pipeline
/v1 = gnocchiv1+noauth

View File

@ -23,6 +23,7 @@ from oslo_policy import policy
from paste import deploy
import pecan
from pecan import jsonify
from stevedore import driver
import webob.exc
from gnocchi import exceptions
@ -46,12 +47,16 @@ class GnocchiHook(pecan.hooks.PecanHook):
self.indexer = indexer
self.conf = conf
self.policy_enforcer = policy.Enforcer(conf)
self.auth_helper = driver.DriverManager("gnocchi.rest.auth_helper",
conf.api.auth_mode,
invoke_on_load=True).driver
def on_route(self, state):
state.request.storage = self.storage
state.request.indexer = self.indexer
state.request.conf = self.conf
state.request.policy_enforcer = self.policy_enforcer
state.request.auth_helper = self.auth_helper
class NotImplementedMiddleware(object):
@ -102,8 +107,7 @@ def load_app(conf, indexer=None, storage=None,
APPCONFIGS[configkey] = config
LOG.info("WSGI config used: %s", cfg_path)
appname = "gnocchi" + ("+" + conf.api.auth_mode
if conf.api.auth_mode else "")
appname = "gnocchi+" + conf.api.auth_mode
app = deploy.loadapp("config:" + cfg_path, name=appname,
global_conf={'configkey': configkey})
return cors.CORS(app, conf=conf)

View File

@ -0,0 +1,63 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2016 Red Hat, Inc.
# Copyright © 2014-2015 eNovance
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import webob
from gnocchi import rest
class KeystoneAuthHelper(object):
@staticmethod
def get_resource_policy_filter(rule, resource_type):
try:
# Check if the policy allows the user to list any resource
rest.enforce(rule, {
"resource_type": resource_type,
})
except webob.exc.HTTPForbidden:
user, project = rest.get_user_and_project()
policy_filter = []
try:
# Check if the policy allows the user to list resources linked
# to their project
rest.enforce(rule, {
"resource_type": resource_type,
"project_id": project,
})
except webob.exc.HTTPForbidden:
pass
else:
policy_filter.append({"=": {"project_id": project}})
try:
# Check if the policy allows the user to list resources linked
# to their created_by_project
rest.enforce(rule, {
"resource_type": resource_type,
"created_by_project_id": project,
})
except webob.exc.HTTPForbidden:
pass
else:
policy_filter.append({"=": {"created_by_project_id": project}})
if not policy_filter:
# We need to have at least one policy filter in place
rest.abort(403, "Insufficient privileges")
return {"or": policy_filter}
NoAuthHelper = KeystoneAuthHelper

View File

@ -112,7 +112,7 @@ class ConfigFixture(fixture.GabbiFixture):
# Set pagination to a testable value
conf.set_override('max_limit', 7, 'api')
# Those tests do not use any auth
conf.set_override("auth_mode", None, 'api')
conf.set_override("auth_mode", "noauth", 'api')
self.index = index

View File

@ -155,7 +155,7 @@ class RestTest(tests_base.TestCase, testscenarios.TestWithScenarios):
if self.auth:
self.conf.set_override("auth_mode", "keystone", group="api")
else:
self.conf.set_override("auth_mode", None, group="api")
self.conf.set_override("auth_mode", "noauth", group="api")
self.app = TestingApp(app.load_app(conf=self.conf,
indexer=self.index,

View File

@ -0,0 +1,5 @@
---
features:
- >-
The REST API authentication mechanism is now pluggable. You can write your
own plugin to specify how segregation and policy should be enforced.

View File

@ -122,6 +122,10 @@ gnocchi.indexer =
gnocchi.aggregates =
moving-average = gnocchi.aggregates.moving_stats:MovingAverage
gnocchi.rest.auth_helper =
noauth = gnocchi.rest.auth_helper:NoAuthHelper
keystone = gnocchi.rest.auth_helper:KeystoneAuthHelper
console_scripts =
gnocchi-upgrade = gnocchi.cli:upgrade
gnocchi-statsd = gnocchi.cli:statsd