Skip to content

Commit

Permalink
Expose Auth Context in Python
Browse files Browse the repository at this point in the history
  • Loading branch information
kpayson64 committed May 11, 2017
1 parent 449bf01 commit 3ca8134
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 0 deletions.
36 changes: 36 additions & 0 deletions src/python/grpcio/grpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,42 @@ def peer(self):
"""
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.
Expand Down
29 changes: 29 additions & 0 deletions src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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":

Expand Down
58 changes: 58 additions & 0 deletions src/python/grpcio/grpc/_cython/_cygrpc/security.pyx.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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

15 changes: 15 additions & 0 deletions src/python/grpcio/grpc/_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import collections
import enum
import logging
import six
import threading
import time

Expand Down Expand Up @@ -255,6 +256,20 @@ def invocation_metadata(self):
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:
Expand Down
1 change: 1 addition & 0 deletions src/python/grpcio_tests/tests/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
154 changes: 154 additions & 0 deletions src/python/grpcio_tests/tests/unit/_auth_context_test.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 3ca8134

Please sign in to comment.