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 1 commit
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
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',
],
}
)
136 changes: 98 additions & 38 deletions sros2/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,45 @@
import platform
import shutil
import subprocess

from collections import namedtuple

from ros2cli.node.strategy import NodeStrategy

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 @@ -298,6 +336,25 @@ def create_cert(root_path, name):
'%s ca -batch -create_serial -config ca_conf.cnf -days 3650 -in %s -out %s' %
(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 = """\
Expand Down Expand Up @@ -328,18 +385,20 @@ 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():
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:
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:
Expand All @@ -352,12 +411,10 @@ def create_permission_file(path, name, domain_id, permissions_dict):
<topic>%s</topic>
</topics>
</%s>
""" % (tag, 'rt/' + topic_name, tag)
""" % (tag, 'rt' + topic_name, tag)
ross-desmond marked this conversation as resolved.
Show resolved Hide resolved
# TODO(mikaelarguedas) remove this hardcoded handling for default parameter topics
service_topic_prefixes = {
'Request': 'rq/%s/' % name,
'Reply': 'rr/%s/' % name,
}

ross-desmond marked this conversation as resolved.
Show resolved Hide resolved
service_dict = permissions_dict['services']
default_parameter_topics = [
'describe_parameters',
'get_parameters',
Expand All @@ -366,29 +423,32 @@ 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!
Expand Down
79 changes: 79 additions & 0 deletions sros2/verb/generate_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 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 ros2cli.node.strategy import NodeStrategy
from ros2cli.node.direct import DirectNode
from sros2.api import get_subscriber_info
from sros2.api import get_publisher_info
from sros2.api import get_service_info
from sros2.api import get_node_names
from sros2.verb import VerbExtension
from collections import defaultdict

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