Skip to content

Commit 5e6f324

Browse files
committed
CH-152 user attributes generic API
1 parent f8fb173 commit 5e6f324

2 files changed

Lines changed: 149 additions & 111 deletions

File tree

libraries/cloudharness-common/cloudharness/auth/quota.py

Lines changed: 9 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -5,112 +5,24 @@
55
from cloudharness_model.models import ApplicationConfig
66
from cloudharness import log
77

8+
from .user_attributes import UserNotFound, _filter_attrs, _construct_attribute_tree, _compute_attributes_from_tree, get_user_attributes
89
# quota tree node to hold the tree quota attributes
910

1011

11-
class QuotaNode:
12-
def __init__(self, name, attrs):
13-
self.attrs = attrs
14-
self.name = name
15-
self.children = []
1612

17-
def addChild(self, child):
18-
self.children.append(child)
19-
20-
21-
def _filter_quota_attrs(attrs, valid_keys_map):
22-
# only use the attributes defined by the valid keys map
23-
valid_attrs = {}
24-
if attrs is None:
25-
return valid_attrs
26-
for key in attrs:
27-
if key in valid_keys_map:
28-
# map to value
29-
valid_attrs.update({key: attrs[key][0]})
30-
return valid_attrs
3113

3214

3315
def get_group_quotas(group, application_config: ApplicationConfig):
3416
base_quotas = application_config.get("harness", {}).get("quotas", {})
3517
valid_keys_map = {key for key in base_quotas}
36-
return _compute_quotas_from_tree(_construct_quota_tree([group], valid_keys_map))
37-
38-
39-
def _construct_quota_tree(groups, valid_keys_map) -> QuotaNode:
40-
root = QuotaNode("root", {})
41-
for group in groups:
42-
r = root
43-
paths = group["path"].split("/")[1:]
44-
# loop through all segements except the last segment
45-
# the last segment is the one we want to add the attributes to
46-
for segment in paths[0: len(paths) - 1]:
47-
for child in r.children:
48-
if child.name == segment:
49-
r = child
50-
break
51-
else:
52-
# no child found, add it with the segment name of the path
53-
n = QuotaNode(segment, {})
54-
r.addChild(n)
55-
r = n
56-
# add the child with it's attributes and last segment name
57-
n = QuotaNode(
58-
paths[len(paths) - 1],
59-
_filter_quota_attrs(group["attributes"], valid_keys_map)
60-
)
61-
r.addChild(n)
62-
return root
63-
64-
65-
def _compute_quotas_from_tree(node: QuotaNode):
66-
"""Recursively traverse the tree and find the quota per level
67-
the lower leafs overrule parent leafs values
18+
return _compute_attributes_from_tree(_construct_attribute_tree([group], valid_keys_map))
6819

69-
Args:
70-
node (QuotaNode): the quota tree of QuotaNodes of the user for the given application
71-
72-
Returns:
73-
dict: key/value pairs of the quotas
7420

75-
Example:
76-
{'quota-ws-maxcpu': 1000, 'quota-ws-open': 10, 'quota-ws-max': 8}
77-
78-
Algorithm explanation:
79-
/Base {'quota-ws-max': 12345, 'quota-ws-maxcpu': 50, 'quota-ws-open': 1}\n
80-
/Base/Base 1/Base 1 1 {'quota-ws-maxcpu': 2, 'quota-ws-open': 10}\n
81-
/Base/Base 2 {'quota-ws-max': 8, 'quota-ws-maxcpu': 250}\n
82-
/Low CPU {'quota-ws-max': 3, 'quota-ws-maxcpu': 1000, 'quota-ws-open': 1}\n
83-
84-
result: {'quota-ws-maxcpu': 1000, 'quota-ws-open': 10, 'quota-ws-max': 8}\n
85-
quota-ws-maxcpu from path "/Low CPU"\n
86-
--> overrules paths "/Base/Base 1/Base 1 1" and "/Base/Base 2" (higher value)\n
87-
--> /Base quota-ws-max is not used because this one is not the lowest
88-
leaf with this attribute (Base 1 1 and Base 2 are "lower")\n
89-
quota-ws-open from path "/Base/Base 1/Base 1 1"\n
90-
quota-ws-max from path "/Base/Base 2"\n
91-
"""
92-
new_attrs = {}
93-
for child in node.children:
94-
child_attrs = _compute_quotas_from_tree(child)
95-
for key in child_attrs:
96-
try:
97-
# we expect all quota values to be numbers: the unit is implicit and
98-
# defined at usage time
99-
child_val = attribute_to_quota(child_attrs[key])
100-
except:
101-
# value not a float, skip
102-
continue
103-
if not key in new_attrs or new_attrs[key] < child_val:
104-
new_attrs.update({key: child_val})
105-
for key in new_attrs:
106-
node.attrs.update({key: new_attrs[key]})
107-
return node.attrs
10821

10922

11023
def attribute_to_quota(attr_value: str):
11124
return float(re.sub("[^0-9.]", "", attr_value) if type(attr_value) is str else attr_value)
11225

113-
11426
def get_user_quotas(application_config: ApplicationConfig = None, user_id: str = None) -> dict:
11527
"""Get the user quota from Keycloak and application
11628
@@ -126,28 +38,14 @@ def get_user_quotas(application_config: ApplicationConfig = None, user_id: str =
12638
"""
12739
if not application_config:
12840
application_config = get_current_configuration()
129-
13041
base_quotas = application_config.get("harness", {}).get("quotas", {})
42+
43+
44+
valid_keys_map = {key for key in base_quotas}
45+
13146
try:
132-
auth_client = AuthClient()
133-
if not user_id:
134-
user_id = auth_client.get_current_user()["id"]
135-
user = auth_client.get_user(user_id, with_details=True)
136-
except KeycloakError as e:
47+
return get_user_attributes(user_id, valid_keys_map=valid_keys_map, default_attributes=base_quotas)
48+
49+
except UserNotFound as e:
13750
log.warning("Quotas not available: error retrieving user: %s", user_id)
13851
return base_quotas
139-
140-
valid_keys_map = {key for key in base_quotas}
141-
142-
group_quotas = _compute_quotas_from_tree(
143-
_construct_quota_tree(
144-
user["userGroups"],
145-
valid_keys_map))
146-
user_quotas = _filter_quota_attrs(user["attributes"], valid_keys_map)
147-
for key in group_quotas:
148-
if key not in user_quotas:
149-
user_quotas.update({key: group_quotas[key]})
150-
for key in base_quotas:
151-
if key not in user_quotas:
152-
user_quotas.update({key: attribute_to_quota(base_quotas[key])})
153-
return user_quotas
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import re
2+
from keycloak import KeycloakError
3+
from .keycloak import AuthClient
4+
from cloudharness.applications import get_current_configuration
5+
from cloudharness_model.models import ApplicationConfig
6+
from cloudharness import log
7+
8+
class KCAttributeNode:
9+
def __init__(self, name, attrs):
10+
self.attrs = attrs
11+
self.name = name
12+
self.children = []
13+
14+
def addChild(self, child):
15+
self.children.append(child)
16+
17+
18+
def _filter_attrs(attrs, valid_keys_map):
19+
# only use the attributes defined by the valid keys map
20+
valid_attrs = {}
21+
if attrs is None:
22+
return valid_attrs
23+
for key in attrs:
24+
if key in valid_keys_map:
25+
# map to value
26+
valid_attrs.update({key: attrs[key][0]})
27+
return valid_attrs
28+
29+
def _construct_attribute_tree(groups, valid_keys_map) -> KCAttributeNode:
30+
"""Construct a tree of attributes from the user groups"""
31+
root = KCAttributeNode("root", {})
32+
for group in groups:
33+
r = root
34+
paths = group["path"].split("/")[1:]
35+
# loop through all segements except the last segment
36+
# the last segment is the one we want to add the attributes to
37+
for segment in paths[0: len(paths) - 1]:
38+
for child in r.children:
39+
if child.name == segment:
40+
r = child
41+
break
42+
else:
43+
# no child found, add it with the segment name of the path
44+
n = KCAttributeNode(segment, {})
45+
r.addChild(n)
46+
r = n
47+
# add the child with it's attributes and last segment name
48+
n = KCAttributeNode(
49+
paths[len(paths) - 1],
50+
_filter_attrs(group["attributes"], valid_keys_map)
51+
)
52+
r.addChild(n)
53+
return root
54+
55+
56+
class UserNotFound(Exception):
57+
pass
58+
59+
def _compute_attributes_from_tree(node: KCAttributeNode, transform_value_fn=lambda x: x):
60+
"""Recursively traverse the tree and find the attributes per level
61+
the lower leafs overrule parent leafs values
62+
63+
Args:
64+
node (QuotaNode): the quota tree of QuotaNodes of the user for the given application
65+
transform_value_fn (function): function to transform the value of the attribute
66+
67+
Returns:
68+
dict: key/value pairs of the quotas
69+
70+
Example:
71+
{'quota-ws-maxcpu': 1000, 'quota-ws-open': 10, 'quota-ws-max': 8}
72+
73+
Algorithm explanation:
74+
/Base {'quota-ws-max': 12345, 'quota-ws-maxcpu': 50, 'quota-ws-open': 1}\n
75+
/Base/Base 1/Base 1 1 {'quota-ws-maxcpu': 2, 'quota-ws-open': 10}\n
76+
/Base/Base 2 {'quota-ws-max': 8, 'quota-ws-maxcpu': 250}\n
77+
/Low CPU {'quota-ws-max': 3, 'quota-ws-maxcpu': 1000, 'quota-ws-open': 1}\n
78+
79+
result: {'quota-ws-maxcpu': 1000, 'quota-ws-open': 10, 'quota-ws-max': 8}\n
80+
quota-ws-maxcpu from path "/Low CPU"\n
81+
--> overrules paths "/Base/Base 1/Base 1 1" and "/Base/Base 2" (higher value)\n
82+
--> /Base quota-ws-max is not used because this one is not the lowest
83+
leaf with this attribute (Base 1 1 and Base 2 are "lower")\n
84+
quota-ws-open from path "/Base/Base 1/Base 1 1"\n
85+
quota-ws-max from path "/Base/Base 2"\n
86+
"""
87+
new_attrs = {}
88+
for child in node.children:
89+
child_attrs = _compute_attributes_from_tree(child)
90+
for key in child_attrs:
91+
try:
92+
child_val = transform_value_fn(child_attrs[key])
93+
except:
94+
# value not a float, skip
95+
continue
96+
if not key in new_attrs or new_attrs[key] < child_val:
97+
new_attrs.update({key: child_val})
98+
for key in new_attrs:
99+
node.attrs.update({key: new_attrs[key]})
100+
return node.attrs
101+
102+
103+
104+
def get_user_attributes(user_id: str = None, valid_keys_map={}, default_attributes={}, transform_value_fn=lambda x: x) -> dict:
105+
"""Get the user attributes from Keycloak recursively from the user attributes and groups
106+
107+
Args:
108+
user_id (str): the Keycloak user id or username to get the quotas for
109+
valid_keys_map (dict): the valid keys to use for the attributes
110+
default_attributes (dict): the default attributes to use if the user does not have the attribute
111+
112+
Returns:
113+
dict: key/value pairs of the user attributes
114+
115+
Example:
116+
{'quota-ws-maxcpu': 1000, 'quota-ws-open': 10, 'quota-ws-max': 8}
117+
"""
118+
119+
try:
120+
auth_client = AuthClient()
121+
if not user_id:
122+
user_id = auth_client.get_current_user()["id"]
123+
user = auth_client.get_user(user_id, with_details=True)
124+
except KeycloakError as e:
125+
log.warning("Quotas not available: error retrieving user: %s", user_id)
126+
raise UserNotFound("User not found") from e
127+
128+
129+
group_quotas = _compute_attributes_from_tree(
130+
_construct_attribute_tree(
131+
user["userGroups"],
132+
valid_keys_map), transform_value_fn)
133+
user_attrs = _filter_attrs(user["attributes"], valid_keys_map)
134+
for key in group_quotas:
135+
if key not in user_attrs:
136+
user_attrs.update({key: group_quotas[key]})
137+
for key in default_attributes:
138+
if key not in user_attrs:
139+
user_attrs.update({key: transform_value_fn(default_attributes[key])})
140+
return user_attrs

0 commit comments

Comments
 (0)