Introduce "basic" authentication mechanism
Change-Id: I369de25a656fa56e960cfba2b768ea10eedd3957
This commit is contained in:
parent
63e1cd95e7
commit
9da4e07b94
@ -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
|
||||
|
@ -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
|
||||
=======
|
||||
|
@ -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
|
||||
|
@ -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.'),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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.
|
@ -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"
|
||||
|
@ -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
|
||||
|
8
tox.ini
8
tox.ini
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user