Introduce "basic" authentication mechanism

Change-Id: I369de25a656fa56e960cfba2b768ea10eedd3957
This commit is contained in:
Julien Danjou 2016-12-15 11:40:44 +01:00
parent 63e1cd95e7
commit 9da4e07b94
13 changed files with 138 additions and 74 deletions

View File

@ -58,10 +58,11 @@ Gnocchi provides these indexer drivers:
Configuring authentication
-----------------------------
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.
The API server supports different authentication methods: `basic` (the default)
which uses the standard HTTP `Authorization` header 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.
.. _`Paste Deployment`: http://pythonpaste.org/deploy/
.. _`OpenStack Keystone`: http://launchpad.net/keystone

View File

@ -5,19 +5,18 @@
Authentication
==============
By default, no authentication is configured in Gnocchi. You need to provides
these headers in your HTTP requests:
By default, the authentication is configured to the "basic" mode. You need to
provide an `Authorization' header in your HTTP requests with a valid username
(the password is not used). The "admin" password is granted all privileges,
whereas any other username is recognize as having standard permissions.
* X-User-Id
* X-Project-Id
You can customize permissions by specifying a different `policy_file` than the
default one.
The `X-Roles` header can also be provided in order to match role based ACL
specified in `policy.json`, as `X-Domain-Id` to match domain based ACL.
If you enable the OpenStack Keystone middleware, you only need to authenticate
against Keystone and provide `X-Auth-Token` header with a valid token for each
request sent to Gnocchi. The headers mentioned above will be filled
automatically based on your Keystone authorizations.
If you set the `api.auth_mode` value to `keystone`, the OpenStack Keystone
middleware will be enabled for authentication. It is then needed to
authenticate against Keystone and provide a `X-Auth-Token` header with a valid
token for each request sent to Gnocchi's API.
Metrics
=======

View File

@ -32,7 +32,7 @@ _RUN = False
def _setup_test_app():
t = test_rest.RestTest()
t.auth = True
t.auth_mode = "basic"
t.setUpClass()
t.setUp()
return t.app

View File

@ -85,7 +85,7 @@ def list_opts():
"rest", "api-paste.ini")),
help='Path to API Paste configuration.'),
cfg.StrOpt('auth_mode',
default="noauth",
default="basic",
choices=extension.ExtensionManager(
"gnocchi.rest.auth_helper").names(),
help='Authentication mode to use.'),

View File

@ -4,6 +4,12 @@ use = egg:Paste#urlmap
/v1 = gnocchiv1+noauth
/healthcheck = healthcheck
[composite:gnocchi+basic]
use = egg:Paste#urlmap
/ = gnocchiversions_pipeline
/v1 = gnocchiv1+noauth
/healthcheck = healthcheck
[composite:gnocchi+keystone]
use = egg:Paste#urlmap
/ = gnocchiversions_pipeline

View File

@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import webob
import werkzeug.http
from gnocchi import rest
@ -97,3 +98,27 @@ class NoAuthHelper(KeystoneAuthHelper):
return user_id
if project_id:
return project_id
class BasicAuthHelper(object):
@staticmethod
def get_current_user(headers):
auth = werkzeug.http.parse_authorization_header(
headers.get("Authorization"))
if auth is None:
rest.abort(401)
return auth.username
def get_auth_info(self, headers):
user = self.get_current_user(headers)
roles = []
if user == "admin":
roles.append("admin")
return {
"user": user,
"roles": roles
}
@staticmethod
def get_resource_policy_filter(headers, rule, resource_type):
return None

View File

@ -1,5 +1,5 @@
{
"admin_or_creator": "role:admin or project_id:%(created_by_project_id)s",
"admin_or_creator": "role:admin or user:%(creator)s or project_id:%(created_by_project_id)s",
"resource_owner": "project_id:%(project_id)s",
"metric_owner": "project_id:%(resource.project_id)s",

View File

@ -111,7 +111,8 @@ class ConfigFixture(fixture.GabbiFixture):
# Set pagination to a testable value
conf.set_override('max_limit', 7, 'api')
# Those tests do not use any auth
# Those tests uses noauth mode
# TODO(jd) Rewrite them for basic
conf.set_override("auth_mode", "noauth", 'api')
self.index = index

View File

@ -14,6 +14,7 @@
# 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 base64
import calendar
import contextlib
import datetime
@ -57,28 +58,40 @@ class TestingApp(webtest.TestApp):
INVALID_TOKEN = str(uuid.uuid4())
def __init__(self, *args, **kwargs):
self.auth = kwargs.pop('auth')
self.auth_mode = kwargs.pop('auth_mode')
self.storage = kwargs.pop('storage')
self.indexer = kwargs.pop('indexer')
super(TestingApp, self).__init__(*args, **kwargs)
# Setup Keystone auth_token fake cache
self.token = self.VALID_TOKEN
# Setup default user for basic auth
self.user = self.USER_ID.encode('ascii')
@contextlib.contextmanager
def use_admin_user(self):
if not self.auth:
raise testcase.TestSkipped("No auth enabled")
old_token = self.token
self.token = self.VALID_TOKEN_ADMIN
try:
yield
finally:
self.token = old_token
if self.auth_mode == "keystone":
old_token = self.token
self.token = self.VALID_TOKEN_ADMIN
try:
yield
finally:
self.token = old_token
elif self.auth_mode == "basic":
old_user = self.user
self.user = b"admin"
try:
yield
finally:
self.user = old_user
elif self.auth_mode == "noauth":
raise testcase.TestSkipped("auth mode is noauth")
else:
raise RuntimeError("Unknown auth_mode")
@contextlib.contextmanager
def use_another_user(self):
if not self.auth:
raise testcase.TestSkipped("No auth enabled")
if self.auth_mode != "keystone":
raise testcase.TestSkipped("Auth mode is not Keystone")
old_token = self.token
self.token = self.VALID_TOKEN_2
try:
@ -88,8 +101,8 @@ class TestingApp(webtest.TestApp):
@contextlib.contextmanager
def use_invalid_token(self):
if not self.auth:
raise testcase.TestSkipped("No auth enabled")
if self.auth_mode != "keystone":
raise testcase.TestSkipped("Auth mode is not Keystone")
old_token = self.token
self.token = self.INVALID_TOKEN
try:
@ -98,8 +111,16 @@ class TestingApp(webtest.TestApp):
self.token = old_token
def do_request(self, req, *args, **kwargs):
if self.auth and self.token is not None:
req.headers['X-Auth-Token'] = self.token
if self.auth_mode in "keystone":
if self.token is not None:
req.headers['X-Auth-Token'] = self.token
elif self.auth_mode == "basic":
req.headers['Authorization'] = (
b"basic " + base64.b64encode(self.user + b":")
)
elif self.auth_mode == "noauth":
req.headers['X-User-Id'] = self.USER_ID
req.headers['X-Project-Id'] = self.PROJECT_ID
response = super(TestingApp, self).do_request(req, *args, **kwargs)
metrics = self.storage.incoming.list_metric_with_measures_to_process(
None, None, full=True)
@ -110,41 +131,40 @@ class TestingApp(webtest.TestApp):
class RestTest(tests_base.TestCase, testscenarios.TestWithScenarios):
scenarios = [
('noauth', dict(auth=False)),
('keystone', dict(auth=True)),
('basic', dict(auth_mode="basic")),
('keystone', dict(auth_mode="keystone")),
('noauth', dict(auth_mode="noauth")),
]
def setUp(self):
super(RestTest, self).setUp()
self.auth_token_fixture = self.useFixture(
ksm_fixture.AuthTokenFixture())
self.auth_token_fixture.add_token_data(
is_v2=True,
token_id=TestingApp.VALID_TOKEN_ADMIN,
user_id=TestingApp.USER_ID_ADMIN,
user_name='adminusername',
project_id=TestingApp.PROJECT_ID_ADMIN,
role_list=['admin'])
self.auth_token_fixture.add_token_data(
is_v2=True,
token_id=TestingApp.VALID_TOKEN,
user_id=TestingApp.USER_ID,
user_name='myusername',
project_id=TestingApp.PROJECT_ID,
role_list=["member"])
self.auth_token_fixture.add_token_data(
is_v2=True,
token_id=TestingApp.VALID_TOKEN_2,
user_id=TestingApp.USER_ID_2,
user_name='myusername2',
project_id=TestingApp.PROJECT_ID_2,
role_list=["member"])
if self.auth_mode == "keystone":
self.auth_token_fixture = self.useFixture(
ksm_fixture.AuthTokenFixture())
self.auth_token_fixture.add_token_data(
is_v2=True,
token_id=TestingApp.VALID_TOKEN_ADMIN,
user_id=TestingApp.USER_ID_ADMIN,
user_name='adminusername',
project_id=TestingApp.PROJECT_ID_ADMIN,
role_list=['admin'])
self.auth_token_fixture.add_token_data(
is_v2=True,
token_id=TestingApp.VALID_TOKEN,
user_id=TestingApp.USER_ID,
user_name='myusername',
project_id=TestingApp.PROJECT_ID,
role_list=["member"])
self.auth_token_fixture.add_token_data(
is_v2=True,
token_id=TestingApp.VALID_TOKEN_2,
user_id=TestingApp.USER_ID_2,
user_name='myusername2',
project_id=TestingApp.PROJECT_ID_2,
role_list=["member"])
if self.auth:
self.conf.set_override("auth_mode", "keystone", group="api")
else:
self.conf.set_override("auth_mode", "noauth", group="api")
self.conf.set_override("auth_mode", self.auth_mode, group="api")
self.app = TestingApp(app.load_app(conf=self.conf,
indexer=self.index,
@ -152,7 +172,7 @@ class RestTest(tests_base.TestCase, testscenarios.TestWithScenarios):
not_implemented_middleware=False),
storage=self.storage,
indexer=self.index,
auth=self.auth)
auth_mode=self.auth_mode)
# NOTE(jd) Used at least by docs
@staticmethod
@ -619,16 +639,15 @@ class ResourceTest(RestTest):
self.resource = self.attributes.copy()
# Set original_resource_id
self.resource['original_resource_id'] = self.resource['id']
if self.auth:
self.resource['created_by_user_id'] = TestingApp.USER_ID
self.resource['created_by_user_id'] = TestingApp.USER_ID
if self.auth_mode in ("keystone", "noauth"):
self.resource['created_by_project_id'] = TestingApp.PROJECT_ID
self.resource['creator'] = (
TestingApp.USER_ID + ":" + TestingApp.PROJECT_ID
)
else:
self.resource['created_by_user_id'] = None
self.resource['created_by_project_id'] = None
self.resource['creator'] = None
elif self.auth_mode == "basic":
self.resource['created_by_project_id'] = ""
self.resource['creator'] = TestingApp.USER_ID
self.resource['ended_at'] = None
self.resource['metrics'] = {}
if 'user_id' not in self.resource:

View File

@ -0,0 +1,9 @@
---
upgrade:
- >-
The `auth_type` option has a new default value set to "basic". This mode
does not do any segregation and uses the standard HTTP `Authorization`
header for authentication. The old "noauth" authentication mechanism based
on the Keystone headers (`X-User-Id`, `X-Creator-Id` and `X-Roles`) and the
Keystone segregation rules, which was the default up to Gnocchi 3.0, is
still available.

View File

@ -91,6 +91,9 @@ pip install -q -U .[${GNOCCHI_VARIANT}]
eval $(pifpaf run gnocchi --indexer-url $INDEXER_URL --storage-url $STORAGE_URL)
# Gnocchi 3.1 uses basic auth by default
export OS_AUTH_TYPE=gnocchi-basic
export GNOCCHI_USER=$GNOCCHI_USER_ID
dump_data $GNOCCHI_DATA/new
echo "* Checking output difference between Gnocchi $old_version and $new_version"

View File

@ -125,6 +125,7 @@ gnocchi.aggregates =
gnocchi.rest.auth_helper =
noauth = gnocchi.rest.auth_helper:NoAuthHelper
keystone = gnocchi.rest.auth_helper:KeystoneAuthHelper
basic = gnocchi.rest.auth_helper:BasicAuthHelper
console_scripts =
gnocchi-upgrade = gnocchi.cli:upgrade

View File

@ -42,7 +42,7 @@ usedevelop = False
setenv = GNOCCHI_VARIANT=test,postgresql,file
deps = gnocchi[{env:GNOCCHI_VARIANT}]>=3.0,<3.1
pifpaf>=0.13
gnocchiclient
gnocchiclient>=2.8.0
commands = pifpaf --env-prefix INDEXER run postgresql {toxinidir}/run-upgrade-tests.sh {posargs}
[testenv:py27-mysql-ceph-upgrade-from-3.0]
@ -54,7 +54,7 @@ skip_install = True
usedevelop = False
setenv = GNOCCHI_VARIANT=test,mysql,ceph,ceph_recommended_lib
deps = gnocchi[{env:GNOCCHI_VARIANT}]>=3.0,<3.1
gnocchiclient
gnocchiclient>=2.8.0
pifpaf>=0.13
commands = pifpaf --env-prefix INDEXER run mysql -- pifpaf --env-prefix STORAGE run ceph {toxinidir}/run-upgrade-tests.sh {posargs}
@ -68,7 +68,7 @@ usedevelop = False
setenv = GNOCCHI_VARIANT=test,postgresql,file
deps = gnocchi[{env:GNOCCHI_VARIANT}]>=2.2,<2.3
pifpaf>=0.13
gnocchiclient
gnocchiclient>=2.8.0
commands = pifpaf --env-prefix INDEXER run postgresql {toxinidir}/run-upgrade-tests.sh {posargs}
[testenv:py27-mysql-ceph-upgrade-from-2.2]
@ -80,7 +80,7 @@ skip_install = True
usedevelop = False
setenv = GNOCCHI_VARIANT=test,mysql,ceph,ceph_recommended_lib
deps = gnocchi[{env:GNOCCHI_VARIANT}]>=2.2,<2.3
gnocchiclient
gnocchiclient>=2.8.0
pifpaf>=0.13
cradox
# cradox is required because 2.2 extra names are incorrect