Skip to content

Commit

Permalink
Add generate_permissions verb + update policy definition to support s…
Browse files Browse the repository at this point in the history
…ervices and actions (#71)

* Add generate permissions security command line

Generate an sros2 yaml permissions file with the permissions of every visible node
on the dds network.

Add custom service security to sros2

Example: run the minimal_publisher_lambda node
Execute: `ros2 security generate_permissions node_policies.yaml`

It will create the following file in the current directory:
```
/minimal_publisher:
  services:
    /minimal_publisher/describe_parameters:
      allows:
      - request
      - reply
      .
      .
      .
  topics:
    /parameter_events:
      allows:
      - publish
      - subscribe
    /topic:
      allows:
      - publish
```

cr https://code.amazon.com/reviews/CR-3943967

* Proposed policy definition changes

Issue: services and actions are not considered in the policy yaml
definition.
Solution: Add ipc types (services and actions)

Issue: access values are strings with p, s, or ps in order. This is not
descriptive and difficult to scale should more permissions become necessary.
Solution: Either change the parsing of the string or change the yaml to
be more flexible and descriptive for users.

The proposed changes include:
* Access value is a list, not a string
* Add ipc types such as actions and services
* Access values are no longer shorthand p or s, but publish/subscribe

Amend policy definition with verbose ROS ipc types
  • Loading branch information
jacobperron authored Jan 11, 2019
2 parents a866f70 + 7a67e2f commit 9efd4e0
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 55 deletions.
32 changes: 32 additions & 0 deletions examples/policy_definition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Values used in policy generation
**ipc_types**: The type of ROS IPC in use, such as service, action, or topic
## Definitions

| name | description |
| ---- | ----------- |
| access permission | The access permission of that node for the specified icp |
| ipc | Inter-process communication, how messages get from one node to another |
| ipc identifier | The specific subsystem id to provide access to (topic name, service name...) |
| ipc types | The inter-process communication subsystem (topics, services...) |

## Options
Most ipc permissions are given on a client/source basis.
Parameter permissions are slightly different. These specify whether this node is allowed to read/write to another node.

| ipc type | identifier | access permission options |
| -------- | ---------- | ------------------------- |
| topics | topic name | subscribe, publish |
| services | service name | request, reply |
| actions | action name | call, execute |
| parameters | node name | read, write |

## Policy yaml file layout

```
nodes:
<node_name>:
<ipc_type>:
<ipc_identifier>
access:
-<access_permissions>
```
41 changes: 25 additions & 16 deletions examples/sample_policy.yaml
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
nodes:
listener:
topics:
chatter:
allow: s # can subscribe to chatter
/chatter:
allow:
- subscribe # can subscribe to chatter
talker:
topics:
chatter:
allow: p # can publish on chatter
/chatter:
allow:
- publish # can publish on chatter
listener_py:
topics:
#'*':
# allow: s # this would allow the listener to subscribe to all topics
chatter:
allow: s # can subscribe to chatter
chatter2:
allow: s # can subscribe to chatter2
# allow:
# - subscribe # this would allow the listener to subscribe to all topics
/chatter:
allow:
- subscribe # can subscribe to chatter
/chatter2:
allow:
- subscribe# can subscribe to chatter2
talker_py:
topics:
#'*':
# allow: p # this would allow the talker to publish on all topics
chatter:
allow: p # allow publishing on chatter
chatter2:
allow: p # allow publishing on chatter2
chatter3:
allow: p # allow publishing on chatter3
# allow:
# - publish # this would allow the talker to publish on all topics
/chatter:
allow:
-publish # allow publishing on chatter
/chatter2:
allow:
- publish# allow publishing on chatter2
/chatter3:
allow:
-publish # allow publishing on chatter3
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
':CreatePermissionVerb',
'distribute_key = sros2.verb.distribute_key:DistributeKeyVerb',
'list_keys = sros2.verb.list_keys:ListKeysVerb',
'generate_permissions = sros2.verb.generate_permissions:GeneratePermissionsVerb',
],
}
)
144 changes: 105 additions & 39 deletions sros2/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,54 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import namedtuple
import itertools
import os
import platform
import shutil
import subprocess

HIDDEN_NODE_PREFIX = '_'

NodeName = namedtuple('NodeName', ('name', 'namespace', 'full_name'))
TopicInfo = namedtuple('Topic', ('name', 'types'))


def get_node_names(*, node, include_hidden_nodes=False):
node_names_and_namespaces = node.get_node_names_and_namespaces()
return [
NodeName(
name=t[0],
namespace=t[1],
full_name=t[1] + ('' if t[1].endswith('/') else '/') + t[0])
for t in node_names_and_namespaces
if (
include_hidden_nodes or
(t[0] and not t[0].startswith(HIDDEN_NODE_PREFIX))
)
]


def get_topics(node_name, func):
names_and_types = func(node_name.name, node_name.namespace)
return [
TopicInfo(
name=t[0],
types=t[1])
for t in names_and_types]


def get_subscriber_info(node, node_name):
return get_topics(node_name, node.get_subscriber_names_and_types_by_node)


def get_publisher_info(node, node_name):
return get_topics(node_name, node.get_publisher_names_and_types_by_node)


def get_service_info(node, node_name):
return get_topics(node_name, node.get_service_names_and_types_by_node)


def find_openssl_executable():
if platform.system() != 'Darwin':
Expand Down Expand Up @@ -299,6 +341,28 @@ def create_cert(root_path, name):
(openssl_executable, req_relpath, cert_relpath), root_path)


def format_publish_permissions(topics):
return """
<publish>
<partitions>
<partition></partition>
</partitions>
<topics>%s
</topics>
</publish> """ % topics


def format_subscription_permissions(topics):
return """
<subscribe>
<partitions>
<partition></partition>
</partitions>
<topics>%s
</topics>
</subscribe> """ % topics


def create_permission_file(path, name, domain_id, permissions_dict):
permission_str = """\
<?xml version="1.0" encoding="UTF-8"?>
Expand Down Expand Up @@ -328,19 +392,23 @@ def create_permission_file(path, name, domain_id, permissions_dict):
# TODO(mikaelarguedas) remove this hardcoded handling for default topics
# TODO(mikaelarguedas) update dictionary based on existing rule
# if it already exists (rather than overriding the rule)
topic_dict['parameter_events'] = {'allow': 'ps'}
topic_dict['clock'] = {'allow': 's'}
topic_dict['/parameter_events'] = {'allow': ['publish', 'subscribe']}
topic_dict['/clock'] = {'allow': ['subscribe']}
# we have some policies to add !
for topic_name, policy in topic_dict.items():
# add a / if it doesn't exist
formatted_topic_name = '/' + (topic_name.lstrip('/'))
tags = []
if policy['allow'] == 'ps':
tags = ['publish', 'subscribe']
elif policy['allow'] == 's':
tags = ['subscribe']
elif policy['allow'] == 'p':
tags = ['publish']
else:
print("unknown permission policy '%s', skipping" % policy['allow'])
publish = 'publish'
subscribe = 'subscribe'
if publish in policy['allow']:
tags.append(publish)
policy['allow'].remove(publish)
if subscribe in policy['allow']:
tags.append(subscribe)
policy['allow'].remove(subscribe)
if len(policy['allow']) > 0:
print('unknown permission policy \'%s\', skipping' % policy['allow'])
continue
for tag in tags:
permission_str += """\
Expand All @@ -352,12 +420,9 @@ def create_permission_file(path, name, domain_id, permissions_dict):
<topic>%s</topic>
</topics>
</%s>
""" % (tag, 'rt/' + topic_name, tag)
""" % (tag, 'rt' + formatted_topic_name, tag)
# TODO(mikaelarguedas) remove this hardcoded handling for default parameter topics
service_topic_prefixes = {
'Request': 'rq/%s/' % name,
'Reply': 'rr/%s/' % name,
}
service_dict = permissions_dict['services']
default_parameter_topics = [
'describe_parameters',
'get_parameters',
Expand All @@ -366,30 +431,31 @@ def create_permission_file(path, name, domain_id, permissions_dict):
'set_parameters',
'set_parameters_atomically',
]
for topic_suffix, topic_prefix in service_topic_prefixes.items():
service_topics = [
(topic_prefix + topic + topic_suffix) for topic in default_parameter_topics]
topics_string = ''
for service_topic in service_topics:
topics_string += """
<topic>%s</topic>""" % (service_topic)
permission_str += """
<publish>
<partitions>
<partition></partition>
</partitions>
<topics>%s
</topics>
</publish>
<subscribe>
<partitions>
<partition></partition>
</partitions>
<topics>%s
</topics>
</subscribe>
""" % (topics_string, topics_string)

default_parameter_topics = ['/%s/%s' % (name, topic) for topic in default_parameter_topics]
rw_access_pair = {'allow': ['request', 'reply']}
for topic in default_parameter_topics:
service_dict[topic] = rw_access_pair
published_topics_string = ''
subscribed_topic_string = ''

for topic, permission_dict in service_dict.items():
permissions = permission_dict['allow']
request_topic = 'rq%sRequest' % topic
reply_topic = 'rr%sReply' % topic

if 'reply' in permissions:
subscribed_topic_string += """
<topic>%s</topic>""" % (request_topic)
published_topics_string += """
<topic>%s</topic>""" % (reply_topic)
if 'request' in permissions:
subscribed_topic_string += """
<topic>%s</topic>""" % (reply_topic)
published_topics_string += """
<topic>%s</topic>""" % (request_topic)

permission_str += format_publish_permissions(published_topics_string)
permission_str += format_subscription_permissions(subscribed_topic_string)
else:
# no policy found: allow everything!
permission_str += """\
Expand Down
82 changes: 82 additions & 0 deletions sros2/verb/generate_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright 2016-2017 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

try:
from argcomplete.completers import DirectoriesCompleter
except ImportError:
def DirectoriesCompleter():
return None
try:
from argcomplete.completers import FilesCompleter
except ImportError:
def FilesCompleter(*, allowednames, directories):
return None

from collections import defaultdict

from ros2cli.node.direct import DirectNode
from ros2cli.node.strategy import NodeStrategy
from sros2.api import get_node_names
from sros2.api import get_publisher_info
from sros2.api import get_service_info
from sros2.api import get_subscriber_info
from sros2.verb import VerbExtension


def formatTopics(topic_list, permission, topic_map):
for topic in topic_list:
topic_map[topic.name].append(permission)


class GeneratePermissionsVerb(VerbExtension):
"""Generate permissions."""

def add_arguments(self, parser, cli_name):

arg = parser.add_argument(
'POLICY_FILE_PATH', help='path of the permission yaml file')
arg.completer = FilesCompleter(
allowednames=('yaml'), directories=False)

def main(self, *, args):
node_names = []
with NodeStrategy(args) as node:
node_names = get_node_names(node=node, include_hidden_nodes=False)
policy_dict = {}
with DirectNode(args) as node:
for node_name in node_names:
subscribers = get_subscriber_info(node=node, node_name=node_name)
publishers = get_publisher_info(node=node, node_name=node_name)
services = get_service_info(node=node, node_name=node_name)
topic_map = defaultdict(list)
formatTopics(publishers, 'publish', topic_map)
formatTopics(subscribers, 'subscribe', topic_map)
formatted_topic_map = {}
for topic_name, permission_list in topic_map.items():
formatted_topic_map[topic_name] = {'allow': permission_list}
service_map = defaultdict(list)
formatTopics(services, 'reply', service_map)
formatted_services_map = {}
for service, permission_list in service_map.items():
formatted_services_map[service] = {'allow': permission_list}
policy_dict[node_name.name] = {'topics': formatted_topic_map}
policy_dict[node_name.name]['services'] = formatted_services_map
import yaml
from io import open
formatted_policy_dict = {'nodes': policy_dict}
if policy_dict:
with open(args.POLICY_FILE_PATH, 'w') as stream:
yaml.dump(formatted_policy_dict, stream, default_flow_style=False)
else:
print('No nodes found to generate policies')

0 comments on commit 9efd4e0

Please sign in to comment.