Python GA security API

pull/6762/head
Nathaniel Manista 9 years ago
parent 10da197c1e
commit 29243ddb18
  1. 252
      src/python/grpcio/grpc/__init__.py
  2. 123
      src/python/grpcio/grpc/_plugin_wrapping.py

@ -352,6 +352,85 @@ class Call(six.with_metaclass(abc.ABCMeta, RpcContext)):
raise NotImplementedError()
############ Authentication & Authorization Interfaces & Classes #############
class ChannelCredentials(object):
"""A value encapsulating the data required to create a secure Channel.
This class has no supported interface - it exists to define the type of its
instances and its instances exist to be passed to other functions.
"""
def __init__(self, credentials):
self._credentials = credentials
class CallCredentials(object):
"""A value encapsulating data asserting an identity over a channel.
A CallCredentials may be composed with ChannelCredentials to always assert
identity for every call over that Channel.
This class has no supported interface - it exists to define the type of its
instances and its instances exist to be passed to other functions.
"""
def __init__(self, credentials):
self._credentials = credentials
class AuthMetadataContext(six.with_metaclass(abc.ABCMeta)):
"""Provides information to call credentials metadata plugins.
Attributes:
service_url: A string URL of the service being called into.
method_name: A string of the fully qualified method name being called.
"""
class AuthMetadataPluginCallback(six.with_metaclass(abc.ABCMeta)):
"""Callback object received by a metadata plugin."""
def __call__(self, metadata, error):
"""Inform the gRPC runtime of the metadata to construct a CallCredentials.
Args:
metadata: An iterable of 2-sequences (e.g. tuples) of metadata key/value
pairs.
error: An Exception to indicate error or None to indicate success.
"""
raise NotImplementedError()
class AuthMetadataPlugin(six.with_metaclass(abc.ABCMeta)):
"""A specification for custom authentication."""
def __call__(self, context, callback):
"""Implements authentication by passing metadata to a callback.
Implementations of this method must not block.
Args:
context: An AuthMetadataContext providing information on the RPC that the
plugin is being called to authenticate.
callback: An AuthMetadataPluginCallback to be invoked either synchronously
or asynchronously.
"""
raise NotImplementedError()
class ServerCredentials(object):
"""A value encapsulating the data required to open a secure port on a Server.
This class has no supported interface - it exists to define the type of its
instances and its instances exist to be passed to other functions.
"""
def __init__(self, credentials):
self._credentials = credentials
######################## Multi-Callable Interfaces ###########################
@ -359,7 +438,9 @@ class UnaryUnaryMultiCallable(six.with_metaclass(abc.ABCMeta)):
"""Affords invoking a unary-unary RPC."""
@abc.abstractmethod
def __call__(self, request, timeout=None, metadata=None, with_call=False):
def __call__(
self, request, timeout=None, metadata=None, credentials=None,
with_call=False):
"""Synchronously invokes the underlying RPC.
Args:
@ -367,6 +448,7 @@ class UnaryUnaryMultiCallable(six.with_metaclass(abc.ABCMeta)):
timeout: An optional duration of time in seconds to allow for the RPC.
metadata: An optional sequence of pairs of bytes to be transmitted to the
service-side of the RPC.
credentials: An optional CallCredentials for the RPC.
with_call: Whether or not to include return a Call for the RPC in addition
to the response.
@ -382,7 +464,7 @@ class UnaryUnaryMultiCallable(six.with_metaclass(abc.ABCMeta)):
raise NotImplementedError()
@abc.abstractmethod
def future(self, request, timeout=None, metadata=None):
def future(self, request, timeout=None, metadata=None, credentials=None):
"""Asynchronously invokes the underlying RPC.
Args:
@ -390,6 +472,7 @@ class UnaryUnaryMultiCallable(six.with_metaclass(abc.ABCMeta)):
timeout: An optional duration of time in seconds to allow for the RPC.
metadata: An optional sequence of pairs of bytes to be transmitted to the
service-side of the RPC.
credentials: An optional CallCredentials for the RPC.
Returns:
An object that is both a Call for the RPC and a Future. In the event of
@ -404,7 +487,7 @@ class UnaryStreamMultiCallable(six.with_metaclass(abc.ABCMeta)):
"""Affords invoking a unary-stream RPC."""
@abc.abstractmethod
def __call__(self, request, timeout=None, metadata=None):
def __call__(self, request, timeout=None, metadata=None, credentials=None):
"""Invokes the underlying RPC.
Args:
@ -412,6 +495,7 @@ class UnaryStreamMultiCallable(six.with_metaclass(abc.ABCMeta)):
timeout: An optional duration of time in seconds to allow for the RPC.
metadata: An optional sequence of pairs of bytes to be transmitted to the
service-side of the RPC.
credentials: An optional CallCredentials for the RPC.
Returns:
An object that is both a Call for the RPC and an iterator of response
@ -426,7 +510,8 @@ class StreamUnaryMultiCallable(six.with_metaclass(abc.ABCMeta)):
@abc.abstractmethod
def __call__(
self, request_iterator, timeout=None, metadata=None, with_call=False):
self, request_iterator, timeout=None, metadata=None, credentials=None,
with_call=False):
"""Synchronously invokes the underlying RPC.
Args:
@ -434,6 +519,7 @@ class StreamUnaryMultiCallable(six.with_metaclass(abc.ABCMeta)):
timeout: An optional duration of time in seconds to allow for the RPC.
metadata: An optional sequence of pairs of bytes to be transmitted to the
service-side of the RPC.
credentials: An optional CallCredentials for the RPC.
with_call: Whether or not to include return a Call for the RPC in addition
to the response.
@ -449,7 +535,8 @@ class StreamUnaryMultiCallable(six.with_metaclass(abc.ABCMeta)):
raise NotImplementedError()
@abc.abstractmethod
def future(self, request_iterator, timeout=None, metadata=None):
def future(
self, request_iterator, timeout=None, metadata=None, credentials=None):
"""Asynchronously invokes the underlying RPC.
Args:
@ -457,6 +544,7 @@ class StreamUnaryMultiCallable(six.with_metaclass(abc.ABCMeta)):
timeout: An optional duration of time in seconds to allow for the RPC.
metadata: An optional sequence of pairs of bytes to be transmitted to the
service-side of the RPC.
credentials: An optional CallCredentials for the RPC.
Returns:
An object that is both a Call for the RPC and a Future. In the event of
@ -471,7 +559,8 @@ class StreamStreamMultiCallable(six.with_metaclass(abc.ABCMeta)):
"""Affords invoking a stream-stream RPC in any call style."""
@abc.abstractmethod
def __call__(self, request_iterator, timeout=None, metadata=None):
def __call__(
self, request_iterator, timeout=None, metadata=None, credentials=None):
"""Invokes the underlying RPC.
Args:
@ -479,6 +568,7 @@ class StreamStreamMultiCallable(six.with_metaclass(abc.ABCMeta)):
timeout: An optional duration of time in seconds to allow for the RPC.
metadata: An optional sequence of pairs of bytes to be transmitted to the
service-side of the RPC.
credentials: An optional CallCredentials for the RPC.
Returns:
An object that is both a Call for the RPC and an iterator of response
@ -690,7 +780,6 @@ class RpcMethodHandler(six.with_metaclass(abc.ABCMeta)):
class HandlerCallDetails(six.with_metaclass(abc.ABCMeta)):
"""Describes an RPC that has just arrived for service.
Attributes:
method: The method name of the RPC.
invocation_metadata: The metadata from the invocation side of the RPC.
@ -750,6 +839,25 @@ class Server(six.with_metaclass(abc.ABCMeta)):
"""
raise NotImplementedError()
@abc.abstractmethod
def add_secure_port(self, address, server_credentials):
"""Reserves a port for secure RPC service after this Server becomes active.
This method may only be called before calling this Server's start method is
called.
Args:
address: The address for which to open a port.
server_credentials: A ServerCredentials.
Returns:
An integer port on which RPCs will be serviced after this link has been
started. This is typically the same number as the port number contained
in the passed address, but will likely be different if the port number
contained in the passed address was zero.
"""
raise NotImplementedError()
@abc.abstractmethod
def start(self):
"""Starts this Server's service of RPCs.
@ -792,6 +900,120 @@ class Server(six.with_metaclass(abc.ABCMeta)):
################################# Functions ################################
def ssl_channel_credentials(
root_certificates=None, private_key=None, certificate_chain=None):
"""Creates a ChannelCredentials for use with an SSL-enabled Channel.
Args:
root_certificates: The PEM-encoded root certificates or unset to ask for
them to be retrieved from a default location.
private_key: The PEM-encoded private key to use or unset if no private key
should be used.
certificate_chain: The PEM-encoded certificate chain to use or unset if no
certificate chain should be used.
Returns:
A ChannelCredentials for use with an SSL-enabled Channel.
"""
if private_key is not None or certificate_chain is not None:
pair = _cygrpc.SslPemKeyCertPair(private_key, certificate_chain)
else:
pair = None
return ChannelCredentials(
_cygrpc.channel_credentials_ssl(root_certificates, pair))
def metadata_call_credentials(metadata_plugin, name=None):
"""Construct CallCredentials from an AuthMetadataPlugin.
Args:
metadata_plugin: An AuthMetadataPlugin to use as the authentication behavior
in the created CallCredentials.
name: A name for the plugin.
Returns:
A CallCredentials.
"""
from grpc import _plugin_wrapping
if name is None:
try:
effective_name = metadata_plugin.__name__
except AttributeError:
effective_name = metadata_plugin.__class__.__name__
else:
effective_name = name
return CallCredentials(
_plugin_wrapping.call_credentials_metadata_plugin(
metadata_plugin, effective_name))
def composite_call_credentials(call_credentials, additional_call_credentials):
"""Compose two CallCredentials to make a new one.
Args:
call_credentials: A CallCredentials object.
additional_call_credentials: Another CallCredentials object to compose on
top of call_credentials.
Returns:
A new CallCredentials composed of the two given CallCredentials.
"""
return CallCredentials(
_cygrpc.call_credentials_composite(
call_credentials._credentials,
additional_call_credentials._credentials))
def composite_channel_credentials(channel_credentials, call_credentials):
"""Compose a ChannelCredentials and a CallCredentials.
Args:
channel_credentials: A ChannelCredentials.
call_credentials: A CallCredentials.
Returns:
A ChannelCredentials composed of the given ChannelCredentials and
CallCredentials.
"""
return ChannelCredentials(
_cygrpc.channel_credentials_composite(
channel_credentials._credentials, call_credentials._credentials))
def ssl_server_credentials(
private_key_certificate_chain_pairs, root_certificates=None,
require_client_auth=False):
"""Creates a ServerCredentials for use with an SSL-enabled Server.
Args:
private_key_certificate_chain_pairs: A nonempty sequence each element of
which is a pair the first element of which is a PEM-encoded private key
and the second element of which is the corresponding PEM-encoded
certificate chain.
root_certificates: PEM-encoded client root certificates to be used for
verifying authenticated clients. If omitted, require_client_auth must also
be omitted or be False.
require_client_auth: A boolean indicating whether or not to require clients
to be authenticated. May only be True if root_certificates is not None.
Returns:
A ServerCredentials for use with an SSL-enabled Server.
"""
if len(private_key_certificate_chain_pairs) == 0:
raise ValueError(
'At least one private key-certificate chain pair is required!')
elif require_client_auth and root_certificates is None:
raise ValueError(
'Illegal to require client auth without providing root certificates!')
else:
return ServerCredentials(
_cygrpc.server_credentials_ssl(
root_certificates,
[_cygrpc.SslPemKeyCertPair(key, pem)
for key, pem in private_key_certificate_chain_pairs],
require_client_auth))
def channel_ready_future(channel):
"""Creates a Future tracking when a Channel is ready.
@ -825,6 +1047,22 @@ def insecure_channel(target, options=None):
return _channel.Channel(target, None, options)
def secure_channel(target, credentials, options=None):
"""Creates an insecure Channel to a server.
Args:
target: The target to which to connect.
credentials: A ChannelCredentials instance.
options: A sequence of string-value pairs according to which to configure
the created channel.
Returns:
A Channel to the target through which RPCs may be conducted.
"""
from grpc import _channel
return _channel.Channel(target, credentials, options)
def server(generic_rpc_handlers, thread_pool, options=None):
"""Creates a Server with which RPCs can be serviced.

@ -0,0 +1,123 @@
# Copyright 2015, 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.
import collections
import threading
import grpc
from grpc._cython import cygrpc
class AuthMetadataContext(
collections.namedtuple(
'AuthMetadataContext', ('service_url', 'method_name',)),
grpc.AuthMetadataContext):
pass
class AuthMetadataPluginCallback(grpc.AuthMetadataContext):
def __init__(self, callback):
self._callback = callback
def __call__(self, metadata, error):
self._callback(metadata, error)
class _WrappedCygrpcCallback(object):
def __init__(self, cygrpc_callback):
self.is_called = False
self.error = None
self.is_called_lock = threading.Lock()
self.cygrpc_callback = cygrpc_callback
def _invoke_failure(self, error):
# TODO(atash) translate different Exception superclasses into different
# status codes.
self.cygrpc_callback(
cygrpc.Metadata([]), cygrpc.StatusCode.internal, error.message)
def _invoke_success(self, metadata):
try:
cygrpc_metadata = cygrpc.Metadata(
cygrpc.Metadatum(key, value)
for key, value in metadata)
except Exception as error:
self._invoke_failure(error)
return
self.cygrpc_callback(cygrpc_metadata, cygrpc.StatusCode.ok, '')
def __call__(self, metadata, error):
with self.is_called_lock:
if self.is_called:
raise RuntimeError('callback should only ever be invoked once')
if self.error:
self._invoke_failure(self.error)
return
self.is_called = True
if error is None:
self._invoke_success(metadata)
else:
self._invoke_failure(error)
def notify_failure(self, error):
with self.is_called_lock:
if not self.is_called:
self.error = error
class _WrappedPlugin(object):
def __init__(self, plugin):
self.plugin = plugin
def __call__(self, context, cygrpc_callback):
wrapped_cygrpc_callback = _WrappedCygrpcCallback(cygrpc_callback)
wrapped_context = AuthMetadataContext(
context.service_url, context.method_name)
try:
self.plugin(
wrapped_context, AuthMetadataPluginCallback(wrapped_cygrpc_callback))
except Exception as error:
wrapped_cygrpc_callback.notify_failure(error)
raise
def call_credentials_metadata_plugin(plugin, name):
"""
Args:
plugin: A callable accepting a grpc.AuthMetadataContext
object and a callback (itself accepting a list of metadata key/value
2-tuples and a None-able exception value). The callback must be eventually
called, but need not be called in plugin's invocation.
plugin's invocation must be non-blocking.
"""
return cygrpc.call_credentials_metadata_plugin(
cygrpc.CredentialsMetadataPlugin(_WrappedPlugin(plugin), name))
Loading…
Cancel
Save