Skip to content

Commit 7e66279

Browse files
authored
Merge pull request #229 from QuanMPhm/186/openshift_rbac
Allow direct communication to Openshift RBAC API
2 parents e950e83 + 8bdb720 commit 7e66279

File tree

3 files changed

+259
-25
lines changed

3 files changed

+259
-25
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: 115 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
]
4646

4747

48+
OPENSHIFT_ROLES = ["admin", "edit", "view"]
49+
50+
4851
def clean_openshift_metadata(obj):
4952
if "metadata" in obj:
5053
for attr in IGNORED_ATTRIBUTES:
@@ -262,25 +265,51 @@ def create_federated_user(self, unique_id):
262265
logger.info(f"User {unique_id} successfully created")
263266

264267
def assign_role_on_user(self, username, project_id):
265-
# /users/<user_name>/projects/<project>/roles/<role>
266-
url = (
267-
f"{self.auth_url}/users/{username}/projects/{project_id}"
268-
f"/roles/{self.member_role_name}"
269-
)
268+
"""Assign a role to a user in a project using direct OpenShift API calls"""
270269
try:
271-
r = self.session.put(url)
272-
self.check_response(r)
273-
except Conflict:
270+
# Try to get existing rolebindings with same name
271+
# as the role name in project's namespace
272+
rolebinding = self._openshift_get_rolebindings(
273+
project_id, self.member_role_name
274+
)
275+
276+
if not self._user_in_rolebinding(username, rolebinding):
277+
# Add user to existing rolebinding
278+
if "subjects" not in rolebinding:
279+
rolebinding["subjects"] = []
280+
rolebinding["subjects"].append({"kind": "User", "name": username})
281+
self._openshift_update_rolebindings(project_id, rolebinding)
282+
283+
except kexc.NotFoundError:
284+
# Create new rolebinding if it doesn't exist
285+
self._openshift_create_rolebindings(
286+
project_id, username, self.member_role_name
287+
)
288+
except kexc.ConflictError:
289+
# Role already exists, ignore
274290
pass
275291

276292
def remove_role_from_user(self, username, project_id):
277-
# /users/<user_name>/projects/<project>/roles/<role>
278-
url = (
279-
f"{self.auth_url}/users/{username}/projects/{project_id}"
280-
f"/roles/{self.member_role_name}"
281-
)
282-
r = self.session.delete(url)
283-
self.check_response(r)
293+
"""Remove a role from a user in a project using direct OpenShift API calls"""
294+
try:
295+
rolebinding = self._openshift_get_rolebindings(
296+
project_id, self.member_role_name
297+
)
298+
299+
if "subjects" in rolebinding:
300+
rolebinding["subjects"] = [
301+
subject
302+
for subject in rolebinding["subjects"]
303+
if not (
304+
subject.get("kind") == "User"
305+
and subject.get("name") == username
306+
)
307+
]
308+
self._openshift_update_rolebindings(project_id, rolebinding)
309+
310+
except kexc.NotFoundError:
311+
# Rolebinding doesn't exist, nothing to remove
312+
pass
284313

285314
def _create_project(self, project_name, project_id):
286315
pi_username = self.allocation.project.pi.username
@@ -306,13 +335,13 @@ def _create_project(self, project_name, project_id):
306335
logger.info(f"Project {project_id} and limit range successfully created")
307336

308337
def _get_role(self, username, project_id):
309-
# /users/<user_name>/projects/<project>/roles/<role>
310-
url = (
311-
f"{self.auth_url}/users/{username}/projects/{project_id}"
312-
f"/roles/{self.member_role_name}"
338+
rolebindings = self._openshift_get_rolebindings(
339+
project_id, self.member_role_name
313340
)
314-
r = self.session.get(url)
315-
return self.check_response(r)
341+
if not self._user_in_rolebinding(username, rolebindings):
342+
raise NotFound(
343+
f"User {username} has no rolebindings in project {project_id}"
344+
)
316345

317346
def _get_project(self, project_id):
318347
return self._openshift_get_project(project_id)
@@ -323,9 +352,23 @@ def _delete_user(self, username):
323352
logger.info(f"User {username} successfully deleted")
324353

325354
def get_users(self, project_id):
326-
url = f"{self.auth_url}/projects/{project_id}/users"
327-
r = self.session.get(url)
328-
return set(self.check_response(r))
355+
"""Get all users with roles in a project"""
356+
users = set()
357+
358+
# Check all standard OpenShift roles
359+
for role in OPENSHIFT_ROLES:
360+
try:
361+
rolebinding = self._openshift_get_rolebindings(project_id, role)
362+
if "subjects" in rolebinding:
363+
users.update(
364+
subject["name"]
365+
for subject in rolebinding["subjects"]
366+
if subject.get("kind") == "User"
367+
)
368+
except kexc.NotFoundError:
369+
continue
370+
371+
return users
329372

330373
def _openshift_get_user(self, username):
331374
api = self.get_resource_api(API_USER, "User")
@@ -488,3 +531,51 @@ def _openshift_delete_resourcequota(self, project_id, resourcequota_name):
488531
"""In an openshift namespace {project_id) delete a specified resourcequota"""
489532
api = self.get_resource_api(API_CORE, "ResourceQuota")
490533
return api.delete(namespace=project_id, name=resourcequota_name).to_dict()
534+
535+
def _user_in_rolebinding(self, username, rolebinding):
536+
"""Check if a user is in a rolebinding"""
537+
if "subjects" not in rolebinding:
538+
return False
539+
540+
return any(
541+
subject.get("kind") == "User" and subject.get("name") == username
542+
for subject in rolebinding["subjects"]
543+
)
544+
545+
def _openshift_get_rolebindings(self, project_name, role):
546+
api = self.get_resource_api(API_RBAC, "RoleBinding")
547+
result = clean_openshift_metadata(
548+
api.get(namespace=project_name, name=role).to_dict()
549+
)
550+
551+
# Ensure subjects is a list
552+
if not result.get("subjects"):
553+
result["subjects"] = []
554+
555+
return result
556+
557+
def _openshift_create_rolebindings(self, project_name, username, role):
558+
api = self.get_resource_api(API_RBAC, "RoleBinding")
559+
payload = {
560+
"metadata": {"name": role, "namespace": project_name},
561+
"subjects": [{"name": username, "kind": "User"}],
562+
"roleRef": {"name": role, "kind": "ClusterRole"},
563+
}
564+
return clean_openshift_metadata(
565+
api.create(body=payload, namespace=project_name).to_dict()
566+
)
567+
568+
def _openshift_update_rolebindings(self, project_name, rolebinding):
569+
api = self.get_resource_api(API_RBAC, "RoleBinding")
570+
return clean_openshift_metadata(
571+
api.patch(body=rolebinding, namespace=project_name).to_dict()
572+
)
573+
574+
def _openshift_list_rolebindings(self, project_name):
575+
"""List all rolebindings in a project"""
576+
api = self.get_resource_api(API_RBAC, "RoleBinding")
577+
try:
578+
result = clean_openshift_metadata(api.get(namespace=project_name).to_dict())
579+
return result.get("items", [])
580+
except kexc.NotFoundError:
581+
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)