Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ros2 service crystal #71

Merged
merged 7 commits into from
Jan 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
ross-desmond marked this conversation as resolved.
Show resolved Hide resolved
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':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change necessary? I think it would be better not include it to help keep the PR succinct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would help keep the PR succinct, but then there would be different ways of describing the security for services(-request, -response) vs topics (ps). It makes the parsing and description of the elements more verbose and robust.

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')