This commit introduces OpenID Connect (OIDC) authentication
support to the DCManager API, allowing requests to be authenticated
using either Keystone tokens or OIDC tokens.
Behavior overview:
- The REST API will authenticate using Keystone when the
`X-Auth-Token` header is provided (existing behavior).
- When the `OIDC-Token` header is provided, OIDC authentication
is performed instead.
- If both tokens are present, default to Keystone authentication.
For OIDC authentication:
- The REST API retrieves OIDC IDP configuration parameters from
`system service-parameters` under `kube-apiserver`:
- `oidc-issuer-url`
- `oidc-client-id`
- `oidc-username-claim`
- `oidc-groups-claim`
- If OIDC parameters are not configured, authentication fails
with an unauthenticated response.
- If configured, the REST API validates the OIDC token with the
IDP issuer and extracts claims.
- OIDC arguments and claims are cached.
- External users and groups are mapped to internal
Project+Role tuples based on StarlingX rolebindings.
Test Plan:
PASS: Authenticate REST API requests with Keystone (`X-Auth-Token`).
PASS: Authenticate REST API requests with OIDC (`OIDC-Token`).
PASS: Verify Keystone is used when both tokens are present.
PASS: Verify unauthenticated response when OIDC parameters are
missing.
PASS: Validate token claims and role mappings are applied correctly.
PASS: Confirm cached tokens continue to authorize during temporary
IDP connectivity loss.
PASS: Force to OIDC token validation to return claims as None and verify
the api returns a NotAuthorized exception.
Depends-On: https://review.opendev.org/c/starlingx/integ/+/970455
Story: 2011646
Task: 53594
Change-Id: I830084fcad9b6413477e703514325030c7dc58a2
Signed-off-by: Hugo Brito <hugo.brito@windriver.com>
236 lines
7.1 KiB
Python
236 lines
7.1 KiB
Python
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
|
|
# Copyright (c) 2017-2026 Wind River Systems, Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 http.client
|
|
|
|
from oslo_config import cfg
|
|
from oslo_config import fixture as fixture_config
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import uuidutils
|
|
import pecan
|
|
from pecan.configuration import set_config
|
|
from pecan.testing import load_test_app
|
|
|
|
from dcmanager.api import api_config
|
|
from dcmanager.common import config
|
|
from dcmanager.tests.base import DCManagerTestCase
|
|
from dcmanager.tests.unit.common import consts as test_consts
|
|
from dcmanager.tests import utils
|
|
|
|
config.register_options()
|
|
OPT_GROUP_NAME = "keystone_authtoken"
|
|
cfg.CONF.import_group(OPT_GROUP_NAME, "keystonemiddleware.auth_token")
|
|
|
|
|
|
class DCManagerApiTest(DCManagerTestCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.addCleanup(set_config, {}, overwrite=True)
|
|
|
|
api_config.test_init()
|
|
|
|
config_fixture = fixture_config.Config()
|
|
self.CONF = self.useFixture(config_fixture).conf
|
|
config_fixture.set_config_dirs([])
|
|
|
|
self.CONF.set_override("auth_strategy", "noauth")
|
|
self.CONF.set_override(
|
|
"region_name",
|
|
uuidutils.generate_uuid().replace("-", ""),
|
|
group="keystone_authtoken",
|
|
)
|
|
|
|
self.app = self._make_app()
|
|
|
|
self.url = "/"
|
|
# The put method is used as a default value, leading to the generic
|
|
# implementation on controllers in case the method is not specified
|
|
self.method = self.app.put
|
|
self.params = {}
|
|
self.upload_files = None
|
|
self.verb = None
|
|
self.headers = {
|
|
"X-Auth-Token": "fake_keystone_token",
|
|
"X-Tenant-Id": utils.UUID1,
|
|
"X_ROLE": "admin,member,reader",
|
|
"X-Identity-Status": "Confirmed",
|
|
"X-Project-Name": "admin",
|
|
}
|
|
|
|
def _make_app(self, enable_acl=False):
|
|
self.config_fixture = {
|
|
"app": {
|
|
"root": "dcmanager.api.controllers.root.RootController",
|
|
"modules": ["dcmanager.api"],
|
|
"enable_acl": enable_acl,
|
|
"errors": {400: "/error", "__force_dict__": True},
|
|
},
|
|
}
|
|
|
|
return load_test_app(self.config_fixture)
|
|
|
|
def _send_request(self):
|
|
"""Send a request to a url"""
|
|
|
|
kwargs = {}
|
|
|
|
if self.upload_files:
|
|
kwargs = {"upload_files": self.upload_files}
|
|
|
|
return self.method(
|
|
self.url,
|
|
headers=self.headers,
|
|
params=self.params,
|
|
expect_errors=True,
|
|
**kwargs,
|
|
)
|
|
|
|
def _assert_response(
|
|
self,
|
|
response,
|
|
status_code=http.client.OK,
|
|
content_type=test_consts.APPLICATION_JSON,
|
|
):
|
|
"""Assert the response for a request"""
|
|
|
|
self.assertEqual(response.status_code, status_code)
|
|
self.assertEqual(response.content_type, content_type)
|
|
|
|
def _assert_pecan_and_response(
|
|
self,
|
|
response,
|
|
http_status,
|
|
content=None,
|
|
call_count=1,
|
|
content_type=test_consts.TEXT_PLAIN,
|
|
):
|
|
"""Assert the response and pecan abort for a failed request"""
|
|
self._assert_pecan(http_status, content, call_count=call_count)
|
|
self._assert_response(response, http_status, content_type)
|
|
|
|
def tearDown(self):
|
|
super(DCManagerApiTest, self).tearDown()
|
|
pecan.set_config({}, overwrite=True)
|
|
|
|
|
|
class TestRootController(DCManagerApiTest):
|
|
"""Test version listing on root URI."""
|
|
|
|
def setUp(self):
|
|
super(TestRootController, self).setUp()
|
|
|
|
self.url = "/"
|
|
self.method = self.app.get
|
|
|
|
def _test_method_returns_405(self, method, content_type=test_consts.TEXT_PLAIN):
|
|
self.method = method
|
|
|
|
response = self._send_request()
|
|
|
|
self._assert_pecan_and_response(
|
|
response, http.client.METHOD_NOT_ALLOWED, content_type=content_type
|
|
)
|
|
|
|
def test_get(self):
|
|
"""Test get request succeeds with correct versions"""
|
|
|
|
response = self._send_request()
|
|
|
|
self._assert_response(response)
|
|
json_body = jsonutils.loads(response.body)
|
|
versions = json_body.get("versions")
|
|
self.assertEqual(1, len(versions))
|
|
|
|
def test_request_id(self):
|
|
"""Test request for root returns the correct request id"""
|
|
|
|
response = self._send_request()
|
|
|
|
self._assert_response(response)
|
|
self.assertIn("x-openstack-request-id", response.headers)
|
|
self.assertTrue(response.headers["x-openstack-request-id"].startswith("req-"))
|
|
id_part = response.headers["x-openstack-request-id"].split("req-")[1]
|
|
self.assertTrue(uuidutils.is_uuid_like(id_part))
|
|
|
|
def test_post(self):
|
|
"""Test post request is not allowed on root"""
|
|
|
|
self._test_method_returns_405(self.app.post)
|
|
|
|
def test_put(self):
|
|
"""Test put request is not allowed on root"""
|
|
|
|
self._test_method_returns_405(self.app.put)
|
|
|
|
def test_patch(self):
|
|
"""Test patch request is not allowed on root"""
|
|
|
|
self._test_method_returns_405(self.app.patch)
|
|
|
|
def test_delete(self):
|
|
"""Test delete request is not allowed on root"""
|
|
|
|
self._test_method_returns_405(self.app.delete)
|
|
|
|
def test_head(self):
|
|
"""Test head request is not allowed on root"""
|
|
|
|
self._test_method_returns_405(self.app.head, content_type=test_consts.TEXT_HTML)
|
|
|
|
|
|
class TestErrors(DCManagerApiTest):
|
|
|
|
def setUp(self):
|
|
super(TestErrors, self).setUp()
|
|
cfg.CONF.set_override("admin_tenant", "fake_tenant_id", group="cache")
|
|
|
|
def test_404(self):
|
|
self.url = "/assert_called_once"
|
|
self.method = self.app.get
|
|
|
|
response = self._send_request()
|
|
self._assert_response(
|
|
response, http.client.NOT_FOUND, content_type=test_consts.TEXT_PLAIN
|
|
)
|
|
|
|
def test_version_1_root_controller(self):
|
|
self.url = f"/v1.0/{uuidutils.generate_uuid()}/bad_method"
|
|
self.method = self.app.patch
|
|
|
|
response = self._send_request()
|
|
|
|
self._assert_pecan_and_response(response, http.client.NOT_FOUND)
|
|
|
|
|
|
class TestKeystoneAuth(DCManagerApiTest):
|
|
"""Test requests using keystone as the authentication strategy"""
|
|
|
|
def setUp(self):
|
|
super(TestKeystoneAuth, self).setUp()
|
|
|
|
cfg.CONF.set_override("auth_strategy", "keystone")
|
|
|
|
self.method = self.app.get
|
|
|
|
def test_auth_not_enforced_for_root(self):
|
|
"""Test authentication is not enforced for root url"""
|
|
|
|
response = self._send_request()
|
|
self._assert_response(response)
|