Files
distcloud/distributedcloud/dcmanager/tests/unit/api/test_root_controller.py
Hugo Brito 2b198a8009 Add OIDC authentication support to DCManager API
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>
2026-01-12 17:55:41 -03:00

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)