Skip to content

Commit 5ceafcc

Browse files
authored
Add Policy to Allow Quota Updates (#5)
A user with the 'manager' role should be able to set quotas in dynamic projects that it manages.
1 parent a7641c4 commit 5ceafcc

File tree

11 files changed

+492
-261
lines changed

11 files changed

+492
-261
lines changed

.github/workflows/pull-request.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ jobs:
2424
- name: Unit Test
2525
run: python -m unittest discover
2626
- name: Test Generation
27-
run: oslopolicy-policy-generator --namespace unikorn_openstack_policy
27+
run: |
28+
oslopolicy-policy-generator --namespace unikorn_openstack_policy_compute
29+
oslopolicy-policy-generator --namespace unikorn_openstack_policy_network

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/dist
22
/python_unikorn_openstack_policy.egg-info/
33
__pycache__
4+
*.swp

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ pip3 install dist/python_unikorn_openstack_policy-0.1.0-py3-none-any.whl
7171
### Generating Policy Files
7272

7373
```bash
74-
oslopolicy-policy-generator --namespace unikorn_openstack_policy
74+
oslopolicy-policy-generator --namespace unikorn_openstack_policy_compute
75+
oslopolicy-policy-generator --namespace unikorn_openstack_policy_network
7576
```
7677

7778
## Development

pyproject.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,19 @@ classifiers = [
2525
]
2626
dependencies = [
2727
"oslo.config",
28-
"neutron"
28+
"neutron",
29+
"nova"
2930
]
3031

3132
[project.urls]
3233
homepage = "https://github.com/unikorn-cloud/python-unikorn-openstack-policy"
3334

3435
[project.entry-points."oslo.policy.policies"]
35-
unikorn_openstack_policy = "unikorn_openstack_policy:list_rules"
36+
unikorn_openstack_policy_compute = "unikorn_openstack_policy.compute:list_rules"
37+
unikorn_openstack_policy_network = "unikorn_openstack_policy.network:list_rules"
3638

3739
[project.entry-points."oslo.policy.enforcer"]
38-
unikorn_openstack_policy = "unikorn_openstack_policy:get_enforcer"
40+
unikorn_openstack_policy_compute = "unikorn_openstack_policy.compute:get_enforcer"
41+
unikorn_openstack_policy_network = "unikorn_openstack_policy.network:get_enforcer"
3942

4043
# vi: ts=4 noet:

unikorn_openstack_policy/__init__.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,4 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""
16-
Base library entrypoints.
17-
"""
18-
19-
import itertools
20-
21-
from oslo_config import cfg
22-
from oslo_policy import policy
23-
24-
from unikorn_openstack_policy import network
25-
26-
def list_rules():
27-
"""Implements the "oslo.policy.policies" entry point"""
28-
29-
return itertools.chain(
30-
network.list_rules(),
31-
)
32-
33-
34-
def get_enforcer():
35-
"""Implements the "oslo.policy.enforcer" entry point"""
36-
37-
enforcer = policy.Enforcer(conf=cfg.CONF)
38-
enforcer.register_defaults(list_rules())
39-
40-
return enforcer
41-
4215
# vi: ts=4 et:

unikorn_openstack_policy/base.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Copyright 2024 the Unikorn Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Defines Oslo Policy Rules.
17+
"""
18+
19+
# pylint: disable=line-too-long
20+
21+
import itertools
22+
import re
23+
24+
from oslo_policy import policy
25+
26+
rules = [
27+
# The domain manager has the role 'manager', as defined by
28+
# https://docs.scs.community/standards/scs-0302-v1-domain-manager-role/
29+
policy.RuleDefault(
30+
name='is_domain_manager',
31+
check_str='role:manager',
32+
description='Rule for manager access',
33+
),
34+
35+
# A common helper to define that the user is a manager and the resource
36+
# target is in the same domain as the user is scoped to.
37+
policy.RuleDefault(
38+
name='is_project_manager_owner',
39+
check_str='rule:is_domain_manager and project_id:%(project_id)s',
40+
description='Rule for domain manager ownership',
41+
),
42+
]
43+
44+
45+
def list_rules():
46+
"""Implements the "oslo.policy.policies" entry point"""
47+
48+
return rules
49+
50+
51+
class MissingRuleException(Exception):
52+
"""
53+
Raised when a rule cannot be resolved
54+
"""
55+
56+
57+
def _find_rule(name, rule_list):
58+
"""Return a named rule if it exists or None"""
59+
60+
for rule in rule_list:
61+
if rule.name == name:
62+
return rule
63+
64+
raise MissingRuleException('unable to resolve referenced rule ' + name)
65+
66+
67+
def _wrap_check_str(tokens):
68+
"""If the check string is more than one token, wrap it in parenteses"""
69+
70+
if len(tokens) > 1:
71+
tokens.insert(0, '(')
72+
tokens.append(')')
73+
74+
return tokens
75+
76+
77+
def _recurse_build_check_str(check_str, rule_list):
78+
"""
79+
Given a check string, this does macro expansion of rule:roo strings
80+
removing and inlining them.
81+
"""
82+
83+
out = []
84+
85+
for token in re.split(r'\s+', check_str):
86+
if token.isspace():
87+
continue
88+
89+
# Handle leading parentheses.
90+
clean = token.lstrip('(')
91+
for _ in range(len(token) - len(clean)):
92+
out.append('(')
93+
94+
# Handle trailing parentheses.
95+
token = clean
96+
97+
clean = token.rstrip(')')
98+
trail = len(token) - len(clean)
99+
100+
# If the token is a rule, then expand it.
101+
matches = re.match(r'rule:([\w_]+)', clean)
102+
if matches:
103+
rule = _find_rule(matches.group(1), rule_list)
104+
sub_check_str = _recurse_build_check_str(rule.check_str, rule_list)
105+
out.extend(_wrap_check_str(sub_check_str))
106+
else:
107+
out.append(clean)
108+
109+
for _ in range(trail):
110+
out.append(')')
111+
112+
return out
113+
114+
115+
def _build_check_str(check_str, rule_list):
116+
"""
117+
Given a check string, this does macro expansion of rule:roo strings
118+
removing and inlining them.
119+
"""
120+
121+
check_str = ' '.join(_recurse_build_check_str(check_str, rule_list))
122+
check_str = re.sub(r'\( ', '(', check_str)
123+
check_str = re.sub(r' \)', ')', check_str)
124+
return check_str
125+
126+
127+
def inherit_rules(mine, theirs):
128+
"""
129+
Given my rules, add any from openstack so we can use that as a source of truth.
130+
"""
131+
132+
expanded = []
133+
134+
for rule in mine:
135+
try:
136+
inherited_rule = _find_rule(rule.name, theirs)
137+
138+
check_str = _build_check_str(inherited_rule.check_str, theirs)
139+
140+
expanded.append(policy.RuleDefault(
141+
name=rule.name,
142+
check_str=f'{rule.check_str} or ({check_str})',
143+
description=rule.description,
144+
))
145+
except MissingRuleException:
146+
pass
147+
148+
return itertools.chain(rules, expanded)
149+
150+
# vi: ts=4 et:

unikorn_openstack_policy/compute.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright 2024 the Unikorn Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Defines Oslo Policy Rules.
17+
"""
18+
19+
# pylint: disable=line-too-long
20+
21+
from nova import policies
22+
from oslo_config import cfg
23+
from oslo_policy import policy
24+
from unikorn_openstack_policy import base
25+
26+
rules = [
27+
# The domain manager needs to be able to alter the default quotas
28+
# or it won't we able to fulfill any cluster creation requests.
29+
policy.RuleDefault(
30+
name='os_compute_api:os-quota-sets:update',
31+
check_str='rule:is_project_manager_owner',
32+
description='Update the quotas',
33+
)
34+
]
35+
36+
37+
def list_rules():
38+
"""Implements the "oslo.policy.policies" entry point"""
39+
40+
# For every defined rule, look for a corresponding one sourced directly
41+
# from nova, this means we can augment the exact rule defined for a
42+
# specific version of nova,
43+
return base.inherit_rules(rules, list(policies.list_rules()))
44+
45+
46+
def get_enforcer():
47+
"""Implements the "oslo.policy.enforcer" entry point"""
48+
49+
enforcer = policy.Enforcer(conf=cfg.CONF)
50+
enforcer.register_defaults(list_rules())
51+
52+
return enforcer
53+
54+
55+
# vi: ts=4 et:

0 commit comments

Comments
 (0)