diff --git a/src/csharp/Grpc.Core.Tests/AuthContextTest.cs b/src/csharp/Grpc.Core.Tests/AuthContextTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..f5fa469520cc84018e3c78c2b597eca4e1e36eb9 --- /dev/null +++ b/src/csharp/Grpc.Core.Tests/AuthContextTest.cs @@ -0,0 +1,86 @@ +#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 NUnit.Framework; +using Grpc.Core; +using System.Linq; + +namespace Grpc.Core.Tests +{ + public class AuthContextTest + { + [Test] + public void EmptyContext() + { + var context = new AuthContext(null, new Dictionary<string, List<AuthProperty>>()); + Assert.IsFalse(context.IsPeerAuthenticated); + Assert.IsNull(context.PeerIdentityPropertyName); + Assert.AreEqual(0, context.PeerIdentity.Count()); + Assert.AreEqual(0, context.Properties.Count()); + Assert.AreEqual(0, context.FindPropertiesByName("nonexistent").Count()); + } + + [Test] + public void AuthenticatedContext() + { + var property1 = AuthProperty.Create("abc", new byte[] { 68, 69, 70 }); + var context = new AuthContext("some_identity", new Dictionary<string, List<AuthProperty>> + { + {"some_identity", new List<AuthProperty> {property1}} + }); + Assert.IsTrue(context.IsPeerAuthenticated); + Assert.AreEqual("some_identity", context.PeerIdentityPropertyName); + Assert.AreEqual(1, context.PeerIdentity.Count()); + } + + [Test] + public void FindPropertiesByName() + { + var property1 = AuthProperty.Create("abc", new byte[] {68, 69, 70}); + var property2 = AuthProperty.Create("abc", new byte[] {71, 72, 73 }); + var property3 = AuthProperty.Create("abc", new byte[] {}); + var context = new AuthContext(null, new Dictionary<string, List<AuthProperty>> + { + {"existent", new List<AuthProperty> {property1, property2}}, + {"foobar", new List<AuthProperty> {property3}}, + }); + Assert.AreEqual(3, context.Properties.Count()); + Assert.AreEqual(0, context.FindPropertiesByName("nonexistent").Count()); + + var existentProperties = new List<AuthProperty>(context.FindPropertiesByName("existent")); + Assert.AreEqual(2, existentProperties.Count); + } + } +} diff --git a/src/csharp/Grpc.Core.Tests/AuthPropertyTest.cs b/src/csharp/Grpc.Core.Tests/AuthPropertyTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..745191b80db0c248b8db82b6af8133e8292e94ed --- /dev/null +++ b/src/csharp/Grpc.Core.Tests/AuthPropertyTest.cs @@ -0,0 +1,82 @@ +#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 NUnit.Framework; + +namespace Grpc.Core.Tests +{ + public class AuthPropertyTest + { + [Test] + public void Create_NameIsNotNull() + { + Assert.Throws(typeof(ArgumentNullException), () => AuthProperty.Create(null, new byte[0])); + Assert.Throws(typeof(ArgumentNullException), () => AuthProperty.CreateUnsafe(null, new byte[0])); + } + + [Test] + public void Create_ValueIsNotNull() + { + Assert.Throws(typeof(ArgumentNullException), () => AuthProperty.Create("abc", null)); + Assert.Throws(typeof(ArgumentNullException), () => AuthProperty.CreateUnsafe("abc", null)); + } + + [Test] + public void Create() + { + var valueBytes = new byte[] { 68, 69, 70 }; + var authProperty = AuthProperty.Create("abc", valueBytes); + + Assert.AreEqual("abc", authProperty.Name); + Assert.AreNotSame(valueBytes, authProperty.ValueBytesUnsafe); + CollectionAssert.AreEqual(valueBytes, authProperty.ValueBytes); + CollectionAssert.AreEqual(valueBytes, authProperty.ValueBytesUnsafe); + Assert.AreEqual("DEF", authProperty.Value); + } + + [Test] + public void CreateUnsafe() + { + var valueBytes = new byte[] { 68, 69, 70 }; + var authProperty = AuthProperty.CreateUnsafe("abc", valueBytes); + + Assert.AreEqual("abc", authProperty.Name); + Assert.AreSame(valueBytes, authProperty.ValueBytesUnsafe); + Assert.AreNotSame(valueBytes, authProperty.ValueBytes); + CollectionAssert.AreEqual(valueBytes, authProperty.ValueBytes); + CollectionAssert.AreEqual(valueBytes, authProperty.ValueBytesUnsafe); + Assert.AreEqual("DEF", authProperty.Value); + } + } +} diff --git a/src/csharp/Grpc.Core.Tests/ClientServerTest.cs b/src/csharp/Grpc.Core.Tests/ClientServerTest.cs index 6bf9756962ea8637b379ec438258c81a89cd0558..3a99107c422eea38eeb0c9b61c3d2ab0ce91c969 100644 --- a/src/csharp/Grpc.Core.Tests/ClientServerTest.cs +++ b/src/csharp/Grpc.Core.Tests/ClientServerTest.cs @@ -375,6 +375,18 @@ namespace Grpc.Core.Tests Assert.AreEqual("PASS", Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc")); } + [Test] + public void ServerCallContext_AuthContextNotPopulated() + { + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + Assert.IsFalse(context.AuthContext.IsPeerAuthenticated); + Assert.AreEqual(0, context.AuthContext.Properties.Count()); + return "PASS"; + }); + Assert.AreEqual("PASS", Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "abc")); + } + [Test] public async Task Channel_WaitForStateChangedAsync() { diff --git a/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj b/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj index 646effe21a7c0eee42f48ff4bc0c70d749a05db7..a5e60e7328590c29c48d8e9bfb2862951579967d 100644 --- a/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj +++ b/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj @@ -81,6 +81,8 @@ <Compile Include="ShutdownHookPendingCallTest.cs" /> <Compile Include="ShutdownHookClientTest.cs" /> <Compile Include="AppDomainUnloadTest.cs" /> + <Compile Include="AuthContextTest.cs" /> + <Compile Include="AuthPropertyTest.cs" /> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <ItemGroup> diff --git a/src/csharp/Grpc.Core/AuthContext.cs b/src/csharp/Grpc.Core/AuthContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..340b2201c79f56f933b26ecd84b574a710f9c74d --- /dev/null +++ b/src/csharp/Grpc.Core/AuthContext.cs @@ -0,0 +1,128 @@ +#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.Linq; +using Grpc.Core.Internal; +using Grpc.Core.Utils; + +namespace Grpc.Core +{ + /// <summary> + /// Authentication context for a call. + /// AuthContext is the only reliable source of truth when it comes to authenticating calls. + /// Using any other call/context properties for authentication purposes is wrong and inherently unsafe. + /// Note: experimental API that can change or be removed without any prior notice. + /// </summary> + public class AuthContext + { + string peerIdentityPropertyName; + Dictionary<string, List<AuthProperty>> properties; + + /// <summary> + /// Initializes a new instance of the <see cref="T:Grpc.Core.AuthContext"/> class. + /// </summary> + /// <param name="peerIdentityPropertyName">Peer identity property name.</param> + /// <param name="properties">Multimap of auth properties by name.</param> + internal AuthContext(string peerIdentityPropertyName, Dictionary<string, List<AuthProperty>> properties) + { + this.peerIdentityPropertyName = peerIdentityPropertyName; + this.properties = GrpcPreconditions.CheckNotNull(properties); + } + + /// <summary> + /// Returns <c>true</c> if the peer is authenticated. + /// </summary> + public bool IsPeerAuthenticated + { + get + { + return peerIdentityPropertyName != null; + } + } + + /// <summary> + /// Gets the name of the property that indicates the peer identity. Returns <c>null</c> + /// if the peer is not authenticated. + /// </summary> + public string PeerIdentityPropertyName + { + get + { + return peerIdentityPropertyName; + } + } + + /// <summary> + /// Gets properties that represent the peer identity (there can be more than one). Returns an empty collection + /// if the peer is not authenticated. + /// </summary> + public IEnumerable<AuthProperty> PeerIdentity + { + get + { + if (peerIdentityPropertyName == null) + { + return Enumerable.Empty<AuthProperty>(); + } + return properties[peerIdentityPropertyName]; + } + } + + /// <summary> + /// Gets the auth properties of this context. + /// </summary> + public IEnumerable<AuthProperty> Properties + { + get + { + return properties.Values.SelectMany(v => v); + } + } + + /// <summary> + /// Returns the auth properties with given name (there can be more than one). + /// If no properties of given name exist, an empty collection will be returned. + /// </summary> + public IEnumerable<AuthProperty> FindPropertiesByName(string propertyName) + { + List<AuthProperty> result; + if (!properties.TryGetValue(propertyName, out result)) + { + return Enumerable.Empty<AuthProperty>(); + } + return result; + } + } +} diff --git a/src/csharp/Grpc.Core/AuthProperty.cs b/src/csharp/Grpc.Core/AuthProperty.cs new file mode 100644 index 0000000000000000000000000000000000000000..c7a132b09eb6895ed6f5c3aed1b2e932570ed77f --- /dev/null +++ b/src/csharp/Grpc.Core/AuthProperty.cs @@ -0,0 +1,126 @@ +#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.Linq; +using Grpc.Core.Internal; +using Grpc.Core.Utils; + +namespace Grpc.Core +{ + /// <summary> + /// A property of an <see cref="AuthContext"/>. + /// Note: experimental API that can change or be removed without any prior notice. + /// </summary> + public class AuthProperty + { + string name; + byte[] valueBytes; + Lazy<string> value; + + private AuthProperty(string name, byte[] valueBytes) + { + this.name = GrpcPreconditions.CheckNotNull(name); + this.valueBytes = GrpcPreconditions.CheckNotNull(valueBytes); + this.value = new Lazy<string>(() => MarshalUtils.GetStringUTF8(this.valueBytes)); + } + + /// <summary> + /// Gets the name of the property. + /// </summary> + public string Name + { + get + { + return name; + } + } + + /// <summary> + /// Gets the string value of the property. + /// </summary> + public string Value + { + get + { + return value.Value; + } + } + + /// <summary> + /// Gets the binary value of the property. + /// </summary> + public byte[] ValueBytes + { + get + { + var valueCopy = new byte[valueBytes.Length]; + Buffer.BlockCopy(valueBytes, 0, valueCopy, 0, valueBytes.Length); + return valueCopy; + } + } + + /// <summary> + /// Creates an instance of <c>AuthProperty</c>. + /// </summary> + /// <param name="name">the name</param> + /// <param name="valueBytes">the binary value of the property</param> + public static AuthProperty Create(string name, byte[] valueBytes) + { + GrpcPreconditions.CheckNotNull(valueBytes); + var valueCopy = new byte[valueBytes.Length]; + Buffer.BlockCopy(valueBytes, 0, valueCopy, 0, valueBytes.Length); + return new AuthProperty(name, valueCopy); + } + + /// <summary> + /// Gets the binary value of the property (without making a defensive copy). + /// </summary> + internal byte[] ValueBytesUnsafe + { + get + { + return valueBytes; + } + } + + /// <summary> + /// Creates and instance of <c>AuthProperty</c> without making a defensive copy of <c>valueBytes</c>. + /// </summary> + internal static AuthProperty CreateUnsafe(string name, byte[] valueBytes) + { + return new AuthProperty(name, valueBytes); + } + } +} diff --git a/src/csharp/Grpc.Core/Grpc.Core.csproj b/src/csharp/Grpc.Core/Grpc.Core.csproj index 23e1ddcf7f70928c50af77ff7c77e975432a598f..d6d8dfac22409ad1ac6975b84e10e3ff8d0b3296 100644 --- a/src/csharp/Grpc.Core/Grpc.Core.csproj +++ b/src/csharp/Grpc.Core/Grpc.Core.csproj @@ -131,6 +131,10 @@ <Compile Include="Internal\RequestCallContextSafeHandle.cs" /> <Compile Include="Utils\TaskUtils.cs" /> <Compile Include="Internal\CallFlags.cs" /> + <Compile Include="AuthContext.cs" /> + <Compile Include="Internal\AuthContextSafeHandle.cs" /> + <Compile Include="Internal\MarshalUtils.cs" /> + <Compile Include="AuthProperty.cs" /> </ItemGroup> <ItemGroup> <None Include="Grpc.Core.project.json" /> diff --git a/src/csharp/Grpc.Core/Internal/AuthContextSafeHandle.cs b/src/csharp/Grpc.Core/Internal/AuthContextSafeHandle.cs new file mode 100644 index 0000000000000000000000000000000000000000..59e33a0fdf16472e79c2f8ba4d487d2eeafc3d73 --- /dev/null +++ b/src/csharp/Grpc.Core/Internal/AuthContextSafeHandle.cs @@ -0,0 +1,119 @@ +#region Copyright notice and license + +// 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. + +#endregion + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using Grpc.Core; +using Grpc.Core.Utils; + +namespace Grpc.Core.Internal +{ + /// <summary> + /// grpc_auth_context + /// </summary> + internal class AuthContextSafeHandle : SafeHandleZeroIsInvalid + { + static readonly NativeMethods Native = NativeMethods.Get(); + + private AuthContextSafeHandle() + { + } + + /// <summary> + /// Copies contents of the native auth context into a new <c>AuthContext</c> instance. + /// </summary> + public AuthContext ToAuthContext() + { + if (IsInvalid) + { + return new AuthContext(null, new Dictionary<string, List<AuthProperty>>()); + } + + var peerIdentityPropertyName = Marshal.PtrToStringAnsi(Native.grpcsharp_auth_context_peer_identity_property_name(this)); + + var propertiesDict = new Dictionary<string, List<AuthProperty>>(); + + var it = Native.grpcsharp_auth_context_property_iterator(this); + IntPtr authPropertyPtr = IntPtr.Zero; + while ((authPropertyPtr = Native.grpcsharp_auth_property_iterator_next(ref it)) != IntPtr.Zero) + { + var authProperty = PtrToAuthProperty(authPropertyPtr); + + if (!propertiesDict.ContainsKey(authProperty.Name)) + { + propertiesDict[authProperty.Name] = new List<AuthProperty>(); + } + propertiesDict[authProperty.Name].Add(authProperty); + } + + return new AuthContext(peerIdentityPropertyName, propertiesDict); + } + + protected override bool ReleaseHandle() + { + Native.grpcsharp_auth_context_release(handle); + return true; + } + + private AuthProperty PtrToAuthProperty(IntPtr authPropertyPtr) + { + var nativeAuthProperty = (NativeAuthProperty) Marshal.PtrToStructure(authPropertyPtr, typeof(NativeAuthProperty)); + var name = Marshal.PtrToStringAnsi(nativeAuthProperty.Name); + var valueBytes = new byte[(int) nativeAuthProperty.ValueLength]; + Marshal.Copy(nativeAuthProperty.Value, valueBytes, 0, (int)nativeAuthProperty.ValueLength); + return AuthProperty.CreateUnsafe(name, valueBytes); + } + + /// <summary> + /// grpc_auth_property + /// </summary> + internal struct NativeAuthProperty + { + public IntPtr Name; + public IntPtr Value; + public UIntPtr ValueLength; + } + + /// <summary> + /// grpc_auth_property_iterator + /// </summary> + internal struct NativeAuthPropertyIterator + { + public IntPtr AuthContext; + public UIntPtr Index; + public IntPtr Name; + } + } +} diff --git a/src/csharp/Grpc.Core/Internal/BatchContextSafeHandle.cs b/src/csharp/Grpc.Core/Internal/BatchContextSafeHandle.cs index efae149f09818f1b1bdca2b0b9759f18d31f776a..6dee6d8c352436432c140a7aeec7b85a15d0f159 100644 --- a/src/csharp/Grpc.Core/Internal/BatchContextSafeHandle.cs +++ b/src/csharp/Grpc.Core/Internal/BatchContextSafeHandle.cs @@ -43,7 +43,6 @@ namespace Grpc.Core.Internal /// </summary> internal class BatchContextSafeHandle : SafeHandleZeroIsInvalid { - static readonly Encoding EncodingUTF8 = System.Text.Encoding.UTF8; static readonly NativeMethods Native = NativeMethods.Get(); private BatchContextSafeHandle() @@ -75,7 +74,7 @@ namespace Grpc.Core.Internal { UIntPtr detailsLength; IntPtr detailsPtr = Native.grpcsharp_batch_context_recv_status_on_client_details(this, out detailsLength); - string details = PtrToStringUtf8(detailsPtr, (int) detailsLength.ToUInt32()); + string details = MarshalUtils.PtrToStringUTF8(detailsPtr, (int) detailsLength.ToUInt32()); var status = new Status(Native.grpcsharp_batch_context_recv_status_on_client_status(this), details); IntPtr metadataArrayPtr = Native.grpcsharp_batch_context_recv_status_on_client_trailing_metadata(this); @@ -108,12 +107,5 @@ namespace Grpc.Core.Internal Native.grpcsharp_batch_context_destroy(handle); return true; } - - string PtrToStringUtf8(IntPtr ptr, int len) - { - var bytes = new byte[len]; - Marshal.Copy(ptr, bytes, 0, len); - return EncodingUTF8.GetString(bytes); - } } } diff --git a/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs b/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs index 710ca480e88b7f2362eb4018d82b7548eeae1e9a..3c368fbc6cd9e132ab152efc894276862ee9604a 100644 --- a/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs +++ b/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs @@ -45,7 +45,6 @@ namespace Grpc.Core.Internal internal class CallSafeHandle : SafeHandleZeroIsInvalid, INativeCall { public static readonly CallSafeHandle NullInstance = new CallSafeHandle(); - static readonly Encoding EncodingUTF8 = System.Text.Encoding.UTF8; static readonly NativeMethods Native = NativeMethods.Get(); const uint GRPC_WRITE_BUFFER_HINT = 1; @@ -140,7 +139,7 @@ namespace Grpc.Core.Internal var ctx = BatchContextSafeHandle.Create(); var optionalPayloadLength = optionalPayload != null ? new UIntPtr((ulong)optionalPayload.Length) : UIntPtr.Zero; completionQueue.CompletionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success)); - var statusDetailBytes = EncodingUTF8.GetBytes(status.Detail); + var statusDetailBytes = MarshalUtils.GetBytesUTF8(status.Detail); Native.grpcsharp_call_send_status_from_server(this, ctx, status.StatusCode, statusDetailBytes, new UIntPtr((ulong)statusDetailBytes.Length), metadataArray, sendEmptyInitialMetadata, optionalPayload, optionalPayloadLength, writeFlags).CheckOk(); } @@ -204,6 +203,11 @@ namespace Grpc.Core.Internal } } + public AuthContextSafeHandle GetAuthContext() + { + return Native.grpcsharp_call_auth_context(this); + } + protected override bool ReleaseHandle() { Native.grpcsharp_call_destroy(handle); diff --git a/src/csharp/Grpc.Core/Internal/MarshalUtils.cs b/src/csharp/Grpc.Core/Internal/MarshalUtils.cs new file mode 100644 index 0000000000000000000000000000000000000000..897e2f70bb47d65ba75de419107dda193f843b13 --- /dev/null +++ b/src/csharp/Grpc.Core/Internal/MarshalUtils.cs @@ -0,0 +1,90 @@ +#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.Text; + +namespace Grpc.Core.Internal +{ + /// <summary> + /// Useful methods for native/managed marshalling. + /// </summary> + internal static class MarshalUtils + { + static readonly Encoding EncodingUTF8 = System.Text.Encoding.UTF8; + static readonly Encoding EncodingASCII = System.Text.Encoding.ASCII; + + /// <summary> + /// Converts <c>IntPtr</c> pointing to a UTF-8 encoded byte array to <c>string</c>. + /// </summary> + public static string PtrToStringUTF8(IntPtr ptr, int len) + { + var bytes = new byte[len]; + Marshal.Copy(ptr, bytes, 0, len); + return EncodingUTF8.GetString(bytes); + } + + /// <summary> + /// Returns byte array containing UTF-8 encoding of given string. + /// </summary> + public static byte[] GetBytesUTF8(string str) + { + return EncodingUTF8.GetBytes(str); + } + + /// <summary> + /// Get string from a UTF8 encoded byte array. + /// </summary> + public static string GetStringUTF8(byte[] bytes) + { + return EncodingUTF8.GetString(bytes); + } + + /// <summary> + /// Returns byte array containing ASCII encoding of given string. + /// </summary> + public static byte[] GetBytesASCII(string str) + { + return EncodingASCII.GetBytes(str); + } + + /// <summary> + /// Get string from an ASCII encoded byte array. + /// </summary> + public static string GetStringASCII(byte[] bytes) + { + return EncodingASCII.GetString(bytes); + } + } +} diff --git a/src/csharp/Grpc.Core/Internal/NativeMethods.cs b/src/csharp/Grpc.Core/Internal/NativeMethods.cs index aff9550e8d24b6612be00815f025e4a3d18e489c..dd65f0521725a11105c61833e1797dd9ddd764ff 100644 --- a/src/csharp/Grpc.Core/Internal/NativeMethods.cs +++ b/src/csharp/Grpc.Core/Internal/NativeMethods.cs @@ -148,6 +148,12 @@ namespace Grpc.Core.Internal public readonly Delegates.grpcsharp_server_shutdown_and_notify_callback_delegate grpcsharp_server_shutdown_and_notify_callback; public readonly Delegates.grpcsharp_server_destroy_delegate grpcsharp_server_destroy; + public readonly Delegates.grpcsharp_call_auth_context_delegate grpcsharp_call_auth_context; + public readonly Delegates.grpcsharp_auth_context_peer_identity_property_name_delegate grpcsharp_auth_context_peer_identity_property_name; + public readonly Delegates.grpcsharp_auth_context_property_iterator_delegate grpcsharp_auth_context_property_iterator; + public readonly Delegates.grpcsharp_auth_property_iterator_next_delegate grpcsharp_auth_property_iterator_next; + public readonly Delegates.grpcsharp_auth_context_release_delegate grpcsharp_auth_context_release; + public readonly Delegates.gprsharp_now_delegate gprsharp_now; public readonly Delegates.gprsharp_inf_future_delegate gprsharp_inf_future; public readonly Delegates.gprsharp_inf_past_delegate gprsharp_inf_past; @@ -256,6 +262,12 @@ namespace Grpc.Core.Internal this.grpcsharp_server_shutdown_and_notify_callback = GetMethodDelegate<Delegates.grpcsharp_server_shutdown_and_notify_callback_delegate>(library); this.grpcsharp_server_destroy = GetMethodDelegate<Delegates.grpcsharp_server_destroy_delegate>(library); + this.grpcsharp_call_auth_context = GetMethodDelegate<Delegates.grpcsharp_call_auth_context_delegate>(library); + this.grpcsharp_auth_context_peer_identity_property_name = GetMethodDelegate<Delegates.grpcsharp_auth_context_peer_identity_property_name_delegate>(library); + this.grpcsharp_auth_context_property_iterator = GetMethodDelegate<Delegates.grpcsharp_auth_context_property_iterator_delegate>(library); + this.grpcsharp_auth_property_iterator_next = GetMethodDelegate<Delegates.grpcsharp_auth_property_iterator_next_delegate>(library); + this.grpcsharp_auth_context_release = GetMethodDelegate<Delegates.grpcsharp_auth_context_release_delegate>(library); + this.gprsharp_now = GetMethodDelegate<Delegates.gprsharp_now_delegate>(library); this.gprsharp_inf_future = GetMethodDelegate<Delegates.gprsharp_inf_future_delegate>(library); this.gprsharp_inf_past = GetMethodDelegate<Delegates.gprsharp_inf_past_delegate>(library); @@ -404,6 +416,12 @@ namespace Grpc.Core.Internal public delegate void grpcsharp_server_shutdown_and_notify_callback_delegate(ServerSafeHandle server, CompletionQueueSafeHandle cq, BatchContextSafeHandle ctx); public delegate void grpcsharp_server_destroy_delegate(IntPtr server); + public delegate AuthContextSafeHandle grpcsharp_call_auth_context_delegate(CallSafeHandle call); + public delegate IntPtr grpcsharp_auth_context_peer_identity_property_name_delegate(AuthContextSafeHandle authContext); // returns const char* + public delegate AuthContextSafeHandle.NativeAuthPropertyIterator grpcsharp_auth_context_property_iterator_delegate(AuthContextSafeHandle authContext); + public delegate IntPtr grpcsharp_auth_property_iterator_next_delegate(ref AuthContextSafeHandle.NativeAuthPropertyIterator iterator); // returns const auth_property* + public delegate void grpcsharp_auth_context_release_delegate(IntPtr authContext); + public delegate Timespec gprsharp_now_delegate(ClockType clockType); public delegate Timespec gprsharp_inf_future_delegate(ClockType clockType); public delegate Timespec gprsharp_inf_past_delegate(ClockType clockType); diff --git a/src/csharp/Grpc.Core/Metadata.cs b/src/csharp/Grpc.Core/Metadata.cs index 6fc715d6ee1a8c75a935044f194faf25bd7ba476..6e195b49ccd5dc7ab6855e12145c41a8e036f673 100644 --- a/src/csharp/Grpc.Core/Metadata.cs +++ b/src/csharp/Grpc.Core/Metadata.cs @@ -32,12 +32,10 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; +using Grpc.Core.Internal; using Grpc.Core.Utils; namespace Grpc.Core @@ -242,7 +240,6 @@ namespace Grpc.Core /// </summary> public class Entry { - private static readonly Encoding Encoding = Encoding.ASCII; private static readonly Regex ValidKeyRegex = new Regex("^[a-z0-9_-]+$"); readonly string key; @@ -306,7 +303,7 @@ namespace Grpc.Core { if (valueBytes == null) { - return Encoding.GetBytes(value); + return MarshalUtils.GetBytesASCII(value); } // defensive copy to guarantee immutability @@ -324,7 +321,7 @@ namespace Grpc.Core get { GrpcPreconditions.CheckState(!IsBinary, "Cannot access string value of a binary metadata entry"); - return value ?? Encoding.GetString(valueBytes); + return value ?? MarshalUtils.GetStringASCII(valueBytes); } } @@ -358,7 +355,7 @@ namespace Grpc.Core /// </summary> internal byte[] GetSerializedValueUnsafe() { - return valueBytes ?? Encoding.GetBytes(value); + return valueBytes ?? MarshalUtils.GetBytesASCII(value); } /// <summary> @@ -371,7 +368,7 @@ namespace Grpc.Core { return new Entry(key, null, valueBytes); } - return new Entry(key, Encoding.GetString(valueBytes), null); + return new Entry(key, MarshalUtils.GetStringASCII(valueBytes), null); } private static string NormalizeKey(string key) diff --git a/src/csharp/Grpc.Core/ServerCallContext.cs b/src/csharp/Grpc.Core/ServerCallContext.cs index 8f28fbc045339a2ddc59d8529cc25e4722443423..c8950b7677930782e7ab5e63136df96f6fe44a2a 100644 --- a/src/csharp/Grpc.Core/ServerCallContext.cs +++ b/src/csharp/Grpc.Core/ServerCallContext.cs @@ -32,7 +32,6 @@ #endregion using System; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -56,6 +55,7 @@ namespace Grpc.Core private Status status = Status.DefaultSuccess; private Func<Metadata, Task> writeHeadersFunc; private IHasWriteOptions writeOptionsHolder; + private Lazy<AuthContext> authContext; internal ServerCallContext(CallSafeHandle callHandle, string method, string host, DateTime deadline, Metadata requestHeaders, CancellationToken cancellationToken, Func<Metadata, Task> writeHeadersFunc, IHasWriteOptions writeOptionsHolder) @@ -68,6 +68,7 @@ namespace Grpc.Core this.cancellationToken = cancellationToken; this.writeHeadersFunc = writeHeadersFunc; this.writeOptionsHolder = writeOptionsHolder; + this.authContext = new Lazy<AuthContext>(GetAuthContextEager); } /// <summary> @@ -187,6 +188,26 @@ namespace Grpc.Core writeOptionsHolder.WriteOptions = value; } } + + /// <summary> + /// Gets the <c>AuthContext</c> associated with this call. + /// Note: Access to AuthContext is an experimental API that can change without any prior notice. + /// </summary> + public AuthContext AuthContext + { + get + { + return authContext.Value; + } + } + + private AuthContext GetAuthContextEager() + { + using (var authContextNative = callHandle.GetAuthContext()) + { + return authContextNative.ToAuthContext(); + } + } } /// <summary> diff --git a/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs b/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs index f85e272711f96dd8e6b921b149bf0cfc7fbdc5c2..377dad63f400abc3723b8a405a797586eb457776 100644 --- a/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs +++ b/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs @@ -37,6 +37,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Google.Protobuf; using Grpc.Core; using Grpc.Core.Utils; using Grpc.Testing; @@ -68,7 +69,7 @@ namespace Grpc.IntegrationTesting server = new Server { - Services = { TestService.BindService(new TestServiceImpl()) }, + Services = { TestService.BindService(new SslCredentialsTestServiceImpl()) }, Ports = { { Host, ServerPort.PickUnused, serverCredentials } } }; server.Start(); @@ -95,5 +96,40 @@ namespace Grpc.IntegrationTesting var response = client.UnaryCall(new SimpleRequest { ResponseSize = 10 }); Assert.AreEqual(10, response.Payload.Body.Length); } + + [Test] + public async Task AuthContextIsPopulated() + { + var call = client.StreamingInputCall(); + await call.RequestStream.CompleteAsync(); + var response = await call.ResponseAsync; + Assert.AreEqual(12345, response.AggregatedPayloadSize); + } + + private class SslCredentialsTestServiceImpl : TestService.TestServiceBase + { + public override async Task<SimpleResponse> UnaryCall(SimpleRequest request, ServerCallContext context) + { + return new SimpleResponse { Payload = CreateZerosPayload(request.ResponseSize) }; + } + + public override async Task<StreamingInputCallResponse> StreamingInputCall(IAsyncStreamReader<StreamingInputCallRequest> requestStream, ServerCallContext context) + { + var authContext = context.AuthContext; + await requestStream.ForEachAsync(async request => {}); + + Assert.IsTrue(authContext.IsPeerAuthenticated); + Assert.AreEqual("x509_subject_alternative_name", authContext.PeerIdentityPropertyName); + Assert.IsTrue(authContext.PeerIdentity.Count() > 0); + Assert.AreEqual("ssl", authContext.FindPropertiesByName("transport_security_type").First().Value); + + return new StreamingInputCallResponse { AggregatedPayloadSize = 12345 }; + } + + private static Payload CreateZerosPayload(int size) + { + return new Payload { Body = ByteString.CopyFrom(new byte[size]) }; + } + } } } diff --git a/src/csharp/ext/grpc_csharp_ext.c b/src/csharp/ext/grpc_csharp_ext.c index 6a241190b274c0e957ecbd0c3b43c1e34ade1b91..491df4de6adf6facec2112b6d9fdcca28100d485 100644 --- a/src/csharp/ext/grpc_csharp_ext.c +++ b/src/csharp/ext/grpc_csharp_ext.c @@ -1023,6 +1023,31 @@ GPR_EXPORT grpc_call_credentials *GPR_CALLTYPE grpcsharp_metadata_credentials_cr return grpc_metadata_credentials_create_from_plugin(plugin, NULL); } +/* Auth context */ + +GPR_EXPORT grpc_auth_context *GPR_CALLTYPE grpcsharp_call_auth_context(grpc_call *call) { + return grpc_call_auth_context(call); +} + +GPR_EXPORT const char *GPR_CALLTYPE grpcsharp_auth_context_peer_identity_property_name( + const grpc_auth_context *ctx) { + return grpc_auth_context_peer_identity_property_name(ctx); +} + +GPR_EXPORT grpc_auth_property_iterator GPR_CALLTYPE +grpcsharp_auth_context_property_iterator(const grpc_auth_context *ctx) { + return grpc_auth_context_property_iterator(ctx); +} + +GPR_EXPORT const grpc_auth_property *GPR_CALLTYPE grpcsharp_auth_property_iterator_next( + grpc_auth_property_iterator *it) { + return grpc_auth_property_iterator_next(it); +} + +GPR_EXPORT void GPR_CALLTYPE grpcsharp_auth_context_release(grpc_auth_context *ctx) { + grpc_auth_context_release(ctx); +} + /* Logging */ typedef void(GPR_CALLTYPE *grpcsharp_log_func)(const char *file, int32_t line, diff --git a/src/csharp/tests.json b/src/csharp/tests.json index 4ce6769eee5f61e74c2d80bc2581e159783bac5e..63f2e97f2dbb11061a2906df8c5cca072e40a5db 100644 --- a/src/csharp/tests.json +++ b/src/csharp/tests.json @@ -8,6 +8,8 @@ "Grpc.Core.Internal.Tests.MetadataArraySafeHandleTest", "Grpc.Core.Internal.Tests.TimespecTest", "Grpc.Core.Tests.AppDomainUnloadTest", + "Grpc.Core.Tests.AuthContextTest", + "Grpc.Core.Tests.AuthPropertyTest", "Grpc.Core.Tests.CallCredentialsTest", "Grpc.Core.Tests.CallOptionsTest", "Grpc.Core.Tests.ChannelCredentialsTest",