Skip to content

Commit a7641c4

Browse files
authored
Fuck OpenStack (#4)
Add in support to grab existing rules from the OpenStack distro, then essentially reduce them into one liners so we can override the existing policies. Then, because OpenStack is essentially bollocks, we need to work around its inadequacies to make it even work, because half the infromation required is missing, and a lot of what would be required is actually hard coded to fail.
1 parent bd27973 commit a7641c4

File tree

3 files changed

+216
-54
lines changed

3 files changed

+216
-54
lines changed

README.md

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,57 @@
77

88
Oslo policy generation and testing framework.
99

10-
## Installation
10+
### Network Service
11+
12+
We need the following to be allowed (non-root):
13+
14+
* Provisioning of provider networks in managed projects
15+
16+
Problem with Neutron is, it has zero view of identity hierarchies.
17+
When you create a network, for example, it infers the network from the token, and that's it.
18+
There is no way to infer the domain also and allow access at that level.
19+
This may, in fact, although not proven, go all the way back to Keystone not encoding this hierarchical information in the token.
20+
Which then, basically, says that Keystone's encoding of scope in a token is totally stupid in the first place, and should be way more generalized, because having to re-authenticate in multiple scopes is a massive butt pain.
21+
22+
Then, after you consider the lack of decent support for scoped policies, there is the fact that provisioning with a specific project ID is not even handled by policies at all, but hard coded, then we are in a world of pain.
23+
24+
Our only remaining option is to take our domain admin `manager` role, and create a role on every project we create.
25+
Then when we want to create a network, we need to create a token bound to that project.
26+
Finally, we need to allow the `manager` to create provider networks in the project.
27+
28+
## Usage
29+
30+
You first need to create a non-admin role to perform all the necessary actions.
31+
Unikorn already requires the [SCS domain admin](https://docs.scs.community/standards/scs-0302-v1-domain-manager-role/) functionality for reduced privilege user/project creation, so we use the same role.
32+
As an admin account:
33+
34+
```bash
35+
openstack role create manager
36+
```
37+
38+
Assuming a user has then been created, with the `manager` role on a domain, authenticate as that user scoped to the managed domain, then create a project/user:
39+
40+
```bash
41+
openstack project create --domain my-managed-domain my-project
42+
openstack user create --domain my-managed-domain my-user
43+
```
44+
45+
Then to actually provision a provider network you need to bind the `manager` role to the project:
46+
47+
```bash
48+
openstack role add --user my-manager-user --domain my-managed-domain --project my-project manager
49+
```
50+
51+
At this point, you must have [installed](#installation) the policies we define in this library, though whatever mechanism your orchestration layer provides.
52+
Re-authenticate as the `manager` user, now scoped to the project, and create the network:
53+
54+
```bash
55+
openstack network create --provider-network-type vlan --provider-physical-network physnet1 --provider-segment 666 my-provider-network
56+
```
57+
58+
Then, after all that, take a step back and assess your life choices and whether you should be using OpenStack in the first place...
59+
60+
### Installation
1161

1262
> [!NOTE]
1363
> Running the following will install all the necessary dependencies.
@@ -18,21 +68,23 @@ python3 -m build
1868
pip3 install dist/python_unikorn_openstack_policy-0.1.0-py3-none-any.whl
1969
```
2070

21-
## Generating Policy Files
71+
### Generating Policy Files
2272

2373
```bash
2474
oslopolicy-policy-generator --namespace unikorn_openstack_policy
2575
```
2676

27-
## Coding Standards
77+
## Development
78+
79+
### Coding Standards
2880

2981
You require 10/10 when running:
3082

3183
```bash
3284
pylint unikorn_openstack_policy
3385
```
3486

35-
## Testing
87+
### Testing
3688

3789
You must test everything works and get 100% pass rate when running:
3890

unikorn_openstack_policy/network.py

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
# pylint: disable=line-too-long
2020

21-
from neutron.conf.policies import base, network
21+
import re
22+
23+
from neutron.conf import policies
2224
from oslo_policy import policy
2325

2426
rules = [
@@ -33,8 +35,8 @@
3335
# A common helper to define that the user is a manager and the resource
3436
# target is in the same domain as the user is scoped to.
3537
policy.RuleDefault(
36-
name='is_domain_manager_owner',
37-
check_str='rule:is_domain_manager and domain_id:%(domain_id)s',
38+
name='is_project_manager_owner',
39+
check_str='rule:is_domain_manager and project_id:%(project_id)s',
3840
description='Rule for domain manager ownership',
3941
),
4042

@@ -44,60 +46,138 @@
4446
# allow provider networks, if the prior rule changes, then we can open up a security hole.
4547
policy.RuleDefault(
4648
name='create_network',
47-
check_str='rule:is_domain_manager_owner or rule:base_create_network',
49+
check_str='rule:is_project_manager_owner or rule:base_create_network',
4850
description='Create a network',
4951
),
5052
policy.RuleDefault(
5153
name='delete_network',
52-
check_str='rule:is_domain_manager_owner or rule:base_delete_network',
54+
check_str='rule:is_project_manager_owner or rule:base_delete_network',
5355
description='Delete a network',
5456
),
5557
policy.RuleDefault(
5658
name='create_network:segments',
57-
check_str='rule:is_domain_manager_owner or rule:base_create_network:segments',
59+
check_str='rule:is_project_manager_owner or rule:base_create_network:segments',
5860
description='Specify ``segments`` attribute when creating a network',
5961
),
6062
policy.RuleDefault(
6163
name='create_network:provider:network_type',
62-
check_str='rule:is_domain_manager_owner or rule:base_create_network:provider:physical_network',
64+
check_str='rule:is_project_manager_owner or rule:base_create_network:provider:physical_network',
6365
description='Specify ``provider:network_type`` when creating a network',
6466
),
6567
policy.RuleDefault(
6668
name='create_network:provider:physical_network',
67-
check_str='rule:is_domain_manager_owner or rule:base_create_network:provider:network_type',
69+
check_str='rule:is_project_manager_owner or rule:base_create_network:provider:network_type',
6870
description='Specify ``provider:physical_network`` when creating a network',
6971
),
7072
policy.RuleDefault(
7173
name='create_network:provider:segmentation_id',
72-
check_str='rule:is_domain_manager_owner or rule:base_create_network:provider:segmentation_id',
74+
check_str='rule:is_project_manager_owner or rule:base_create_network:provider:segmentation_id',
7375
description='Specify ``provider:segmentation_id`` when creating a network',
7476
),
7577
]
7678

7779

78-
def basify(rule):
79-
"""Do a copy of the existing rule with a base_ name prefix"""
80+
class MissingRuleException(Exception):
81+
"""
82+
Raised when a rule cannot be resolved
83+
"""
84+
85+
86+
def _find_rule(name, rule_list):
87+
"""Return a named rule if it exists or None"""
88+
89+
for rule in rule_list:
90+
if rule.name == name:
91+
return rule
92+
93+
raise MissingRuleException('unable to resolve referenced rule ' + name)
94+
95+
96+
def _wrap_check_str(tokens):
97+
"""If the check string is more than one token, wrap it in parenteses"""
98+
99+
if len(tokens) > 1:
100+
tokens.insert(0, '(')
101+
tokens.append(')')
102+
103+
return tokens
104+
105+
106+
def _recurse_build_check_str(check_str, rule_list):
107+
"""
108+
Given a check string, this does macro expansion of rule:roo strings
109+
removing and inlining them.
110+
"""
111+
112+
out = []
80113

81-
return policy.RuleDefault(
82-
name='base_' + rule.name, check_str=rule.check_str, description=rule.description)
114+
for token in re.split(r'\s+', check_str):
115+
if token.isspace():
116+
continue
83117

118+
# Handle leading parentheses.
119+
clean = token.lstrip('(')
120+
for _ in range(len(token) - len(clean)):
121+
out.append('(')
84122

85-
def inherited(rule):
86-
"""Is the rule inherited by one that we have defined?"""
123+
# Handle trailing parentheses.
124+
token = clean
87125

88-
return any(rule.name == my_rule.name for my_rule in rules)
126+
clean = token.rstrip(')')
127+
trail = len(token) - len(clean)
128+
129+
# If the token is a rule, then expand it.
130+
matches = re.match(r'rule:([\w_]+)', clean)
131+
if matches:
132+
rule = _find_rule(matches.group(1), rule_list)
133+
sub_check_str = _recurse_build_check_str(rule.check_str, rule_list)
134+
out.extend(_wrap_check_str(sub_check_str))
135+
else:
136+
out.append(clean)
137+
138+
for _ in range(trail):
139+
out.append(')')
140+
141+
return out
142+
143+
144+
def _build_check_str(check_str, rule_list):
145+
"""
146+
Given a check string, this does macro expansion of rule:roo strings
147+
removing and inlining them.
148+
"""
149+
150+
check_str = ' '.join(_recurse_build_check_str(check_str, rule_list))
151+
check_str = re.sub(r'\( ', '(', check_str)
152+
check_str = re.sub(r' \)', ')', check_str)
153+
return check_str
89154

90155

91156
def list_rules():
92157
"""Implements the "oslo.policy.policies" entry point"""
93158

94-
# Okay now for the "hard" bit. We reference built in rules directly from neutron so
95-
# we can augment the exact rules for a specific version, thus we pick up any changes.
96-
# We prefix the existing rules with "base_" as already seen above but only if they
97-
# are redefined (and by implication referenced) from one of ours.
98-
network_rules = [basify(rule) for rule in network.list_rules() if inherited(rule)]
159+
# For every defined rule, look for a corresponding one sourced directly
160+
# from neutron, this means we can augment the exact rule defined for a
161+
# specific version of neutron,
162+
network_rules = list(policies.list_rules())
163+
164+
inherited_network_rules = []
165+
166+
for rule in rules:
167+
try:
168+
network_rule = _find_rule(rule.name, network_rules)
169+
170+
check_str = _build_check_str(network_rule.check_str, network_rules)
171+
172+
inherited_network_rules.append(policy.RuleDefault(
173+
name='base_' + rule.name,
174+
check_str=check_str,
175+
description=rule.description,
176+
))
177+
except MissingRuleException:
178+
pass
179+
180+
return inherited_network_rules + rules
99181

100-
# Those rules will also rely on base rules, so include them too in the final output.
101-
return base.list_rules() + network_rules + rules
102182

103183
# vi: ts=4 et:

0 commit comments

Comments
 (0)