diff --git a/TGS.Interface/AuthenticationHeaderApplicator.cs b/TGS.Interface/AuthenticationHeaderApplicator.cs new file mode 100644 index 0000000000..c3b9f3cfe7 --- /dev/null +++ b/TGS.Interface/AuthenticationHeaderApplicator.cs @@ -0,0 +1,49 @@ +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Dispatcher; +using System.ServiceModel.Description; + +namespace TGS.Interface +{ + sealed class AuthenticationHeaderApplicator : IEndpointBehavior, IClientMessageInspector + { + readonly RemoteLoginInfo remoteLoginInfo; + + public AuthenticationHeaderApplicator(RemoteLoginInfo loginInfo) + { + remoteLoginInfo = loginInfo; + } + + public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) + { + clientRuntime.ClientMessageInspectors.Add(this); + } + + public object BeforeSendRequest(ref Message request, IClientChannel channel) + { + request.Headers.Add(MessageHeader.CreateHeader("Username", "http://tempuri.org", remoteLoginInfo.Username)); + request.Headers.Add(MessageHeader.CreateHeader("Password", "http://tempuri.org", remoteLoginInfo.Password)); + return null; + } + + public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) + { + //intentionally left blank + } + + public void AfterReceiveReply(ref Message reply, object correlationState) + { + //intentionally left blank + } + + public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) + { + //intentionally left blank + } + + public void Validate(ServiceEndpoint endpoint) + { + //intentionally left blank + } + } +} diff --git a/TGS.Interface/ServerInterface.cs b/TGS.Interface/ServerInterface.cs index 142e08357d..42c72d504c 100644 --- a/TGS.Interface/ServerInterface.cs +++ b/TGS.Interface/ServerInterface.cs @@ -393,24 +393,17 @@ namespace TGS.Interface } //okay we're going over - var binding = new WSHttpBinding() + var binding = new BasicHttpsBinding() { SendTimeout = new TimeSpan(0, 0, 40), MaxReceivedMessageSize = TransferLimitRemote }; var requireAuth = InterfaceName != typeof(ITGConnectivity).Name; - binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; - binding.Security.Mode = requireAuth ? SecurityMode.TransportWithMessageCredential : SecurityMode.Transport; //do not require auth for a connectivity check - binding.Security.Message.ClientCredentialType = requireAuth ? MessageCredentialType.UserName : MessageCredentialType.None; var url = String.Format("https://{0}:{1}/{2}/{3}", LoginInfo.IP, LoginInfo.Port, accessPath, InterfaceName); var address = new EndpointAddress(url); var res = new ChannelFactory(binding, address); if (requireAuth) - { - res.Credentials.UserName.UserName = LoginInfo.Username; - res.Credentials.UserName.Password = LoginInfo.Password; - res.Credentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation; - } + res.Endpoint.EndpointBehaviors.Add(new AuthenticationHeaderApplicator(LoginInfo)); return res; } diff --git a/TGS.Interface/TGS.Interface.csproj b/TGS.Interface/TGS.Interface.csproj index 7fcfc32cbb..bb494f14b9 100644 --- a/TGS.Interface/TGS.Interface.csproj +++ b/TGS.Interface/TGS.Interface.csproj @@ -47,6 +47,7 @@ + diff --git a/TGS.Server/Instance/Administration.cs b/TGS.Server/Instance/Administration.cs index 863f36f540..7d9193b387 100644 --- a/TGS.Server/Instance/Administration.cs +++ b/TGS.Server/Instance/Administration.cs @@ -5,6 +5,7 @@ using System.ServiceModel; using System.Threading; using TGS.Interface; using TGS.Interface.Components; +using TGS.Server.Security; namespace TGS.Server { diff --git a/TGS.Server/Instance/Config.cs b/TGS.Server/Instance/Config.cs index 57968b54cd..76d8fe92a0 100644 --- a/TGS.Server/Instance/Config.cs +++ b/TGS.Server/Instance/Config.cs @@ -15,9 +15,9 @@ namespace TGS.Server object configLock = new object(); //for atomic reads/writes /// - [OperationBehavior(Impersonation = ImpersonationOption.Required)] public string ReadText(string staticRelativePath, bool repo, out string error, out bool unauthorized) { + Server.BeginImpersonation(); string path = null; try { @@ -100,9 +100,9 @@ namespace TGS.Server } /// - [OperationBehavior(Impersonation = ImpersonationOption.Required)] public string WriteText(string staticRelativePath, string data, out bool unauthorized) { + Server.BeginImpersonation(); var path = RelativePath(StaticDirs) + '/' + staticRelativePath; //do not use path.combine or it will try and take the root try { @@ -152,9 +152,9 @@ namespace TGS.Server } } /// - [OperationBehavior(Impersonation = ImpersonationOption.Required)] public string DeleteFile(string staticRelativePath, out bool unauthorized) { + Server.BeginImpersonation(); var path = RelativePath(StaticDirs + '/' + staticRelativePath); //do not use path.combine or it will try and take the root try { @@ -207,9 +207,9 @@ namespace TGS.Server } /// - [OperationBehavior(Impersonation = ImpersonationOption.Required)] public IList ListStaticDirectory(string subDir, out string error, out bool unauthorized) { + Server.BeginImpersonation(); try { if (!Directory.Exists(RelativePath(StaticDirs))) diff --git a/TGS.Server/Security/AuthenticationHeaderDecoder.cs b/TGS.Server/Security/AuthenticationHeaderDecoder.cs new file mode 100644 index 0000000000..3daf822f69 --- /dev/null +++ b/TGS.Server/Security/AuthenticationHeaderDecoder.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.IdentityModel.Claims; +using System.IdentityModel.Policy; +using System.Runtime.InteropServices; +using System.ServiceModel; +using System.ServiceModel.Channels; + +namespace TGS.Server.Security +{ + sealed class AuthenticationHeaderDecoder : ServiceAuthenticationManager + { + [DllImport("advapi32.dll", SetLastError = true)] + static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken); + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool CloseHandle(IntPtr handle); + + public ClaimSet Issuer => throw new NotImplementedException(); + + public string Id => throw new NotImplementedException(); + + public override ReadOnlyCollection Authenticate(ReadOnlyCollection authPolicy, Uri listenUri, ref Message message) + { + try + { + var userPosition = message.Headers.FindHeader("Username", "http://tempuri.org"); + var passPosition = message.Headers.FindHeader("Password", "http://tempuri.org"); + + if (userPosition != -1 && passPosition != -1) + { + + var user = message.Headers.GetHeader(userPosition); + var pass = message.Headers.GetHeader(passPosition); + + var splits = user.Split('\\'); + + var res = LogonUser(splits.Length > 1 ? splits[1] : splits[0], splits.Length > 1 ? splits[0] : null, pass, 3 /*LOGON32_LOGON_NETWORK*/, 0 /*LOGON32_PROVIDER_DEFAULT*/, out IntPtr token); + if (res) + { + //IMPORTANT: logoff the user after all is said and done or they'll stay logged in on the system for as long as we are + OperationContext.Current.OperationCompleted += (a, b) => { CloseHandle(token); }; + return new ReadOnlyCollection(new List { new WindowsAuthorizationPolicy(token) }); + } + } + } + catch { } + return authPolicy; + } + } +} diff --git a/TGS.Server/RootAuthorizationManager.cs b/TGS.Server/Security/RootAuthorizationManager.cs similarity index 97% rename from TGS.Server/RootAuthorizationManager.cs rename to TGS.Server/Security/RootAuthorizationManager.cs index ed81226473..809e87078c 100644 --- a/TGS.Server/RootAuthorizationManager.cs +++ b/TGS.Server/Security/RootAuthorizationManager.cs @@ -5,7 +5,7 @@ using System.Security.Principal; using System.ServiceModel; using TGS.Interface.Components; -namespace TGS.Server +namespace TGS.Server.Security { /// /// A used to determine only if the caller is an admin diff --git a/TGS.Server/Security/WindowsAuthorizationPolicy.cs b/TGS.Server/Security/WindowsAuthorizationPolicy.cs new file mode 100644 index 0000000000..5e5dd17558 --- /dev/null +++ b/TGS.Server/Security/WindowsAuthorizationPolicy.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Claims; +using System.IdentityModel.Policy; +using System.Security.Principal; + +namespace TGS.Server.Security +{ + sealed class WindowsAuthorizationPolicy : IAuthorizationPolicy + { + readonly WindowsIdentity identity; + public WindowsAuthorizationPolicy(IntPtr identityToken) + { + identity = new WindowsIdentity(identityToken); + } + public ClaimSet Issuer => throw new NotImplementedException(); + + public string Id => throw new NotImplementedException(); + + public bool Evaluate(EvaluationContext evaluationContext, ref object state) + { + evaluationContext.Properties["Identities"] = new List { identity }; + return true; + } + } +} diff --git a/TGS.Server/Server.cs b/TGS.Server/Server.cs index c2fe53a342..20161105dc 100644 --- a/TGS.Server/Server.cs +++ b/TGS.Server/Server.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; using System.IO; using System.Reflection; using System.Security.Principal; using System.ServiceModel; using TGS.Interface; using TGS.Interface.Components; +using TGS.Server.Security; namespace TGS.Server { @@ -46,6 +45,14 @@ namespace TGS.Server /// The directory to load and save s to /// static readonly string DefaultConfigDirectory = Directory.CreateDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TGS.Server")).FullName; + + /// + /// Begins user impersonation to allow proper restricted file access + /// + public static void BeginImpersonation() + { + WindowsIdentity.Impersonate(OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Token); + } /// /// Cancels WCF's user impersonation to allow clean access to writing log files @@ -194,7 +201,8 @@ namespace TGS.Server serviceHost = CreateHost(this, ServerInterface.MasterInterfaceName); foreach (var I in ServerInterface.ValidServiceInterfaces) AddEndpoint(serviceHost, I); - serviceHost.Authorization.ServiceAuthorizationManager = new RootAuthorizationManager(); //only admins can diddle us + serviceHost.Authorization.ServiceAuthorizationManager = new RootAuthorizationManager(); //only admins can diddle us + serviceHost.Authentication.ServiceAuthenticationManager = new AuthenticationHeaderDecoder(); } /// @@ -312,6 +320,7 @@ namespace TGS.Server AddEndpoint(host, J); host.Authorization.ServiceAuthorizationManager = instance; + host.Authentication.ServiceAuthenticationManager = new AuthenticationHeaderDecoder(); return host; } @@ -324,15 +333,12 @@ namespace TGS.Server { var bindingName = typetype.Name; host.AddServiceEndpoint(typetype, new NetNamedPipeBinding() { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = ServerInterface.TransferLimitLocal }, bindingName); - var httpsBinding = new WSHttpBinding() + var httpsBinding = new BasicHttpsBinding() { SendTimeout = new TimeSpan(0, 0, 40), MaxReceivedMessageSize = ServerInterface.TransferLimitRemote }; var requireAuth = typetype.Name != typeof(ITGConnectivity).Name; - httpsBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; - httpsBinding.Security.Mode = requireAuth ? SecurityMode.TransportWithMessageCredential : SecurityMode.Transport; //do not require auth for a connectivity check - httpsBinding.Security.Message.ClientCredentialType = requireAuth ? MessageCredentialType.UserName : MessageCredentialType.None; host.AddServiceEndpoint(typetype, httpsBinding, bindingName); } diff --git a/TGS.Server/TGS.Server.csproj b/TGS.Server/TGS.Server.csproj index 349c09be39..83f970c8c1 100644 --- a/TGS.Server/TGS.Server.csproj +++ b/TGS.Server/TGS.Server.csproj @@ -71,6 +71,7 @@ + ..\packages\System.Interactive.Async.3.1.1\lib\net45\System.Interactive.Async.dll @@ -80,6 +81,7 @@ + @@ -91,7 +93,7 @@ - + @@ -111,6 +113,7 @@ +