Skip to content

Commit 8bdb720

Browse files
committed
Allow direct communication to Openshift RBAC API
1 parent cee3d22 commit 8bdb720

File tree

3 files changed

+260
-26
lines changed

3 files changed

+260
-26
lines changed

src/coldfront_plugin_cloud/management/commands/add_openshift_resource.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@
77
ResourceType,
88
)
99

10-
from coldfront_plugin_cloud import attributes
10+
from coldfront_plugin_cloud import attributes, openshift
1111

1212

1313
class Command(BaseCommand):
1414
help = "Create OpenShift resource"
1515

16+
@staticmethod
17+
def validate_role(role):
18+
if role not in openshift.OPENSHIFT_ROLES:
19+
raise ValueError(
20+
f"Invalid role, {role} is not one of {', '.join(openshift.OPENSHIFT_ROLES)}"
21+
)
22+
1623
def add_arguments(self, parser):
1724
parser.add_argument(
1825
"--name", type=str, required=True, help="Name of OpenShift resource"
@@ -45,6 +52,8 @@ def add_arguments(self, parser):
4552
)
4653

4754
def handle(self, *args, **options):
55+
self.validate_role(options["role"])
56+
4857
if options["for_virtualization"]:
4958
resource_description = "OpenShift Virtualization environment"
5059
resource_type = "OpenShift Virtualization"

src/coldfront_plugin_cloud/openshift.py

Lines changed: 116 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
}
3636

3737

38+
OPENSHIFT_ROLES = ["admin", "edit", "view"]
39+
40+
3841
def clean_openshift_metadata(obj):
3942
if "metadata" in obj:
4043
for attr in IGNORED_ATTRIBUTES:
@@ -253,25 +256,51 @@ def create_federated_user(self, unique_id):
253256
logger.info(f"User {unique_id} successfully created")
254257

255258
def assign_role_on_user(self, username, project_id):
256-
# /users/<user_name>/projects/<project>/roles/<role>
257-
url = (
258-
f"{self.auth_url}/users/{username}/projects/{project_id}"
259-
f"/roles/{self.member_role_name}"
260-
)
259+
"""Assign a role to a user in a project using direct OpenShift API calls"""
261260
try:
262-
r = self.session.put(url)
263-
self.check_response(r)
264-
except Conflict:
261+
# Try to get existing rolebindings with same name
262+
# as the role name in project's namespace
263+
rolebinding = self._openshift_get_rolebindings(
264+
project_id, self.member_role_name
265+
)
266+
267+
if not self._user_in_rolebinding(username, rolebinding):
268+
# Add user to existing rolebinding
269+
if "subjects" not in rolebinding:
270+
rolebinding["subjects"] = []
271+
rolebinding["subjects"].append({"kind": "User", "name": username})
272+
self._openshift_update_rolebindings(project_id, rolebinding)
273+
274+
except kexc.NotFoundError:
275+
# Create new rolebinding if it doesn't exist
276+
self._openshift_create_rolebindings(
277+
project_id, username, self.member_role_name
278+
)
279+
except kexc.ConflictError:
280+
# Role already exists, ignore
265281
pass
266282

267283
def remove_role_from_user(self, username, project_id):
268-
# /users/<user_name>/projects/<project>/roles/<role>
269-
url = (
270-
f"{self.auth_url}/users/{username}/projects/{project_id}"
271-
f"/roles/{self.member_role_name}"
272-
)
273-
r = self.session.delete(url)
274-
self.check_response(r)
284+
"""Remove a role from a user in a project using direct OpenShift API calls"""
285+
try:
286+
rolebinding = self._openshift_get_rolebindings(
287+
project_id, self.member_role_name
288+
)
289+
290+
if "subjects" in rolebinding:
291+
rolebinding["subjects"] = [
292+
subject
293+
for subject in rolebinding["subjects"]
294+
if not (
295+
subject.get("kind") == "User"
296+
and subject.get("name") == username
297+
)
298+
]
299+
self._openshift_update_rolebindings(project_id, rolebinding)
300+
301+
except kexc.NotFoundError:
302+
# Rolebinding doesn't exist, nothing to remove
303+
pass
275304

276305
def _create_project(self, project_name, project_id):
277306
url = f"{self.auth_url}/projects/{project_id}"
@@ -290,13 +319,13 @@ def _create_project(self, project_name, project_id):
290319
self.check_response(r)
291320

292321
def _get_role(self, username, project_id):
293-
# /users/<user_name>/projects/<project>/roles/<role>
294-
url = (
295-
f"{self.auth_url}/users/{username}/projects/{project_id}"
296-
f"/roles/{self.member_role_name}"
322+
rolebindings = self._openshift_get_rolebindings(
323+
project_id, self.member_role_name
297324
)
298-
r = self.session.get(url)
299-
return self.check_response(r)
325+
if not self._user_in_rolebinding(username, rolebindings):
326+
raise NotFound(
327+
f"User {username} has no rolebindings in project {project_id}"
328+
)
300329

301330
def _get_project(self, project_id):
302331
url = f"{self.auth_url}/projects/{project_id}"
@@ -308,10 +337,24 @@ def _delete_user(self, username):
308337
self._openshift_delete_identity(username)
309338
logger.info(f"User {username} successfully deleted")
310339

311-
def get_users(self, project_id): # TODO (Quan) This should also replaced
312-
url = f"{self.auth_url}/projects/{project_id}/users"
313-
r = self.session.get(url)
314-
return set(self.check_response(r))
340+
def get_users(self, project_id):
341+
"""Get all users with roles in a project"""
342+
users = set()
343+
344+
# Check all standard OpenShift roles
345+
for role in OPENSHIFT_ROLES:
346+
try:
347+
rolebinding = self._openshift_get_rolebindings(project_id, role)
348+
if "subjects" in rolebinding:
349+
users.update(
350+
subject["name"]
351+
for subject in rolebinding["subjects"]
352+
if subject.get("kind") == "User"
353+
)
354+
except kexc.NotFoundError:
355+
continue
356+
357+
return users
315358

316359
def _openshift_get_user(self, username):
317360
api = self.get_resource_api(API_USER, "User")
@@ -446,3 +489,51 @@ def _openshift_delete_resourcequota(self, project_id, resourcequota_name):
446489
"""In an openshift namespace {project_id) delete a specified resourcequota"""
447490
api = self.get_resource_api(API_CORE, "ResourceQuota")
448491
return api.delete(namespace=project_id, name=resourcequota_name).to_dict()
492+
493+
def _user_in_rolebinding(self, username, rolebinding):
494+
"""Check if a user is in a rolebinding"""
495+
if "subjects" not in rolebinding:
496+
return False
497+
498+
return any(
499+
subject.get("kind") == "User" and subject.get("name") == username
500+
for subject in rolebinding["subjects"]
501+
)
502+
503+
def _openshift_get_rolebindings(self, project_name, role):
504+
api = self.get_resource_api(API_RBAC, "RoleBinding")
505+
result = clean_openshift_metadata(
506+
api.get(namespace=project_name, name=role).to_dict()
507+
)
508+
509+
# Ensure subjects is a list
510+
if not result.get("subjects"):
511+
result["subjects"] = []
512+
513+
return result
514+
515+
def _openshift_create_rolebindings(self, project_name, username, role):
516+
api = self.get_resource_api(API_RBAC, "RoleBinding")
517+
payload = {
518+
"metadata": {"name": role, "namespace": project_name},
519+
"subjects": [{"name": username, "kind": "User"}],
520+
"roleRef": {"name": role, "kind": "ClusterRole"},
521+
}
522+
return clean_openshift_metadata(
523+
api.create(body=payload, namespace=project_name).to_dict()
524+
)
525+
526+
def _openshift_update_rolebindings(self, project_name, rolebinding):
527+
api = self.get_resource_api(API_RBAC, "RoleBinding")
528+
return clean_openshift_metadata(
529+
api.patch(body=rolebinding, namespace=project_name).to_dict()
530+
)
531+
532+
def _openshift_list_rolebindings(self, project_name):
533+
"""List all rolebindings in a project"""
534+
api = self.get_resource_api(API_RBAC, "RoleBinding")
535+
try:
536+
result = clean_openshift_metadata(api.get(namespace=project_name).to_dict())
537+
return result.get("items", [])
538+
except kexc.NotFoundError:
539+
return []
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from unittest import mock
2+
3+
import kubernetes.dynamic.exceptions as kexc
4+
5+
from coldfront_plugin_cloud.tests import base
6+
from coldfront_plugin_cloud.openshift import OpenShiftResourceAllocator
7+
8+
9+
class TestMocOpenShiftRBAC(base.TestBase):
10+
def setUp(self) -> None:
11+
mock_resource = mock.Mock()
12+
mock_allocation = mock.Mock()
13+
self.allocator = OpenShiftResourceAllocator(mock_resource, mock_allocation)
14+
self.allocator.id_provider = "fake_idp"
15+
self.allocator.k8_client = mock.Mock()
16+
self.allocator.member_role_name = "admin"
17+
18+
def test_user_in_rolebindings_false(self):
19+
fake_rb = {
20+
"subjects": [
21+
{
22+
"kind": "User",
23+
"name": "fake-user-2",
24+
}
25+
]
26+
}
27+
output = self.allocator._user_in_rolebinding("fake-user", fake_rb)
28+
self.assertFalse(output)
29+
30+
def test_user_in_rolebindings_true(self):
31+
fake_rb = {
32+
"subjects": [
33+
{
34+
"kind": "User",
35+
"name": "fake-user",
36+
}
37+
]
38+
}
39+
output = self.allocator._user_in_rolebinding("fake-user", fake_rb)
40+
self.assertTrue(output)
41+
42+
@mock.patch(
43+
"coldfront_plugin_cloud.openshift.OpenShiftResourceAllocator._openshift_get_rolebindings"
44+
)
45+
@mock.patch(
46+
"coldfront_plugin_cloud.openshift.OpenShiftResourceAllocator._openshift_update_rolebindings"
47+
)
48+
def test_add_user_to_role(self, fake_update_rb, fake_get_rb):
49+
fake_get_rb.return_value = {
50+
"subjects": [],
51+
}
52+
self.allocator.assign_role_on_user("fake-user", "fake-project")
53+
fake_update_rb.assert_called_with(
54+
"fake-project", {"subjects": [{"kind": "User", "name": "fake-user"}]}
55+
)
56+
57+
@mock.patch(
58+
"coldfront_plugin_cloud.openshift.OpenShiftResourceAllocator._openshift_get_rolebindings"
59+
)
60+
@mock.patch(
61+
"coldfront_plugin_cloud.openshift.OpenShiftResourceAllocator._openshift_create_rolebindings"
62+
)
63+
def test_add_user_to_role_not_exists(self, fake_create_rb, fake_get_rb):
64+
fake_error = kexc.NotFoundError(mock.Mock())
65+
fake_get_rb.side_effect = fake_error
66+
self.allocator.assign_role_on_user("fake-user", "fake-project")
67+
fake_create_rb.assert_called_with("fake-project", "fake-user", "admin")
68+
69+
@mock.patch(
70+
"coldfront_plugin_cloud.openshift.OpenShiftResourceAllocator._openshift_get_rolebindings"
71+
)
72+
def test_remove_user_from_role(self, fake_get_rb):
73+
fake_get_rb.return_value = {
74+
"subjects": [{"kind": "User", "name": "fake-user"}],
75+
}
76+
self.allocator.k8_client.resources.get.return_value.patch.return_value.to_dict.return_value = {}
77+
self.allocator.remove_role_from_user("fake-user", "fake-project")
78+
self.allocator.k8_client.resources.get.return_value.patch.assert_called_with(
79+
body={"subjects": []}, namespace="fake-project"
80+
)
81+
82+
def test_remove_user_from_role_not_exists(self):
83+
fake_error = kexc.NotFoundError(mock.Mock())
84+
self.allocator.k8_client.resources.get.return_value.get.side_effect = fake_error
85+
self.allocator.remove_role_from_user("fake-project", "fake-user")
86+
self.allocator.k8_client.resources.get.return_value.patch.assert_not_called()
87+
88+
def test_get_rolebindings(self):
89+
fake_rb = mock.Mock(spec=["to_dict"])
90+
fake_rb.to_dict.return_value = {"subjects": []}
91+
self.allocator.k8_client.resources.get.return_value.get.return_value = fake_rb
92+
res = self.allocator._openshift_get_rolebindings("fake-project", "admin")
93+
self.assertEqual(res, fake_rb.to_dict())
94+
95+
def test_get_rolebindings_no_subjects(self):
96+
fake_rb = mock.Mock(spec=["to_dict"])
97+
fake_rb.to_dict.return_value = {}
98+
self.allocator.k8_client.resources.get.return_value.get.return_value = fake_rb
99+
res = self.allocator._openshift_get_rolebindings("fake-project", "admin")
100+
self.assertEqual(res, {"subjects": []})
101+
102+
def test_list_rolebindings(self):
103+
fake_rb = mock.Mock(spec=["to_dict"])
104+
fake_rb.to_dict.return_value = {
105+
"items": ["rb1", "rb2"],
106+
}
107+
self.allocator.k8_client.resources.get.return_value.get.return_value = fake_rb
108+
res = self.allocator._openshift_list_rolebindings("fake-project")
109+
self.assertEqual(res, ["rb1", "rb2"])
110+
111+
def test_list_rolebindings_not_exists(self):
112+
fake_error = kexc.NotFoundError(mock.Mock())
113+
self.allocator.k8_client.resources.get.return_value.get.side_effect = fake_error
114+
res = self.allocator._openshift_list_rolebindings("fake-project")
115+
self.assertEqual(res, [])
116+
117+
def test_create_rolebindings(self):
118+
fake_rb = mock.Mock(spec=["to_dict"])
119+
fake_rb.to_dict.return_value = {}
120+
self.allocator.k8_client.resources.get.return_value.create.return_value = (
121+
fake_rb
122+
)
123+
res = self.allocator._openshift_create_rolebindings(
124+
"fake-project", "fake-user", "admin"
125+
)
126+
self.assertEqual(res, {})
127+
self.allocator.k8_client.resources.get.return_value.create.assert_called_with(
128+
namespace="fake-project",
129+
body={
130+
"metadata": {"name": "admin", "namespace": "fake-project"},
131+
"subjects": [{"name": "fake-user", "kind": "User"}],
132+
"roleRef": {"name": "admin", "kind": "ClusterRole"},
133+
},
134+
)

0 commit comments

Comments
 (0)