diff --git a/examples/policy_definition.md b/examples/policy_definition.md new file mode 100644 index 00000000..577fe5a8 --- /dev/null +++ b/examples/policy_definition.md @@ -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: + : + : + + access: + - +``` diff --git a/examples/sample_policy.yaml b/examples/sample_policy.yaml index 8c69bfaf..32c2f1b3 100644 --- a/examples/sample_policy.yaml +++ b/examples/sample_policy.yaml @@ -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 diff --git a/setup.py b/setup.py index a8b46799..18620b18 100644 --- a/setup.py +++ b/setup.py @@ -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', ], } ) diff --git a/sros2/api/__init__.py b/sros2/api/__init__.py index fdf7c684..189ecda3 100644 --- a/sros2/api/__init__.py +++ b/sros2/api/__init__.py @@ -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': @@ -299,6 +341,28 @@ def create_cert(root_path, name): (openssl_executable, req_relpath, cert_relpath), root_path) +def format_publish_permissions(topics): + return """ + + + + + %s + + """ % topics + + +def format_subscription_permissions(topics): + return """ + + + + + %s + + """ % topics + + def create_permission_file(path, name, domain_id, permissions_dict): permission_str = """\ @@ -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 += """\ @@ -352,12 +420,9 @@ def create_permission_file(path, name, domain_id, permissions_dict): %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', @@ -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 += """ - %s""" % (service_topic) - permission_str += """ - - - - - %s - - - - - - - %s - - -""" % (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 += """ + %s""" % (request_topic) + published_topics_string += """ + %s""" % (reply_topic) + if 'request' in permissions: + subscribed_topic_string += """ + %s""" % (reply_topic) + published_topics_string += """ + %s""" % (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 += """\ diff --git a/sros2/verb/generate_permissions.py b/sros2/verb/generate_permissions.py new file mode 100644 index 00000000..457b7ca5 --- /dev/null +++ b/sros2/verb/generate_permissions.py @@ -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')