Expose Auth Context in Python

pull/10993/head
Ken Payson 8 years ago
parent 449bf017e1
commit 3ca8134514
  1. 36
      src/python/grpcio/grpc/__init__.py
  2. 29
      src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi
  3. 58
      src/python/grpcio/grpc/_cython/_cygrpc/security.pyx.pxi
  4. 15
      src/python/grpcio/grpc/_server.py
  5. 1
      src/python/grpcio_tests/tests/tests.json
  6. 154
      src/python/grpcio_tests/tests/unit/_auth_context_test.py

@ -776,6 +776,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.

@ -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":

@ -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

@ -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:

@ -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",

@ -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)
Loading…
Cancel
Save