Skip to content

Commit

Permalink
Fuck OpenStack
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
spjmurray committed Aug 6, 2024
1 parent bd27973 commit c474f61
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 60 deletions.
60 changes: 56 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,57 @@

Oslo policy generation and testing framework.

## Installation
### Network Service

We need the following to be allowed (non-root):

* Provisioning of provider networks in managed projects

Problem with Neutron is, it has zero view of identity hierarchies.
When you create a network, for example, it infers the network from the token, and that's it.
There is no way to infer the domain also and allow access at that level.
This may, in fact, although not proven, go all the way back to Keystone not encoding this hierarchical information in the token.
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.

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.

Our only remaining option is to take out domain admin `manager` role, and create a role on every project we create.
Then when we want to create a network, we need to create a token bound to that project.
Finally, we need to allow the `manager` to create provider networks in the project.

## Usage

You first need to create a non-admin role to perform all the necessary actions.
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.
As an admin account:

```bash
openstack role create manager
```

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:

```bash
openstack project create --domain my-managed-domain my-project
openstack user create --domain my-managed-domain my-user
```

Then to actually provision a provider network you need to bind the `manager` role to the project:

```bash
openstack role add --user my-manager-user --domain my-managed-domain --project my-project manager
```

At this point, you must have [installed](#installation) the policies we define in this library, though whatever mechanism your orchestration layer provides.
Re-authenticate as the `manager` user, now scoped to the project, and create the network:

```bash
openstack network create --provider-network-type vlan --provider-physical-network physnet1 --provider-segment 666 my-provider-network
```

Then, after all that, take a step back and assess whether you should be using OpenStack in the first place...

### Installation

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

## Generating Policy Files
### Generating Policy Files

```bash
oslopolicy-policy-generator --namespace unikorn_openstack_policy
```

## Coding Standards
## Development

### Coding Standards

You require 10/10 when running:

```bash
pylint unikorn_openstack_policy
```

## Testing
### Testing

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

Expand Down
126 changes: 103 additions & 23 deletions unikorn_openstack_policy/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

# pylint: disable=line-too-long

from neutron.conf.policies import base, network
import re

from neutron.conf import policies
from oslo_policy import policy

rules = [
Expand All @@ -33,8 +35,8 @@
# A common helper to define that the user is a manager and the resource
# target is in the same domain as the user is scoped to.
policy.RuleDefault(
name='is_domain_manager_owner',
check_str='rule:is_domain_manager and domain_id:%(domain_id)s',
name='is_project_manager_owner',
check_str='rule:is_domain_manager and project_id:%(project_id)s',
description='Rule for domain manager ownership',
),

Expand All @@ -44,60 +46,138 @@
# allow provider networks, if the prior rule changes, then we can open up a security hole.
policy.RuleDefault(
name='create_network',
check_str='rule:is_domain_manager_owner or rule:base_create_network',
check_str='rule:is_project_manager_owner or rule:base_create_network',
description='Create a network',
),
policy.RuleDefault(
name='delete_network',
check_str='rule:is_domain_manager_owner or rule:base_delete_network',
check_str='rule:is_project_manager_owner or rule:base_delete_network',
description='Delete a network',
),
policy.RuleDefault(
name='create_network:segments',
check_str='rule:is_domain_manager_owner or rule:base_create_network:segments',
check_str='rule:is_project_manager_owner or rule:base_create_network:segments',
description='Specify ``segments`` attribute when creating a network',
),
policy.RuleDefault(
name='create_network:provider:network_type',
check_str='rule:is_domain_manager_owner or rule:base_create_network:provider:physical_network',
check_str='rule:is_project_manager_owner or rule:base_create_network:provider:physical_network',
description='Specify ``provider:network_type`` when creating a network',
),
policy.RuleDefault(
name='create_network:provider:physical_network',
check_str='rule:is_domain_manager_owner or rule:base_create_network:provider:network_type',
check_str='rule:is_project_manager_owner or rule:base_create_network:provider:network_type',
description='Specify ``provider:physical_network`` when creating a network',
),
policy.RuleDefault(
name='create_network:provider:segmentation_id',
check_str='rule:is_domain_manager_owner or rule:base_create_network:provider:segmentation_id',
check_str='rule:is_project_manager_owner or rule:base_create_network:provider:segmentation_id',
description='Specify ``provider:segmentation_id`` when creating a network',
),
]


def basify(rule):
"""Do a copy of the existing rule with a base_ name prefix"""
class MissingRuleException(Exception):
"""
Raised when a rule cannot be resolved
"""


def _find_rule(name, rule_list):
"""Return a named rule if it exists or None"""

for rule in rule_list:
if rule.name == name:
return rule

raise MissingRuleException('unable to resolve referenced rule ' + name)


def _wrap_check_str(tokens):
"""If the check string is more than one token, wrap it in parenteses"""

if len(tokens) > 1:
tokens.insert(0, '(')
tokens.append(')')

return tokens


def _recurse_build_check_str(check_str, rule_list):
"""
Given a check string, this does macro expansion of rule:roo strings
removing and inlining them.
"""

out = []

return policy.RuleDefault(
name='base_' + rule.name, check_str=rule.check_str, description=rule.description)
for token in re.split(r'\s+', check_str):
if token.isspace():
continue

# Handle leading parentheses.
clean = token.lstrip('(')
for _ in range(len(token) - len(clean)):
out.append('(')

def inherited(rule):
"""Is the rule inherited by one that we have defined?"""
# Handle trailing parentheses.
token = clean

return any(rule.name == my_rule.name for my_rule in rules)
clean = token.rstrip(')')
trail = len(token) - len(clean)

# If the token is a rule, then expand it.
matches = re.match(r'rule:([\w_]+)', clean)
if matches:
rule = _find_rule(matches.group(1), rule_list)
sub_check_str = _recurse_build_check_str(rule.check_str, rule_list)
out.extend(_wrap_check_str(sub_check_str))
else:
out.append(clean)

for _ in range(trail):
out.append(')')

return out


def _build_check_str(check_str, rule_list):
"""
Given a check string, this does macro expansion of rule:roo strings
removing and inlining them.
"""

check_str = ' '.join(_recurse_build_check_str(check_str, rule_list))
check_str = re.sub(r'\( ', '(', check_str)
check_str = re.sub(r' \)', ')', check_str)
return check_str


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

# Okay now for the "hard" bit. We reference built in rules directly from neutron so
# we can augment the exact rules for a specific version, thus we pick up any changes.
# We prefix the existing rules with "base_" as already seen above but only if they
# are redefined (and by implication referenced) from one of ours.
network_rules = [basify(rule) for rule in network.list_rules() if inherited(rule)]
# For every defined rule, look for a corresponding one sourced directly
# from neutron, this means we can augment the exact rule defined for a
# specific version of neutron,
network_rules = list(policies.list_rules())

inherited_network_rules = []

for rule in rules:
try:
network_rule = _find_rule(rule.name, network_rules)

check_str = _build_check_str(network_rule.check_str, network_rules)

inherited_network_rules.append(policy.RuleDefault(
name='base_' + rule.name,
check_str=check_str,
description=rule.description,
))
except MissingRuleException:
pass

return inherited_network_rules + rules

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

# vi: ts=4 et:
Loading

0 comments on commit c474f61

Please sign in to comment.