[Python] Implement auth API for Flight

This commit is contained in:
David Li committed 2019-04-11 08:25:14 -04:00
1 parent 14774082f9
commit 48d42485fe
7 files changed
+641 -75

No files matched your search

+59 -11
View File
@@ -26,6 +26,49 @@ namespace arrow {
namespace py {
namespace flight {
PyServerAuthHandler::PyServerAuthHandler(PyObject* handler,
PyServerAuthHandlerVtable vtable)
: vtable_(vtable) {
Py_INCREF(handler);
handler_.reset(handler);
}
Status PyServerAuthHandler::Authenticate(
const arrow::flight::ServerAuthSender& outgoing,
const arrow::flight::ServerAuthReader& incoming) {
PyAcquireGIL lock;
vtable_.authenticate(handler_.obj(), outgoing, incoming);
return CheckPyError();
}
Status PyServerAuthHandler::IsValid(const std::string& token,
std::string* peer_identity) {
PyAcquireGIL lock;
vtable_.is_valid(handler_.obj(), token, peer_identity);
return CheckPyError();
}
PyClientAuthHandler::PyClientAuthHandler(PyObject* handler,
PyClientAuthHandlerVtable vtable)
: vtable_(vtable) {
Py_INCREF(handler);
handler_.reset(handler);
}
Status PyClientAuthHandler::Authenticate(
const arrow::flight::ClientAuthSender& outgoing,
const arrow::flight::ClientAuthReader& incoming) {
PyAcquireGIL lock;
vtable_.authenticate(handler_.obj(), outgoing, incoming);
return CheckPyError();
}
Status PyClientAuthHandler::GetToken(std::string* token) {
PyAcquireGIL lock;
vtable_.get_token(handler_.obj(), token);
return CheckPyError();
}
PyFlightServer::PyFlightServer(PyObject* server, PyFlightServerVtable vtable)
: vtable_(vtable) {
Py_INCREF(server);
@@ -37,40 +80,45 @@ Status PyFlightServer::ListFlights(
const arrow::flight::Criteria* criteria,
std::unique_ptr<arrow::flight::FlightListing>* listings) {
PyAcquireGIL lock;
vtable_.list_flights(server_.obj(), criteria, listings);
vtable_.list_flights(server_.obj(), context, criteria, listings);
return CheckPyError();
}
Status PyFlightServer::GetFlightInfo(const arrow::flight::FlightDescriptor& request,
Status PyFlightServer::GetFlightInfo(const arrow::flight::ServerCallContext& context,
const arrow::flight::FlightDescriptor& request,
std::unique_ptr<arrow::flight::FlightInfo>* info) {
PyAcquireGIL lock;
vtable_.get_flight_info(server_.obj(), request, info);
vtable_.get_flight_info(server_.obj(), context, request, info);
return CheckPyError();
}
Status PyFlightServer::DoGet(const arrow::flight::Ticket& request,
Status PyFlightServer::DoGet(const arrow::flight::ServerCallContext& context,
const arrow::flight::Ticket& request,
std::unique_ptr<arrow::flight::FlightDataStream>* stream) {
PyAcquireGIL lock;
vtable_.do_get(server_.obj(), request, stream);
vtable_.do_get(server_.obj(), context, request, stream);
return CheckPyError();
}
Status PyFlightServer::DoPut(std::unique_ptr<arrow::flight::FlightMessageReader> reader) {
Status PyFlightServer::DoPut(const arrow::flight::ServerCallContext& context,
std::unique_ptr<arrow::flight::FlightMessageReader> reader) {
PyAcquireGIL lock;
vtable_.do_put(server_.obj(), std::move(reader));
vtable_.do_put(server_.obj(), context, std::move(reader));
return CheckPyError();
}
Status PyFlightServer::DoAction(const arrow::flight::Action& action,
Status PyFlightServer::DoAction(const arrow::flight::ServerCallContext& context,
const arrow::flight::Action& action,
std::unique_ptr<arrow::flight::ResultStream>* result) {
PyAcquireGIL lock;
vtable_.do_action(server_.obj(), action, result);
vtable_.do_action(server_.obj(), context, action, result);
return CheckPyError();
}
Status PyFlightServer::ListActions(std::vector<arrow::flight::ActionType>* actions) {
Status PyFlightServer::ListActions(const arrow::flight::ServerCallContext& context,
std::vector<arrow::flight::ActionType>* actions) {
PyAcquireGIL lock;
vtable_.list_actions(server_.obj(), actions);
vtable_.list_actions(server_.obj(), context, actions);
return CheckPyError();
}
+66 -11
View File
@@ -19,6 +19,7 @@
#define PYARROW_FLIGHT_H
#include <memory>
#include <string>
#include <vector>
#include "arrow/flight/api.h"
@@ -35,21 +36,70 @@ namespace flight {
/// Python.
class ARROW_PYTHON_EXPORT PyFlightServerVtable {
public:
std::function<void(PyObject*, const arrow::flight::Criteria*,
std::function<void(PyObject*, const arrow::flight::ServerCallContext&,
const arrow::flight::Criteria*,
std::unique_ptr<arrow::flight::FlightListing>*)>
list_flights;
std::function<void(PyObject*, const arrow::flight::FlightDescriptor&,
std::function<void(PyObject*, const arrow::flight::ServerCallContext&,
const arrow::flight::FlightDescriptor&,
std::unique_ptr<arrow::flight::FlightInfo>*)>
get_flight_info;
std::function<void(PyObject*, const arrow::flight::Ticket&,
std::function<void(PyObject*, const arrow::flight::ServerCallContext&,
const arrow::flight::Ticket&,
std::unique_ptr<arrow::flight::FlightDataStream>*)>
do_get;
std::function<void(PyObject*, std::unique_ptr<arrow::flight::FlightMessageReader>)>
std::function<void(PyObject*, const arrow::flight::ServerCallContext&,
std::unique_ptr<arrow::flight::FlightMessageReader>)>
do_put;
std::function<void(PyObject*, const arrow::flight::Action&,
std::function<void(PyObject*, const arrow::flight::ServerCallContext&,
const arrow::flight::Action&,
std::unique_ptr<arrow::flight::ResultStream>*)>
do_action;
std::function<void(PyObject*, std::vector<arrow::flight::ActionType>*)> list_actions;
std::function<void(PyObject*, const arrow::flight::ServerCallContext&,
std::vector<arrow::flight::ActionType>*)>
list_actions;
};
class ARROW_PYTHON_EXPORT PyServerAuthHandlerVtable {
public:
std::function<void(PyObject*, const arrow::flight::ServerAuthSender&,
const arrow::flight::ServerAuthReader&)>
authenticate;
std::function<void(PyObject*, const std::string&, std::string*)> is_valid;
};
class ARROW_PYTHON_EXPORT PyClientAuthHandlerVtable {
public:
std::function<void(PyObject*, const arrow::flight::ClientAuthSender&,
const arrow::flight::ClientAuthReader&)>
authenticate;
std::function<void(PyObject*, std::string*)> get_token;
};
/// \brief A helper to implement an auth mechanism in Python.
class ARROW_PYTHON_EXPORT PyServerAuthHandler : public arrow::flight::ServerAuthHandler {
public:
explicit PyServerAuthHandler(PyObject* handler, PyServerAuthHandlerVtable vtable);
Status Authenticate(const arrow::flight::ServerAuthSender& outgoing,
const arrow::flight::ServerAuthReader& incoming) override;
Status IsValid(const std::string& token, std::string* peer_identity) override;
private:
OwnedRefNoGIL handler_;
PyServerAuthHandlerVtable vtable_;
};
/// \brief A helper to implement an auth mechanism in Python.
class ARROW_PYTHON_EXPORT PyClientAuthHandler : public arrow::flight::ClientAuthHandler {
public:
explicit PyClientAuthHandler(PyObject* handler, PyClientAuthHandlerVtable vtable);
Status Authenticate(const arrow::flight::ClientAuthSender& outgoing,
const arrow::flight::ClientAuthReader& incoming) override;
Status GetToken(std::string* token) override;
private:
OwnedRefNoGIL handler_;
PyClientAuthHandlerVtable vtable_;
};
class ARROW_PYTHON_EXPORT PyFlightServer : public arrow::flight::FlightServerBase {
@@ -63,14 +113,19 @@ class ARROW_PYTHON_EXPORT PyFlightServer : public arrow::flight::FlightServerBas
Status ListFlights(const arrow::flight::ServerCallContext& context,
const arrow::flight::Criteria* criteria,
std::unique_ptr<arrow::flight::FlightListing>* listings) override;
Status GetFlightInfo(const arrow::flight::FlightDescriptor& request,
Status GetFlightInfo(const arrow::flight::ServerCallContext& context,
const arrow::flight::FlightDescriptor& request,
std::unique_ptr<arrow::flight::FlightInfo>* info) override;
Status DoGet(const arrow::flight::Ticket& request,
Status DoGet(const arrow::flight::ServerCallContext& context,
const arrow::flight::Ticket& request,
std::unique_ptr<arrow::flight::FlightDataStream>* stream) override;
Status DoPut(std::unique_ptr<arrow::flight::FlightMessageReader> reader) override;
Status DoAction(const arrow::flight::Action& action,
Status DoPut(const arrow::flight::ServerCallContext& context,
std::unique_ptr<arrow::flight::FlightMessageReader> reader) override;
Status DoAction(const arrow::flight::ServerCallContext& context,
const arrow::flight::Action& action,
std::unique_ptr<arrow::flight::ResultStream>* result) override;
Status ListActions(std::vector<arrow::flight::ActionType>* actions) override;
Status ListActions(const arrow::flight::ServerCallContext& context,
std::vector<arrow::flight::ActionType>* actions) override;
private:
OwnedRefNoGIL server_;
+6 -6
View File
@@ -35,7 +35,7 @@ class FlightServer(pyarrow.flight.FlightServerBase):
return (descriptor.descriptor_type.value, descriptor.command,
tuple(descriptor.path or tuple()))
def list_flights(self, criteria):
def list_flights(self, context, criteria):
for key, table in self.flights.items():
if key[1] is not None:
descriptor = \
@@ -51,7 +51,7 @@ class FlightServer(pyarrow.flight.FlightServerBase):
descriptor, endpoints,
table.num_rows, 0)
def get_flight_info(self, descriptor):
def get_flight_info(self, context, descriptor):
key = FlightServer.descriptor_to_key(descriptor)
if key in self.flights:
table = self.flights[key]
@@ -64,25 +64,25 @@ class FlightServer(pyarrow.flight.FlightServerBase):
table.num_rows, 0)
raise KeyError('Flight not found.')
def do_put(self, descriptor, reader):
def do_put(self, context, descriptor, reader):
key = FlightServer.descriptor_to_key(descriptor)
print(key)
self.flights[key] = reader.read_all()
print(self.flights[key])
def do_get(self, ticket):
def do_get(self, context, ticket):
key = ast.literal_eval(ticket.ticket.decode())
if key not in self.flights:
return None
return pyarrow.flight.RecordBatchStream(self.flights[key])
def list_actions(self):
def list_actions(self, context):
return [
("clear", "Clear the stored flights."),
("shutdown", "Shut down this server."),
]
def do_action(self, action):
def do_action(self, context, action):
if action.type == "clear":
raise NotImplementedError(
"{} is not implemented.".format(action.type))
+271 -19
View File
@@ -312,6 +312,17 @@ cdef class FlightClient:
return result
def authenticate(self, auth_handler):
"""Authenticate to the server."""
cdef unique_ptr[CClientAuthHandler] handler
if not isinstance(auth_handler, ClientAuthHandler):
raise TypeError(
"FlightClient.authenticate takes a ClientAuthHandler, "
"not '{}'".format(type(auth_handler)))
handler.reset((<ClientAuthHandler> auth_handler).to_handler())
with nogil:
check_status(self.client.get().Authenticate(move(handler)))
def list_actions(self):
"""List the actions available on a service."""
cdef:
@@ -474,6 +485,148 @@ cdef class GeneratorStream(FlightDataStream):
return new CPyGeneratorFlightDataStream(self, self.schema, callback)
cdef class ServerCallContext:
"""Per-call state/context."""
cdef:
const CServerCallContext* context
def peer_identity(self):
"""Get the identity of the authenticated peer.
May be the empty string.
"""
return tobytes(self.context.peer_identity())
@staticmethod
cdef ServerCallContext wrap(const CServerCallContext& context):
cdef ServerCallContext result = \
ServerCallContext.__new__(ServerCallContext)
result.context = &context
return result
cdef class ServerAuthReader:
"""A reader for messages from the client during an auth handshake."""
cdef:
const CServerAuthReader* reader
def read(self):
cdef c_string token
if not self.reader:
raise ValueError("Cannot use ServerAuthReader outside "
"ServerAuthHandler.authenticate")
with nogil:
check_status(self.reader.Read(&token))
return token
cdef void poison(self):
"""Prevent further usage of this object.
This object is constructed by taking a pointer to a reference,
so we want to make sure Python users do not access this after
the reference goes away.
"""
self.reader = NULL
@staticmethod
cdef ServerAuthReader wrap(const CServerAuthReader& reader):
cdef ServerAuthReader result = \
ServerAuthReader.__new__(ServerAuthReader)
result.reader = &reader
return result
cdef class ServerAuthSender:
"""A writer for messages to the client during an auth handshake."""
cdef:
const CServerAuthSender* sender
def write(self, message):
cdef c_string c_message = tobytes(message)
if not self.sender:
raise ValueError("Cannot use ServerAuthSender outside "
"ServerAuthHandler.authenticate")
with nogil:
check_status(self.sender.Write(c_message))
cdef void poison(self):
"""Prevent further usage of this object.
This object is constructed by taking a pointer to a reference,
so we want to make sure Python users do not access this after
the reference goes away.
"""
self.sender = NULL
@staticmethod
cdef ServerAuthSender wrap(const CServerAuthSender& sender):
cdef ServerAuthSender result = \
ServerAuthSender.__new__(ServerAuthSender)
result.sender = &sender
return result
cdef class ClientAuthReader:
"""A reader for messages from the server during an auth handshake."""
cdef:
const CClientAuthReader* reader
def read(self):
cdef c_string token
if not self.reader:
raise ValueError("Cannot use ClientAuthReader outside "
"ClientAuthHandler.authenticate")
with nogil:
check_status(self.reader.Read(&token))
return token
cdef void poison(self):
"""Prevent further usage of this object.
This object is constructed by taking a pointer to a reference,
so we want to make sure Python users do not access this after
the reference goes away.
"""
self.reader = NULL
@staticmethod
cdef ClientAuthReader wrap(const CClientAuthReader& reader):
cdef ClientAuthReader result = \
ClientAuthReader.__new__(ClientAuthReader)
result.reader = &reader
return result
cdef class ClientAuthSender:
"""A writer for messages to the server during an auth handshake."""
cdef:
const CClientAuthSender* sender
def write(self, message):
cdef c_string c_message = tobytes(message)
if not self.sender:
raise ValueError("Cannot use ClientAuthSender outside "
"ClientAuthHandler.authenticate")
with nogil:
check_status(self.sender.Write(c_message))
cdef void poison(self):
"""Prevent further usage of this object.
This object is constructed by taking a pointer to a reference,
so we want to make sure Python users do not access this after
the reference goes away.
"""
self.sender = NULL
@staticmethod
cdef ClientAuthSender wrap(const CClientAuthSender& sender):
cdef ClientAuthSender result = \
ClientAuthSender.__new__(ClientAuthSender)
result.sender = &sender
return result
cdef void _data_stream_next(void* self, CFlightPayload* payload) except *:
"""Callback for implementing FlightDataStream in Python."""
cdef:
@@ -534,12 +687,14 @@ cdef void _data_stream_next(void* self, CFlightPayload* payload) except *:
"not {}.".format(type(result)))
cdef void _list_flights(void* self, const CCriteria* c_criteria,
cdef void _list_flights(void* self, const CServerCallContext& context,
const CCriteria* c_criteria,
unique_ptr[CFlightListing]* listing) except *:
"""Callback for implementing ListFlights in Python."""
cdef:
vector[CFlightInfo] flights
result = (<object> self).list_flights(c_criteria.expression)
result = (<object> self).list_flights(ServerCallContext.wrap(context),
c_criteria.expression)
for info in result:
if not isinstance(info, FlightInfo):
raise TypeError("FlightServerBase.list_flights must return "
@@ -549,14 +704,16 @@ cdef void _list_flights(void* self, const CCriteria* c_criteria,
listing.reset(new CSimpleFlightListing(flights))
cdef void _get_flight_info(void* self, CFlightDescriptor c_descriptor,
cdef void _get_flight_info(void* self, const CServerCallContext& context,
CFlightDescriptor c_descriptor,
unique_ptr[CFlightInfo]* info) except *:
"""Callback for implementing Flight servers in Python."""
cdef:
FlightDescriptor py_descriptor = \
FlightDescriptor.__new__(FlightDescriptor)
py_descriptor.descriptor = c_descriptor
result = (<object> self).get_flight_info(py_descriptor)
result = (<object> self).get_flight_info(ServerCallContext.wrap(context),
py_descriptor)
if not isinstance(result, FlightInfo):
raise TypeError("FlightServerBase.get_flight_info must return "
"a FlightInfo instance, but got {}".format(
@@ -564,7 +721,7 @@ cdef void _get_flight_info(void* self, CFlightDescriptor c_descriptor,
info.reset(new CFlightInfo(deref((<FlightInfo> result).info.get())))
cdef void _do_put(void* self,
cdef void _do_put(void* self, const CServerCallContext& context,
unique_ptr[CFlightMessageReader] reader) except *:
"""Callback for implementing Flight servers in Python."""
cdef:
@@ -574,17 +731,20 @@ cdef void _do_put(void* self,
descriptor.descriptor = reader.get().descriptor()
py_reader.reader.reset(reader.release())
(<object> self).do_put(descriptor, py_reader)
(<object> self).do_put(ServerCallContext.wrap(context), descriptor,
py_reader)
cdef void _do_get(void* self, CTicket ticket,
cdef void _do_get(void* self, const CServerCallContext& context,
CTicket ticket,
unique_ptr[CFlightDataStream]* stream) except *:
"""Callback for implementing Flight servers in Python."""
cdef:
unique_ptr[CFlightDataStream] data_stream
py_ticket = Ticket(ticket.ticket)
result = (<object> self).do_get(py_ticket)
result = (<object> self).do_get(ServerCallContext.wrap(context),
py_ticket)
if not isinstance(result, FlightDataStream):
raise TypeError("FlightServerBase.do_get must return "
"a FlightDataStream")
@@ -607,17 +767,20 @@ cdef void _do_action_result_next(void* self,
result.reset(nullptr)
cdef void _do_action(void* self, const CAction& action,
cdef void _do_action(void* self, const CServerCallContext& context,
const CAction& action,
unique_ptr[CResultStream]* result) except *:
"""Callback for implementing Flight servers in Python."""
cdef:
function[cb_result_next] ptr = &_do_action_result_next
py_action = Action(action.type, pyarrow_wrap_buffer(action.body))
responses = (<object> self).do_action(py_action)
responses = (<object> self).do_action(ServerCallContext.wrap(context),
py_action)
result.reset(new CPyFlightResultStream(responses, ptr))
cdef void _list_actions(void* self, vector[CActionType]* actions) except *:
cdef void _list_actions(void* self, const CServerCallContext& context,
vector[CActionType]* actions) except *:
"""Callback for implementing Flight servers in Python."""
cdef:
CActionType action_type
@@ -629,17 +792,106 @@ cdef void _list_actions(void* self, vector[CActionType]* actions) except *:
actions.push_back(action_type)
cdef void _server_authenticate(void* self, const CServerAuthSender& outgoing,
const CServerAuthReader& incoming) except *:
"""Callback for implementing authentication in Python."""
sender = ServerAuthSender.wrap(outgoing)
reader = ServerAuthReader.wrap(incoming)
try:
(<object> self).authenticate(sender, reader)
finally:
sender.poison()
reader.poison()
cdef void _is_valid(void* self, const c_string& token,
c_string* peer_identity) except *:
"""Callback for implementing authentication in Python."""
cdef c_string c_result
c_result = tobytes((<object> self).is_valid(token))
peer_identity[0] = c_result
cdef void _client_authenticate(void* self, const CClientAuthSender& outgoing,
const CClientAuthReader& incoming) except *:
"""Callback for implementing authentication in Python."""
sender = ClientAuthSender.wrap(outgoing)
reader = ClientAuthReader.wrap(incoming)
try:
(<object> self).authenticate(sender, reader)
finally:
sender.poison()
reader.poison()
cdef void _get_token(void* self, c_string* token) except *:
"""Callback for implementing authentication in Python."""
cdef c_string c_result
c_result = tobytes((<object> self).get_token())
token[0] = c_result
cdef class ServerAuthHandler:
"""Authentication middleware for a server."""
def authenticate(self, outgoing, incoming):
"""Conduct the handshake with the client."""
raise NotImplementedError
def is_valid(self, token):
"""Validate a client token, returning their identity.
May return an empty string (if the auth mechanism does not
name the peer) or raise an exception (if the token is
invalid).
"""
raise NotImplementedError
cdef PyServerAuthHandler* to_handler(self):
cdef PyServerAuthHandlerVtable vtable
vtable.authenticate = _server_authenticate
vtable.is_valid = _is_valid
return new PyServerAuthHandler(self, vtable)
cdef class ClientAuthHandler:
"""Authentication plugin for a client."""
def authenticate(self, outgoing, incoming):
"""Conduct the handshake with the server."""
raise NotImplementedError
def get_token(self):
"""Get the auth token for a call."""
raise NotImplementedError
cdef PyClientAuthHandler* to_handler(self):
cdef PyClientAuthHandlerVtable vtable
vtable.authenticate = _client_authenticate
vtable.get_token = _get_token
return new PyClientAuthHandler(self, vtable)
cdef class FlightServerBase:
"""A Flight service definition."""
cdef:
unique_ptr[PyFlightServer] server
def run(self, port):
def run(self, port, auth_handler=None):
cdef:
PyFlightServerVtable vtable = PyFlightServerVtable()
int c_port = port
PyFlightServer* c_server
unique_ptr[CServerAuthHandler] c_auth_handler
if auth_handler:
if not isinstance(auth_handler, ServerAuthHandler):
raise TypeError("auth_handler must be a ServerAuthHandler, "
"not a '{}'".format(type(auth_handler)))
c_auth_handler.reset(
(<ServerAuthHandler> auth_handler).to_handler())
vtable.list_flights = &_list_flights
vtable.get_flight_info = &_get_flight_info
@@ -651,25 +903,25 @@ cdef class FlightServerBase:
c_server = new PyFlightServer(self, vtable)
self.server.reset(c_server)
with nogil:
check_status(c_server.Init(c_port))
check_status(c_server.Init(move(c_auth_handler), c_port))
check_status(c_server.ServeWithSignals())
def list_flights(self, criteria):
def list_flights(self, context, criteria):
raise NotImplementedError
def get_flight_info(self, descriptor):
def get_flight_info(self, context, descriptor):
raise NotImplementedError
def do_put(self, descriptor, reader):
def do_put(self, context, descriptor, reader):
raise NotImplementedError
def do_get(self, ticket):
def do_get(self, context, ticket):
raise NotImplementedError
def list_actions(self):
def list_actions(self, context):
raise NotImplementedError
def do_action(self, action):
def do_action(self, context, action):
raise NotImplementedError
def shutdown(self):
+17 -13
View File
@@ -15,16 +15,20 @@
# specific language governing permissions and limitations
# under the License.
from pyarrow._flight import (Action, # noqa
ActionType,
DescriptorType,
FlightClient,
FlightDescriptor,
FlightEndpoint,
FlightInfo,
FlightServerBase,
GeneratorStream,
Location,
Ticket,
RecordBatchStream,
Result)
from pyarrow._flight import ( # noqa
Action,
ActionType,
DescriptorType,
FlightClient,
FlightDescriptor,
FlightEndpoint,
FlightInfo,
FlightServerBase,
GeneratorStream,
Location,
Ticket,
RecordBatchStream,
Result,
ClientAuthHandler,
ServerAuthHandler,
)
+62 -8
View File
@@ -120,11 +120,34 @@ cdef extern from "arrow/flight/api.h" namespace "arrow" nogil:
" arrow::flight::RecordBatchStream"(CFlightDataStream):
CRecordBatchStream(shared_ptr[CRecordBatchReader]& reader)
cdef cppclass CServerAuthReader" arrow::flight::ServerAuthReader":
CStatus Read(c_string* token)
cdef cppclass CServerAuthSender" arrow::flight::ServerAuthSender":
CStatus Write(c_string& token)
cdef cppclass CClientAuthReader" arrow::flight::ClientAuthReader":
CStatus Read(c_string* token)
cdef cppclass CClientAuthSender" arrow::flight::ClientAuthSender":
CStatus Write(c_string& token)
cdef cppclass CServerAuthHandler" arrow::flight::ServerAuthHandler":
pass
cdef cppclass CClientAuthHandler" arrow::flight::ClientAuthHandler":
pass
cdef cppclass CServerCallContext" arrow::flight::ServerCallContext":
c_string& peer_identity()
cdef cppclass CFlightClient" arrow::flight::FlightClient":
@staticmethod
CStatus Connect(const c_string& host, int port,
unique_ptr[CFlightClient]* client)
CStatus Authenticate(unique_ptr[CClientAuthHandler] auth_handler)
CStatus DoAction(CAction& action, unique_ptr[CResultStream]* results)
CStatus ListActions(vector[CActionType]* actions)
@@ -141,18 +164,29 @@ cdef extern from "arrow/flight/api.h" namespace "arrow" nogil:
# Callbacks for implementing Flight servers
# Use typedef to emulate syntax for std::function<void(...)>
ctypedef void cb_list_flights(object, const CCriteria*,
ctypedef void cb_list_flights(object, const CServerCallContext&,
const CCriteria*,
unique_ptr[CFlightListing]*)
ctypedef void cb_get_flight_info(object, const CFlightDescriptor&,
ctypedef void cb_get_flight_info(object, const CServerCallContext&,
const CFlightDescriptor&,
unique_ptr[CFlightInfo]*)
ctypedef void cb_do_put(object, unique_ptr[CFlightMessageReader])
ctypedef void cb_do_get(object, const CTicket&,
ctypedef void cb_do_put(object, const CServerCallContext&,
unique_ptr[CFlightMessageReader])
ctypedef void cb_do_get(object, const CServerCallContext&,
const CTicket&,
unique_ptr[CFlightDataStream]*)
ctypedef void cb_do_action(object, const CAction&,
ctypedef void cb_do_action(object, const CServerCallContext&, const CAction&,
unique_ptr[CResultStream]*)
ctypedef void cb_list_actions(object, vector[CActionType]*)
ctypedef void cb_list_actions(object, const CServerCallContext&,
vector[CActionType]*)
ctypedef void cb_result_next(object, unique_ptr[CResult]*)
ctypedef void cb_data_stream_next(object, CFlightPayload*)
ctypedef void cb_server_authenticate(object, const CServerAuthSender&,
const CServerAuthReader&)
ctypedef void cb_is_valid(object, const c_string&, c_string*)
ctypedef void cb_client_authenticate(object, const CClientAuthSender&,
const CClientAuthReader&)
ctypedef void cb_get_token(object, c_string*)
cdef extern from "arrow/python/flight.h" namespace "arrow::py::flight" nogil:
cdef cppclass PyFlightServerVtable:
@@ -164,13 +198,31 @@ cdef extern from "arrow/python/flight.h" namespace "arrow::py::flight" nogil:
function[cb_do_action] do_action
function[cb_list_actions] list_actions
cdef cppclass PyServerAuthHandlerVtable:
PyServerAuthHandlerVtable()
function[cb_server_authenticate] authenticate
function[cb_is_valid] is_valid
cdef cppclass PyClientAuthHandlerVtable:
PyClientAuthHandlerVtable()
function[cb_client_authenticate] authenticate
function[cb_get_token] get_token
cdef cppclass PyFlightServer:
PyFlightServer(object server, PyFlightServerVtable vtable)
CStatus Init(int port)
CStatus Init(unique_ptr[CServerAuthHandler] auth_handler, int port)
CStatus ServeWithSignals() except *
void Shutdown()
cdef cppclass PyServerAuthHandler\
" arrow::py::flight::PyServerAuthHandler"(CServerAuthHandler):
PyServerAuthHandler(object handler, PyServerAuthHandlerVtable vtable)
cdef cppclass PyClientAuthHandler\
" arrow::py::flight::PyClientAuthHandler"(CClientAuthHandler):
PyClientAuthHandler(object handler, PyClientAuthHandlerVtable vtable)
cdef cppclass CPyFlightResultStream\
" arrow::py::flight::PyFlightResultStream"(CResultStream):
CPyFlightResultStream(object generator,
@@ -197,4 +249,6 @@ cdef extern from "arrow/python/flight.h" namespace "arrow::py::flight" nogil:
unique_ptr[CFlightInfo]* out)
cdef extern from "<utility>" namespace "std":
unique_ptr[CFlightDataStream] move(unique_ptr[CFlightDataStream])
unique_ptr[CFlightDataStream] move(unique_ptr[CFlightDataStream]) nogil
unique_ptr[CServerAuthHandler] move(unique_ptr[CServerAuthHandler]) nogil
unique_ptr[CClientAuthHandler] move(unique_ptr[CClientAuthHandler]) nogil
+160 -7
View File
@@ -16,6 +16,7 @@
# specific language governing permissions and limitations
# under the License.
import base64
import contextlib
import socket
import threading
@@ -24,6 +25,8 @@ import pytest
import pyarrow as pa
from pyarrow.compat import tobytes
flight = pytest.importorskip("pyarrow.flight")
@@ -35,7 +38,7 @@ class ConstantFlightServer(flight.FlightServerBase):
does not properly hold a reference to the Table object.
"""
def do_get(self, ticket):
def do_get(self, context, ticket):
data = [
pa.array([-10, -5, 0, 5, 10])
]
@@ -50,28 +53,36 @@ class EchoFlightServer(flight.FlightServerBase):
super(EchoFlightServer, self).__init__()
self.last_message = None
def do_get(self, ticket):
def do_get(self, context, ticket):
return flight.RecordBatchStream(self.last_message)
def do_put(self, descriptor, reader):
def do_put(self, context, descriptor, reader):
self.last_message = reader.read_all()
class EchoStreamFlightServer(EchoFlightServer):
"""An echo server that streams individual record batches."""
def do_get(self, ticket):
def do_get(self, context, ticket):
return flight.GeneratorStream(
self.last_message.schema,
self.last_message.to_batches(chunksize=1024))
def list_actions(self, context):
return []
def do_action(self, context, action):
if action.type == "who-am-i":
return iter([flight.Result(context.peer_identity())])
raise NotImplementedError
class InvalidStreamFlightServer(flight.FlightServerBase):
"""A Flight server that tries to return messages with differing schemas."""
schema = pa.schema([('a', pa.int32())])
def do_get(self, ticket):
def do_get(self, context, ticket):
data1 = [pa.array([-10, -5, 0, 5, 10], type=pa.int32())]
data2 = [pa.array([-10.0, -5.0, 0.0, 5.0, 10.0], type=pa.float64())]
assert data1.type != data2.type
@@ -82,6 +93,83 @@ class InvalidStreamFlightServer(flight.FlightServerBase):
return flight.GeneratorStream(self.schema, [table1, table2])
class HttpBasicServerAuthHandler(flight.ServerAuthHandler):
"""An example implementation of HTTP basic authentication."""
def __init__(self, creds):
super().__init__()
self.creds = creds
def authenticate(self, outgoing, incoming):
pass
def is_valid(self, token):
if not token:
raise ValueError("unauthenticated: token not provided")
token = base64.b64decode(token)
username, password = token.split(b':')
if username not in self.creds:
raise ValueError("unknown user")
if self.creds[username] != password:
raise ValueError("wrong password")
return username
class HttpBasicClientAuthHandler(flight.ClientAuthHandler):
"""An example implementation of HTTP basic authentication."""
def __init__(self, username, password):
super().__init__()
self.username = tobytes(username)
self.password = tobytes(password)
def authenticate(self, outgoing, incoming):
pass
def get_token(self):
return base64.b64encode(self.username + b':' + self.password)
class TokenServerAuthHandler(flight.ServerAuthHandler):
"""An example implementation of authentication via handshake."""
def __init__(self, creds):
super().__init__()
self.creds = creds
def authenticate(self, outgoing, incoming):
username = incoming.read()
password = incoming.read()
if username in self.creds and self.creds[username] == password:
outgoing.write(base64.b64encode(b'secret:' + username))
else:
raise ValueError("unauthenticated: invalid username/password")
def is_valid(self, token):
token = base64.b64decode(token)
if not token.startswith(b'secret:'):
raise ValueError("unauthenticated: invalid token")
return token[7:]
class TokenClientAuthHandler(flight.ClientAuthHandler):
"""An example implementation of authentication via handshake."""
def __init__(self, username, password):
super().__init__()
self.username = username
self.password = password
self.token = b''
def authenticate(self, outgoing, incoming):
outgoing.write(self.username)
outgoing.write(self.password)
self.token = incoming.read()
def get_token(self):
return self.token
@contextlib.contextmanager
def flight_server(server_base, *args, **kwargs):
"""Spawn a Flight server on a free port, shutting it down when done."""
@@ -92,10 +180,14 @@ def flight_server(server_base, *args, **kwargs):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
port = sock.getsockname()[1]
server_instance = server_base(*args, **kwargs)
auth_handler = kwargs.get('auth_handler')
ctor_kwargs = kwargs
if auth_handler:
del ctor_kwargs['auth_handler']
server_instance = server_base(*args, **ctor_kwargs)
def _server_thread():
server_instance.run(port)
server_instance.run(port, auth_handler=auth_handler)
thread = threading.Thread(target=_server_thread, daemon=True)
thread.start()
@@ -163,3 +255,64 @@ def test_flight_invalid_generator_stream():
client = flight.FlightClient.connect('localhost', server_port)
with pytest.raises(pa.ArrowException):
client.do_get(flight.Ticket(b'')).read_all()
basic_auth_handler = HttpBasicServerAuthHandler(creds={
b"test": b"p4ssw0rd",
})
token_auth_handler = TokenServerAuthHandler(creds={
b"test": b"p4ssw0rd",
})
def test_http_basic_unauth():
"""Test that auth fails when not authenticated."""
with flight_server(EchoStreamFlightServer,
auth_handler=basic_auth_handler) as server_port:
client = flight.FlightClient.connect('localhost', server_port)
action = flight.Action("who-am-i", b"")
with pytest.raises(pa.ArrowException, match=".*unauthenticated.*"):
list(client.do_action(action))
def test_http_basic_auth():
"""Test a Python implementation of HTTP basic authentication."""
with flight_server(EchoStreamFlightServer,
auth_handler=basic_auth_handler) as server_port:
client = flight.FlightClient.connect('localhost', server_port)
action = flight.Action("who-am-i", b"")
client.authenticate(HttpBasicClientAuthHandler('test', 'p4ssw0rd'))
identity = next(client.do_action(action))
assert identity.body.to_pybytes() == b'test'
def test_http_basic_auth_invalid_password():
"""Test that auth fails with the wrong password."""
with flight_server(EchoStreamFlightServer,
auth_handler=basic_auth_handler) as server_port:
client = flight.FlightClient.connect('localhost', server_port)
action = flight.Action("who-am-i", b"")
client.authenticate(HttpBasicClientAuthHandler('test', 'wrong'))
with pytest.raises(pa.ArrowException, match=".*wrong password.*"):
next(client.do_action(action))
def test_token_auth():
"""Test an auth mechanism that uses a handshake."""
with flight_server(EchoStreamFlightServer,
auth_handler=token_auth_handler) as server_port:
client = flight.FlightClient.connect('localhost', server_port)
action = flight.Action("who-am-i", b"")
client.authenticate(TokenClientAuthHandler('test', 'p4ssw0rd'))
identity = next(client.do_action(action))
assert identity.body.to_pybytes() == b'test'
def test_token_auth_invalid():
"""Test an auth mechanism that uses a handshake."""
with flight_server(EchoStreamFlightServer,
auth_handler=token_auth_handler) as server_port:
client = flight.FlightClient.connect('localhost', server_port)
with pytest.raises(pa.ArrowException, match=".*unauthenticated.*"):
client.authenticate(TokenClientAuthHandler('test', 'wrong'))