diff options
author | Ken Payson <kpayson@google.com> | 2017-05-10 12:35:23 -0700 |
---|---|---|
committer | Ken Payson <kpayson@google.com> | 2017-05-11 11:01:57 -0700 |
commit | 3ca8134514536be3014d692388d6e329025cd25e (patch) | |
tree | a43ef7e3ce32655fb6070eb38861738caf7c1846 | |
parent | 449bf017e1d19571b5349911b85802f3039dd7a5 (diff) |
Expose Auth Context in Python
-rw-r--r-- | src/python/grpcio/grpc/__init__.py | 36 | ||||
-rw-r--r-- | src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi | 29 | ||||
-rw-r--r-- | src/python/grpcio/grpc/_cython/_cygrpc/security.pyx.pxi | 58 | ||||
-rw-r--r-- | src/python/grpcio/grpc/_server.py | 15 | ||||
-rw-r--r-- | src/python/grpcio_tests/tests/tests.json | 1 | ||||
-rw-r--r-- | src/python/grpcio_tests/tests/unit/_auth_context_test.py | 154 |
6 files changed, 293 insertions, 0 deletions
diff --git a/src/python/grpcio/grpc/__init__.py b/src/python/grpcio/grpc/__init__.py index 2952dcd36a..5426b47c76 100644 --- a/src/python/grpcio/grpc/__init__.py +++ b/src/python/grpcio/grpc/__init__.py @@ -777,6 +777,42 @@ class ServicerContext(six.with_metaclass(abc.ABCMeta, RpcContext)): raise NotImplementedError() @abc.abstractmethod + def peer_identities(self): + """Gets one or more peer identity(s). + + Equivalent to + servicer_context.auth_context().get( + servicer_context.peer_identity_key()) + + Returns: + An iterable of the identities, or None if the call is not authenticated. + Each identity is returned as a raw bytes type. + """ + raise NotImplementedError() + + @abc.abstractmethod + def peer_identity_key(self): + """The auth property used to identify the peer. + + For example, "x509_common_name" or "x509_subject_alternative_name" are + used to identify an SSL peer. + + Returns: + The auth property (string) that indicates the + peer identity, or None if the call is not authenticated. + """ + raise NotImplementedError() + + @abc.abstractmethod + def auth_context(self): + """Gets the auth context for the call. + + Returns: + A map of strings to an iterable of bytes for each auth property. + """ + raise NotImplementedError() + + @abc.abstractmethod def send_initial_metadata(self, initial_metadata): """Sends the initial metadata value to the client. diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi index 1db2056d47..e71d3e7dc1 100644 --- a/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi +++ b/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi @@ -486,6 +486,35 @@ cdef extern from "grpc/grpc_security.h": grpc_call_credentials *grpc_metadata_credentials_create_from_plugin( grpc_metadata_credentials_plugin plugin, void *reserved) nogil + ctypedef struct grpc_auth_property_iterator: + pass + + ctypedef struct grpc_auth_property: + char *name + char *value + size_t value_length + + grpc_auth_property *grpc_auth_property_iterator_next( + grpc_auth_property_iterator *it) + + grpc_auth_property_iterator grpc_auth_context_property_iterator( + const grpc_auth_context *ctx) + + grpc_auth_property_iterator grpc_auth_context_peer_identity( + const grpc_auth_context *ctx) + + char *grpc_auth_context_peer_identity_property_name( + const grpc_auth_context *ctx) + + grpc_auth_property_iterator grpc_auth_context_find_properties_by_name( + const grpc_auth_context *ctx, const char *name) + + grpc_auth_context_peer_is_authenticated( + const grpc_auth_context *ctx) + + grpc_auth_context *grpc_call_auth_context(grpc_call *call) + + void grpc_auth_context_release(grpc_auth_context *context) cdef extern from "grpc/compression.h": diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/security.pyx.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/security.pyx.pxi index 357b0330d5..a21eac7995 100644 --- a/src/python/grpcio/grpc/_cython/_cygrpc/security.pyx.pxi +++ b/src/python/grpcio/grpc/_cython/_cygrpc/security.pyx.pxi @@ -44,3 +44,61 @@ cdef grpc_ssl_roots_override_result ssl_roots_override_callback( pem_root_certs[0][len(temporary_pem_root_certs)] = '\0' return GRPC_SSL_ROOTS_OVERRIDE_OK + + +def peer_identities(Call call): + cdef grpc_auth_context* auth_context + cdef grpc_auth_property_iterator properties + cdef grpc_auth_property* property + + auth_context = grpc_call_auth_context(call.c_call) + if auth_context == NULL: + return None + properties = grpc_auth_context_peer_identity(auth_context) + identities = [] + while True: + property = grpc_auth_property_iterator_next(&properties) + if property == NULL: + break + if property.value != NULL: + identities.append(<bytes>(property.value)) + grpc_auth_context_release(auth_context) + return identities if identities else None + +def peer_identity_key(Call call): + cdef grpc_auth_context* auth_context + cdef char* c_key + auth_context = grpc_call_auth_context(call.c_call) + if auth_context == NULL: + return None + c_key = grpc_auth_context_peer_identity_property_name(auth_context) + if c_key == NULL: + key = None + else: + key = <bytes> grpc_auth_context_peer_identity_property_name(auth_context) + grpc_auth_context_release(auth_context) + return key + +def auth_context(Call call): + cdef grpc_auth_context* auth_context + cdef grpc_auth_property_iterator properties + cdef grpc_auth_property* property + + auth_context = grpc_call_auth_context(call.c_call) + if auth_context == NULL: + return {} + properties = grpc_auth_context_property_iterator(auth_context) + py_auth_context = {} + while True: + property = grpc_auth_property_iterator_next(&properties) + if property == NULL: + break + if property.name != NULL and property.value != NULL: + key = <bytes> property.name + if key in py_auth_context: + py_auth_context[key].append(<bytes>(property.value)) + else: + py_auth_context[key] = [<bytes> property.value] + grpc_auth_context_release(auth_context) + return py_auth_context + diff --git a/src/python/grpcio/grpc/_server.py b/src/python/grpcio/grpc/_server.py index f29c44a4cf..860085f0c7 100644 --- a/src/python/grpcio/grpc/_server.py +++ b/src/python/grpcio/grpc/_server.py @@ -31,6 +31,7 @@ import collections import enum import logging +import six import threading import time @@ -255,6 +256,20 @@ class _Context(grpc.ServicerContext): def peer(self): return _common.decode(self._rpc_event.operation_call.peer()) + def peer_identities(self): + return cygrpc.peer_identities(self._rpc_event.operation_call) + + def peer_identity_key(self): + id_key = cygrpc.peer_identity_key(self._rpc_event.operation_call) + return id_key if id_key is None else _common.decode(id_key) + + def auth_context(self): + return { + _common.decode(key): value + for key, value in six.iteritems( + cygrpc.auth_context(self._rpc_event.operation_call)) + } + def send_initial_metadata(self, initial_metadata): with self._state.condition: if self._state.client is _CANCELLED: diff --git a/src/python/grpcio_tests/tests/tests.json b/src/python/grpcio_tests/tests/tests.json index f750b05102..9f7587faa6 100644 --- a/src/python/grpcio_tests/tests/tests.json +++ b/src/python/grpcio_tests/tests/tests.json @@ -12,6 +12,7 @@ "unit._api_test.AllTest", "unit._api_test.ChannelConnectivityTest", "unit._api_test.ChannelTest", + "unit._auth_context_test.AuthContextTest", "unit._auth_test.AccessTokenCallCredentialsTest", "unit._auth_test.GoogleCallCredentialsTest", "unit._channel_args_test.ChannelArgsTest", diff --git a/src/python/grpcio_tests/tests/unit/_auth_context_test.py b/src/python/grpcio_tests/tests/unit/_auth_context_test.py new file mode 100644 index 0000000000..fce6ac967d --- /dev/null +++ b/src/python/grpcio_tests/tests/unit/_auth_context_test.py @@ -0,0 +1,154 @@ +# Copyright 2017, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Tests exposure of SSL auth context""" + +import pickle +import unittest + +import grpc +from grpc import _channel +from grpc.framework.foundation import logging_pool +import six + +from tests.unit import test_common +from tests.unit.framework.common import test_constants +from tests.unit import resources + +_REQUEST = b'\x00\x00\x00' +_RESPONSE = b'\x00\x00\x00' + +_UNARY_UNARY = '/test/UnaryUnary' + +_SERVER_HOST_OVERRIDE = 'foo.test.google.fr' +_CLIENT_IDS = (b'*.test.google.fr', b'waterzooi.test.google.be', + b'*.test.youtube.com', b'192.168.1.3',) +_ID = 'id' +_ID_KEY = 'id_key' +_AUTH_CTX = 'auth_ctx' + +_PRIVATE_KEY = resources.private_key() +_CERTIFICATE_CHAIN = resources.certificate_chain() +_TEST_ROOT_CERTIFICATES = resources.test_root_certificates() +_SERVER_CERTS = ((_PRIVATE_KEY, _CERTIFICATE_CHAIN),) +_PROPERTY_OPTIONS = (('grpc.ssl_target_name_override', _SERVER_HOST_OVERRIDE,),) + + +def handle_unary_unary(request, servicer_context): + return pickle.dumps({ + _ID: servicer_context.peer_identities(), + _ID_KEY: servicer_context.peer_identity_key(), + _AUTH_CTX: servicer_context.auth_context() + }) + + +class AuthContextTest(unittest.TestCase): + + def testInsecure(self): + server_pool = logging_pool.pool(test_constants.THREAD_CONCURRENCY) + handler = grpc.method_handlers_generic_handler('test', { + 'UnaryUnary': + grpc.unary_unary_rpc_method_handler(handle_unary_unary) + }) + server = grpc.server(server_pool, (handler,)) + port = server.add_insecure_port('[::]:0') + server.start() + + channel = grpc.insecure_channel('localhost:%d' % port) + response = channel.unary_unary(_UNARY_UNARY)(_REQUEST) + server.stop(None) + + auth_data = pickle.loads(response) + self.assertIsNone(auth_data[_ID]) + self.assertIsNone(auth_data[_ID_KEY]) + self.assertDictEqual({}, auth_data[_AUTH_CTX]) + + def testSecureNoCert(self): + server_pool = logging_pool.pool(test_constants.THREAD_CONCURRENCY) + handler = grpc.method_handlers_generic_handler('test', { + 'UnaryUnary': + grpc.unary_unary_rpc_method_handler(handle_unary_unary) + }) + server = grpc.server(server_pool, (handler,)) + server_cred = grpc.ssl_server_credentials(_SERVER_CERTS) + port = server.add_secure_port('[::]:0', server_cred) + server.start() + + channel_creds = grpc.ssl_channel_credentials( + root_certificates=_TEST_ROOT_CERTIFICATES) + channel = grpc.secure_channel( + 'localhost:{}'.format(port), + channel_creds, + options=_PROPERTY_OPTIONS) + response = channel.unary_unary(_UNARY_UNARY)(_REQUEST) + server.stop(None) + + auth_data = pickle.loads(response) + self.assertIsNone(auth_data[_ID]) + self.assertIsNone(auth_data[_ID_KEY]) + self.assertDictEqual({ + 'transport_security_type': [b'ssl'] + }, auth_data[_AUTH_CTX]) + + def testSecureClientCert(self): + server_pool = logging_pool.pool(test_constants.THREAD_CONCURRENCY) + handler = grpc.method_handlers_generic_handler('test', { + 'UnaryUnary': + grpc.unary_unary_rpc_method_handler(handle_unary_unary) + }) + server = grpc.server(server_pool, (handler,)) + server_cred = grpc.ssl_server_credentials( + _SERVER_CERTS, + root_certificates=_TEST_ROOT_CERTIFICATES, + require_client_auth=True) + port = server.add_secure_port('[::]:0', server_cred) + server.start() + + channel_creds = grpc.ssl_channel_credentials( + root_certificates=_TEST_ROOT_CERTIFICATES, + private_key=_PRIVATE_KEY, + certificate_chain=_CERTIFICATE_CHAIN) + channel = grpc.secure_channel( + 'localhost:{}'.format(port), + channel_creds, + options=_PROPERTY_OPTIONS) + + response = channel.unary_unary(_UNARY_UNARY)(_REQUEST) + server.stop(None) + + auth_data = pickle.loads(response) + auth_ctx = auth_data[_AUTH_CTX] + six.assertCountEqual(self, _CLIENT_IDS, auth_data[_ID]) + self.assertEqual('x509_subject_alternative_name', auth_data[_ID_KEY]) + self.assertSequenceEqual([b'ssl'], auth_ctx['transport_security_type']) + self.assertSequenceEqual([b'*.test.google.com'], + auth_ctx['x509_common_name']) + + +if __name__ == '__main__': + unittest.main(verbosity=2) |