From 9da4e07b94023ea92c47b3d0a801730404f95ae1 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Thu, 15 Dec 2016 11:40:44 +0100 Subject: [PATCH] Introduce "basic" authentication mechanism Change-Id: I369de25a656fa56e960cfba2b768ea10eedd3957 --- doc/source/configuration.rst | 9 +- doc/source/rest.j2 | 21 ++- gnocchi/gendoc.py | 2 +- gnocchi/opts.py | 2 +- gnocchi/rest/api-paste.ini | 6 + gnocchi/rest/auth_helper.py | 25 ++++ gnocchi/rest/policy.json | 2 +- gnocchi/tests/gabbi/fixtures.py | 3 +- gnocchi/tests/test_rest.py | 121 ++++++++++-------- ...auth-keystone-compat-e8f760591d593f07.yaml | 9 ++ run-upgrade-tests.sh | 3 + setup.cfg | 1 + tox.ini | 8 +- 13 files changed, 138 insertions(+), 74 deletions(-) create mode 100644 releasenotes/notes/noauth-keystone-compat-e8f760591d593f07.yaml diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 63e3506bf..2d9408f1a 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -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 diff --git a/doc/source/rest.j2 b/doc/source/rest.j2 index b67d375c5..1c83ed724 100644 --- a/doc/source/rest.j2 +++ b/doc/source/rest.j2 @@ -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 ======= diff --git a/gnocchi/gendoc.py b/gnocchi/gendoc.py index 240b83190..996c715b0 100644 --- a/gnocchi/gendoc.py +++ b/gnocchi/gendoc.py @@ -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 diff --git a/gnocchi/opts.py b/gnocchi/opts.py index 02cdb3f4e..6e7dca4a8 100644 --- a/gnocchi/opts.py +++ b/gnocchi/opts.py @@ -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.'), diff --git a/gnocchi/rest/api-paste.ini b/gnocchi/rest/api-paste.ini index d198362d4..47bb3c32d 100644 --- a/gnocchi/rest/api-paste.ini +++ b/gnocchi/rest/api-paste.ini @@ -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 diff --git a/gnocchi/rest/auth_helper.py b/gnocchi/rest/auth_helper.py index bb4a2d995..c173c8de5 100644 --- a/gnocchi/rest/auth_helper.py +++ b/gnocchi/rest/auth_helper.py @@ -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 diff --git a/gnocchi/rest/policy.json b/gnocchi/rest/policy.json index 00aaedddd..51d396747 100644 --- a/gnocchi/rest/policy.json +++ b/gnocchi/rest/policy.json @@ -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", diff --git a/gnocchi/tests/gabbi/fixtures.py b/gnocchi/tests/gabbi/fixtures.py index 39a94dc60..df98ed341 100644 --- a/gnocchi/tests/gabbi/fixtures.py +++ b/gnocchi/tests/gabbi/fixtures.py @@ -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 diff --git a/gnocchi/tests/test_rest.py b/gnocchi/tests/test_rest.py index a5374ec3a..1c73bb817 100644 --- a/gnocchi/tests/test_rest.py +++ b/gnocchi/tests/test_rest.py @@ -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: diff --git a/releasenotes/notes/noauth-keystone-compat-e8f760591d593f07.yaml b/releasenotes/notes/noauth-keystone-compat-e8f760591d593f07.yaml new file mode 100644 index 000000000..0aaffc38e --- /dev/null +++ b/releasenotes/notes/noauth-keystone-compat-e8f760591d593f07.yaml @@ -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. diff --git a/run-upgrade-tests.sh b/run-upgrade-tests.sh index 2b69558ec..33fe24aea 100755 --- a/run-upgrade-tests.sh +++ b/run-upgrade-tests.sh @@ -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" diff --git a/setup.cfg b/setup.cfg index 3812d3412..10297471a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/tox.ini b/tox.ini index b0e37db17..d83825775 100644 --- a/tox.ini +++ b/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