diff --git a/src/csharp/Grpc.Core/Credentials.cs b/src/csharp/Grpc.Core/Credentials.cs index 4fcac0c4c00..e653d3688cf 100644 --- a/src/csharp/Grpc.Core/Credentials.cs +++ b/src/csharp/Grpc.Core/Credentials.cs @@ -32,10 +32,17 @@ #endregion using System; +using System.Collections.Generic; +using System.Threading.Tasks; + using Grpc.Core.Internal; +using Grpc.Core.Utils; namespace Grpc.Core { + // TODO: rename + public delegate Task AsyncAuthInterceptor(string authUri, Metadata metadata); + /// /// Client-side credentials. Used for creation of a secure channel. /// @@ -135,4 +142,49 @@ namespace Grpc.Core return CredentialsSafeHandle.CreateSslCredentials(rootCertificates, keyCertificatePair); } } + + /// + /// Client-side credentials that delegate metadata based auth to an interceptor. + /// + public partial class MetadataCredentials : Credentials + { + readonly AsyncAuthInterceptor interceptor; + + public MetadataCredentials(AsyncAuthInterceptor interceptor) + { + this.interceptor = interceptor; + } + + internal override CredentialsSafeHandle ToNativeCredentials() + { + NativeMetadataCredentialsPlugin plugin = new NativeMetadataCredentialsPlugin(interceptor); + return plugin.Credentials; + } + } + + public sealed class CompositeCredentials : Credentials + { + readonly List credentials; + + public CompositeCredentials(params Credentials[] credentials) + { + Preconditions.CheckArgument(credentials.Length >= 2, "Composite credentials object can only be created from 2 or more credentials."); + this.credentials = new List(credentials); + } + + public static CompositeCredentials Create(params Credentials[] credentials) + { + return new CompositeCredentials(credentials); + } + + internal override CredentialsSafeHandle ToNativeCredentials() + { + var nativeComposite = credentials[0].ToNativeCredentials(); + for (int i = 1; i < credentials.Count; i++) + { + nativeComposite = CredentialsSafeHandle.CreateComposite(nativeComposite, credentials[i].ToNativeCredentials()); + } + return nativeComposite; + } + } } diff --git a/src/csharp/Grpc.Core/Grpc.Core.csproj b/src/csharp/Grpc.Core/Grpc.Core.csproj index ad2af17bc75..04c3eda1130 100644 --- a/src/csharp/Grpc.Core/Grpc.Core.csproj +++ b/src/csharp/Grpc.Core/Grpc.Core.csproj @@ -49,6 +49,7 @@ + diff --git a/src/csharp/Grpc.Core/Internal/CredentialsSafeHandle.cs b/src/csharp/Grpc.Core/Internal/CredentialsSafeHandle.cs index feed3353624..bab45108e02 100644 --- a/src/csharp/Grpc.Core/Internal/CredentialsSafeHandle.cs +++ b/src/csharp/Grpc.Core/Internal/CredentialsSafeHandle.cs @@ -43,6 +43,9 @@ namespace Grpc.Core.Internal [DllImport("grpc_csharp_ext.dll", CharSet = CharSet.Ansi)] static extern CredentialsSafeHandle grpcsharp_ssl_credentials_create(string pemRootCerts, string keyCertPairCertChain, string keyCertPairPrivateKey); + [DllImport("grpc_csharp_ext.dll")] + static extern CredentialsSafeHandle grpcsharp_composite_credentials_create(CredentialsSafeHandle creds1, CredentialsSafeHandle creds2); + [DllImport("grpc_csharp_ext.dll")] static extern void grpcsharp_credentials_release(IntPtr credentials); @@ -69,6 +72,11 @@ namespace Grpc.Core.Internal } } + public static CredentialsSafeHandle CreateComposite(CredentialsSafeHandle creds1, CredentialsSafeHandle creds2) + { + return grpcsharp_composite_credentials_create(creds1, creds2); + } + protected override bool ReleaseHandle() { grpcsharp_credentials_release(handle); diff --git a/src/csharp/Grpc.Core/Internal/NativeMetadataCredentialsPlugin.cs b/src/csharp/Grpc.Core/Internal/NativeMetadataCredentialsPlugin.cs new file mode 100644 index 00000000000..6662a73b17a --- /dev/null +++ b/src/csharp/Grpc.Core/Internal/NativeMetadataCredentialsPlugin.cs @@ -0,0 +1,112 @@ +#region Copyright notice and license +// 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. +#endregion +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +using Grpc.Core.Logging; +using Grpc.Core.Utils; + +namespace Grpc.Core.Internal +{ + internal delegate void NativeMetadataInterceptor(IntPtr statePtr, IntPtr serviceUrlPtr, IntPtr callbackPtr, IntPtr userDataPtr, bool isDestroy); + + internal class NativeMetadataCredentialsPlugin + { + const string GetMetadataExceptionMsg = "Exception occured in metadata credentials plugin."; + static readonly ILogger Logger = GrpcEnvironment.Logger.ForType(); + + [DllImport("grpc_csharp_ext.dll")] + static extern CredentialsSafeHandle grpcsharp_metadata_credentials_create_from_plugin(NativeMetadataInterceptor interceptor); + + [DllImport("grpc_csharp_ext.dll", CharSet = CharSet.Ansi)] + static extern void grpcsharp_metadata_credentials_notify_from_plugin(IntPtr callbackPtr, IntPtr userData, MetadataArraySafeHandle metadataArray, StatusCode statusCode, string errorDetails); + + AsyncAuthInterceptor interceptor; + GCHandle gcHandle; + NativeMetadataInterceptor nativeInterceptor; + CredentialsSafeHandle credentials; + + public NativeMetadataCredentialsPlugin(AsyncAuthInterceptor interceptor) + { + this.interceptor = Preconditions.CheckNotNull(interceptor, "interceptor"); + this.nativeInterceptor = NativeMetadataInterceptorHandler; + + // Make sure the callback doesn't get garbage collected until it is destroyed. + this.gcHandle = GCHandle.Alloc(this.nativeInterceptor, GCHandleType.Normal); + this.credentials = grpcsharp_metadata_credentials_create_from_plugin(nativeInterceptor); + } + + public CredentialsSafeHandle Credentials + { + get { return credentials; } + } + + private void NativeMetadataInterceptorHandler(IntPtr statePtr, IntPtr serviceUrlPtr, IntPtr callbackPtr, IntPtr userDataPtr, bool isDestroy) + { + if (isDestroy) + { + gcHandle.Free(); + return; + } + + try + { + string serviceUrl = Marshal.PtrToStringAnsi(serviceUrlPtr); + StartGetMetadata(serviceUrl, callbackPtr, userDataPtr); + } + catch (Exception e) + { + grpcsharp_metadata_credentials_notify_from_plugin(callbackPtr, userDataPtr, null, StatusCode.Unknown, GetMetadataExceptionMsg); + Logger.Error(e, GetMetadataExceptionMsg); + } + } + + private async void StartGetMetadata(string serviceUrl, IntPtr callbackPtr, IntPtr userDataPtr) + { + try + { + var metadata = new Metadata(); + await interceptor(serviceUrl, metadata); + using (var metadataArray = MetadataArraySafeHandle.Create(metadata)) + { + grpcsharp_metadata_credentials_notify_from_plugin(callbackPtr, userDataPtr, metadataArray, StatusCode.OK, null); + } + } + catch (Exception e) + { + grpcsharp_metadata_credentials_notify_from_plugin(callbackPtr, userDataPtr, null, StatusCode.Unknown, GetMetadataExceptionMsg); + Logger.Error(e, GetMetadataExceptionMsg); + } + } + } +} diff --git a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj index a0bcf431f7b..f0a39acf753 100644 --- a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj +++ b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj @@ -96,6 +96,7 @@ + diff --git a/src/csharp/Grpc.IntegrationTesting/MetadataCredentialsTest.cs b/src/csharp/Grpc.IntegrationTesting/MetadataCredentialsTest.cs new file mode 100644 index 00000000000..76991dfc209 --- /dev/null +++ b/src/csharp/Grpc.IntegrationTesting/MetadataCredentialsTest.cs @@ -0,0 +1,100 @@ +#region Copyright notice and license + +// 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. + +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Utils; +using Grpc.Testing; +using NUnit.Framework; + +namespace Grpc.IntegrationTesting +{ + public class MetadataCredentialsTest + { + const string Host = "localhost"; + Server server; + Channel channel; + TestService.ITestServiceClient client; + + [TestFixtureSetUp] + public void Init() + { + var serverCredentials = new SslServerCredentials(new[] { new KeyCertificatePair( + File.ReadAllText(TestCredentials.ServerCertChainPath), + File.ReadAllText(TestCredentials.ServerPrivateKeyPath)) }); + server = new Server + { + Services = { TestService.BindService(new TestServiceImpl()) }, + Ports = { { Host, ServerPort.PickUnused, serverCredentials } } + }; + server.Start(); + + var options = new List + { + new ChannelOption(ChannelOptions.SslTargetNameOverride, TestCredentials.DefaultHostOverride) + }; + + var asyncAuthInterceptor = new AsyncAuthInterceptor(async (authUri, metadata) => + { + await Task.Delay(100); // make sure the operation is asynchronous. + metadata.Add("authorization", "SECRET_TOKEN"); + }); + + var clientCredentials = CompositeCredentials.Create( + new SslCredentials(File.ReadAllText(TestCredentials.ClientCertAuthorityPath)), + new MetadataCredentials(asyncAuthInterceptor) + ); + channel = new Channel(Host, server.Ports.Single().BoundPort, clientCredentials, options); + client = TestService.NewClient(channel); + } + + [TestFixtureTearDown] + public void Cleanup() + { + channel.ShutdownAsync().Wait(); + server.ShutdownAsync().Wait(); + } + + [Test] + public void MetadataCredentials() + { + var response = client.UnaryCall(new SimpleRequest { ResponseSize = 10 }); + Assert.AreEqual(10, response.Payload.Body.Length); + } + } +} diff --git a/src/csharp/ext/grpc_csharp_ext.c b/src/csharp/ext/grpc_csharp_ext.c index 51e0728fb9d..657f999ad4e 100644 --- a/src/csharp/ext/grpc_csharp_ext.c +++ b/src/csharp/ext/grpc_csharp_ext.c @@ -68,7 +68,7 @@ grpc_byte_buffer *string_to_byte_buffer(const char *buffer, size_t len) { /* * Helper to maintain lifetime of batch op inputs and store batch op outputs. */ -typedef struct gprcsharp_batch_context { +typedef struct grpcsharp_batch_context { grpc_metadata_array send_initial_metadata; grpc_byte_buffer *send_message; struct { @@ -892,6 +892,45 @@ grpcsharp_server_add_secure_http2_port(grpc_server *server, const char *addr, return grpc_server_add_secure_http2_port(server, addr, creds); } +GPR_EXPORT grpc_credentials *GPR_CALLTYPE grpcsharp_composite_credentials_create( + grpc_credentials *creds1, + grpc_credentials *creds2) { + return grpc_composite_credentials_create(creds1, creds2, NULL); +} + +/* Metadata credentials plugin */ + +GPR_EXPORT void GPR_CALLTYPE grpcsharp_metadata_credentials_notify_from_plugin( + void *callback_ptr, void *user_data, grpc_metadata_array *metadata, + grpc_status_code status, const char *error_details) { + grpc_credentials_plugin_metadata_cb cb = (grpc_credentials_plugin_metadata_cb)callback_ptr; + cb(user_data, metadata->metadata, metadata->count, status, error_details); +} + +typedef void(GPR_CALLTYPE *grpcsharp_metadata_interceptor_func)( + void *state, const char *service_url, void *callback_ptr, + void *user_data, gpr_int32 is_destroy); + +static void grpcsharp_get_metadata_handler(void *state, const char *service_url, + grpc_credentials_plugin_metadata_cb cb, void *user_data) { + grpcsharp_metadata_interceptor_func interceptor = (grpcsharp_metadata_interceptor_func)state; + interceptor(state, service_url, (void*)cb, user_data, 0); +} + +static void grpcsharp_metadata_credentials_destroy_handler(void *state) { + grpcsharp_metadata_interceptor_func interceptor = (grpcsharp_metadata_interceptor_func)state; + interceptor(state, NULL, NULL, NULL, 1); +} + +GPR_EXPORT grpc_credentials *GPR_CALLTYPE grpcsharp_metadata_credentials_create_from_plugin( + grpcsharp_metadata_interceptor_func metadata_interceptor) { + grpc_metadata_credentials_plugin plugin; + plugin.get_metadata = grpcsharp_get_metadata_handler; + plugin.destroy = grpcsharp_metadata_credentials_destroy_handler; + plugin.state = metadata_interceptor; + return grpc_metadata_credentials_create_from_plugin(plugin, NULL); +} + /* Logging */ typedef void(GPR_CALLTYPE *grpcsharp_log_func)(const char *file, gpr_int32 line,