Skip to content

Commit

Permalink
llow direct communication to Openshift API through get_federated_user
Browse files Browse the repository at this point in the history
As the first step towards merging the account manager into our coldfront
cloud plugin, the `get_federated_user` function in the Openshift allocator
will now (through several functions) directly call the Openshift API.
Much of the functions added are copied from the `moc_openshift`
module in the account manager.

Aside from copying some functions,
implementation of this feature also involved:
- A new resource attribute `Identity Name` for the Openshift idp
- A new unit test for the `get_federated_user` function
- Changes to the CI file to enable these new unit tests
  • Loading branch information
QuanMPhm committed Oct 30, 2024
1 parent ce7c8e1 commit 73b751b
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 6 deletions.
1 change: 1 addition & 0 deletions ci/run_functional_tests_openshift.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ set -xe

export OPENSHIFT_MICROSHIFT_USERNAME="admin"
export OPENSHIFT_MICROSHIFT_PASSWORD="pass"
export OPENSHIFT_MICROSHIFT_TOKEN="$(oc create token -n onboarding onboarding-serviceaccount)"

if [[ ! "${CI}" == "true" ]]; then
source /tmp/coldfront_venv/bin/activate
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
git+https://github.com/CCI-MOC/nerc-rates@74eb4a7#egg=nerc_rates
boto3
kubernetes
coldfront >= 1.0.4
python-cinderclient # TODO: Set version for OpenStack Clients
python-keystoneclient
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ python_requires = >=3.8
install_requires =
nerc_rates @ git+https://github.com/CCI-MOC/nerc-rates@74eb4a7
boto3
kubernetes
coldfront >= 1.0.4
python-cinderclient
python-keystoneclient
Expand Down
2 changes: 2 additions & 0 deletions src/coldfront_plugin_cloud/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class CloudAllocationAttribute:


RESOURCE_AUTH_URL = 'Identity Endpoint URL'
RESOURCE_IDENTITY_NAME = 'Identity Name'
RESOURCE_ROLE = 'Role for User in Project'

RESOURCE_FEDERATION_PROTOCOL = 'OpenStack Federation Protocol'
Expand All @@ -32,6 +33,7 @@ class CloudAllocationAttribute:

RESOURCE_ATTRIBUTES = [
CloudResourceAttribute(name=RESOURCE_AUTH_URL),
CloudResourceAttribute(name=RESOURCE_IDENTITY_NAME),
CloudResourceAttribute(name=RESOURCE_FEDERATION_PROTOCOL),
CloudResourceAttribute(name=RESOURCE_IDP),
CloudResourceAttribute(name=RESOURCE_PROJECT_DOMAIN),
Expand Down
117 changes: 111 additions & 6 deletions src/coldfront_plugin_cloud/openshift.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,29 @@
import time
from simplejson.errors import JSONDecodeError

import kubernetes
import kubernetes.dynamic.exceptions as kexc

from coldfront_plugin_cloud import attributes, base, utils

API_PROJECT = "project.openshift.io/v1"
API_USER = "user.openshift.io/v1"
API_RBAC = "rbac.authorization.k8s.io/v1"
API_CORE = "v1"
IGNORED_ATTRIBUTES = [
"resourceVersion",
"creationTimestamp",
"uid",
]

def clean_openshift_metadata(obj):
if "metadata" in obj:
for attr in IGNORED_ATTRIBUTES:
if attr in obj["metadata"]:
del obj["metadata"][attr]

return obj

QUOTA_KEY_MAPPING = {
attributes.QUOTA_LIMITS_CPU: lambda x: {":limits.cpu": f"{x * 1000}m"},
attributes.QUOTA_LIMITS_MEMORY: lambda x: {":limits.memory": f"{x}Mi"},
Expand Down Expand Up @@ -38,6 +59,41 @@ class OpenShiftResourceAllocator(base.ResourceAllocator):

project_name_max_length = 63

logger = logging.getLogger()

def __init__(self, resource, allocation):
super().__init__(resource, allocation)
self.safe_resource_name = utils.env_safe_name(resource.name)
self.id_provider = resource.get_attribute(attributes.RESOURCE_IDENTITY_NAME)
self.apis = {}

self.functional_tests = os.environ.get("FUNCTIONAL_TESTS", "").lower()
self.verify = os.getenv(f"OPENSHIFT_{self.safe_resource_name}_VERIFY", "").lower()

if self.functional_tests == "true" or self.verify == "false":
self.logger = logging.getLogger()
else:
self.logger = logging.getLogger("django")

@functools.cached_property
def k8_client(self):
# Load Endpoint URL and Auth token for new k8 client
openshift_token = os.getenv(f"OPENSHIFT_{self.safe_resource_name}_TOKEN")
openshift_url = self.resource.get_attribute(attributes.RESOURCE_AUTH_URL)

k8_config = kubernetes.client.Configuration()
k8_config.api_key["authorization"] = openshift_token
k8_config.api_key_prefix["authorization"] = "Bearer"
k8_config.host = openshift_url

if self.functional_tests == "true" or self.verify == "false":
k8_config.verify_ssl = False
else:
k8_config.verify_ssl = True

k8s_client = kubernetes.client.ApiClient(configuration=k8_config)
return kubernetes.dynamic.DynamicClient(k8s_client)

@functools.cached_property
def session(self):
var_name = utils.env_safe_name(self.resource.name)
Expand Down Expand Up @@ -71,6 +127,18 @@ def check_response(response: requests.Response):
raise Conflict(f"{response.status_code}: {response.text}")
else:
raise ApiException(f"{response.status_code}: {response.text}")

def qualified_id_user(self, id_user):
return f"{self.id_provider}:{id_user}"

def get_resource_api(self, api_version: str, kind: str):
"""Either return the cached resource api from self.apis, or fetch a
new one, store it in self.apis, and return it."""
k = f"{api_version}:{kind}"
api = self.apis.setdefault(
k, self.k8_client.resources.get(api_version=api_version, kind=kind)
)
return api

def create_project(self, suggested_project_name):
sanitized_project_name = utils.get_sanitized_project_name(suggested_project_name)
Expand Down Expand Up @@ -113,13 +181,14 @@ def reactivate_project(self, project_id):
pass

def get_federated_user(self, username):
url = f"{self.auth_url}/users/{username}"
try:
r = self.session.get(url)
self.check_response(r)
if (
self._openshift_user_exists(username)
and self._openshift_get_identity(username)
and self._openshift_useridentitymapping_exists(username, username)
):
return {'username': username}
except NotFound:
pass

self.logger.info("404: " + f"user ({username}) does not exist")

def create_federated_user(self, unique_id):
url = f"{self.auth_url}/users/{unique_id}"
Expand Down Expand Up @@ -183,3 +252,39 @@ def get_users(self, project_id):
url = f"{self.auth_url}/projects/{project_id}/users"
r = self.session.get(url)
return set(self.check_response(r))

def _openshift_get_user(self, username):
api = self.get_resource_api(API_USER, "User")
return clean_openshift_metadata(api.get(name=username).to_dict())

def _openshift_get_identity(self, id_user):
api = self.get_resource_api(API_USER, "Identity")
return clean_openshift_metadata(
api.get(name=self.qualified_id_user(id_user)).to_dict()
)

def _openshift_user_exists(self, user_name):
try:
self._openshift_get_user(user_name)
except kexc.NotFoundError:
return False
return True

def _openshift_identity_exists(self, id_user):
try:
self._openshift_get_identity(id_user)
except kexc.NotFoundError:
return False
return True

def _openshift_useridentitymapping_exists(self, user_name, id_user):
try:
user = self._openshift_get_user(user_name)
except kexc.NotFoundError:
return False

return any(
identity == self.qualified_id_user(id_user)
for identity in user.get("identities", [])
)

31 changes: 31 additions & 0 deletions src/coldfront_plugin_cloud/tests/unit/openshift/test_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from unittest import mock

from coldfront_plugin_cloud.tests import base
from coldfront_plugin_cloud.openshift import OpenShiftResourceAllocator


class TestOpenshiftUser(base.TestBase):
def setUp(self) -> None:
mock_resource = mock.Mock()
mock_allocation = mock.Mock()
self.mock_openshift_allocator = OpenShiftResourceAllocator(mock_resource, mock_allocation)
self.mock_openshift_allocator.id_provider = "fake_idp"
self.mock_openshift_allocator.logger = mock.Mock()
self.mock_openshift_allocator.k8_client = mock.Mock()

def test_get_federated_user(self):
fake_user = mock.Mock(spec=["to_dict"])
fake_user.to_dict.return_value = {"identities": ["fake_idp:fake_user"]}
self.mock_openshift_allocator.k8_client.resources.get.return_value.get.return_value = fake_user

output = self.mock_openshift_allocator.get_federated_user("fake_user")
self.assertEqual(output, {"username": "fake_user"})

def test_get_federated_user(self):
fake_user = mock.Mock(spec=["to_dict"])
fake_user.to_dict.return_value = {"identities": ["fake_idp:fake_user"]}
self.mock_openshift_allocator.k8_client.resources.get.return_value.get.return_value = fake_user

output = self.mock_openshift_allocator.get_federated_user("fake_user_2")
self.assertEqual(output, None)
self.mock_openshift_allocator.logger.info.assert_called_with("404: user (fake_user_2) does not exist")

0 comments on commit 73b751b

Please sign in to comment.