diff --git a/TGServerService/App.config b/TGServerService/App.config index 26d868825c..601eff041c 100644 --- a/TGServerService/App.config +++ b/TGServerService/App.config @@ -67,6 +67,9 @@ localhost + + + diff --git a/TGServerService/AuthorizationManager.cs b/TGServerService/AuthorizationManager.cs new file mode 100644 index 0000000000..2e8cc2737c --- /dev/null +++ b/TGServerService/AuthorizationManager.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices.AccountManagement; +using System.Security.Principal; +using System.ServiceModel; +using System.ServiceModel.Dispatcher; +using TGServiceInterface; + +namespace TGServerService +{ + partial class TGStationServer : ServiceAuthorizationManager, ITGAdministration + { + SecurityIdentifier TheDroidsWereLookingFor; + + /// + public string GetCurrentAuthorizedGroup() + { + try + { + return TheDroidsWereLookingFor != null ? TheDroidsWereLookingFor.Translate(typeof(GroupPrincipal)).ToString() : "ADMIN"; + } + catch + { + return null; + } + } + + /// + public string SetAuthorizedGroup(string groupName) + { + if(groupName == null) + { + TheDroidsWereLookingFor = null; + var config = Properties.Settings.Default; + config.AuthorizedGroupName = null; + config.Save(); + return "ADMIN"; + } + return FindTheDroidsWereLookingFor(groupName); + } + + string FindTheDroidsWereLookingFor(string search = null) + { + //find the group that is authorized to use the tools + var pc = new PrincipalContext(ContextType.Machine); + var config = Properties.Settings.Default; + var groupName = search ?? config.AuthorizedGroupName; + if (String.IsNullOrWhiteSpace(groupName)) + return null; + var gp = GroupPrincipal.FindByIdentity(pc, search != null ? IdentityType.Name : IdentityType.Sid, groupName); + if (gp == null) + return null; + TheDroidsWereLookingFor = gp.Sid; + if (search != null) + { + config.AuthorizedGroupName = TheDroidsWereLookingFor.Value; + config.Save(); + } + return gp.Name; + } + protected override bool CheckAccessCore(OperationContext operationContext) + { + if (operationContext.EndpointDispatcher.ContractName == typeof(ITGConnectivity).Name) //always allow connectivity checks + return true; + + var windowsIdent = operationContext.ServiceSecurityContext.WindowsIdentity; + //first allow admins + var authSuccess = new WindowsPrincipal(windowsIdent).IsInRole(WindowsBuiltInRole.Administrator); + + //if we're not an admin, check that we aren't trying to access the admin interface + if (!authSuccess && operationContext.EndpointDispatcher.ContractName == typeof(ITGAdministration).Name) + //and allow those in the authorized group + authSuccess = (TheDroidsWereLookingFor != null || windowsIdent.Groups.Contains(TheDroidsWereLookingFor)); + + var actions = new List(); + try + { + var realfilter = (ActionMessageFilter)operationContext.EndpointDispatcher.ContractFilter; + var cnamespace = operationContext.EndpointDispatcher.ContractNamespace; + foreach (var I in realfilter.Actions) + actions.Add(I.Replace(cnamespace, "")); //filter out some garbage + } + catch (Exception e) + { + TGServerService.WriteError("IF YOU SEE THIS CALL CYBERBOSS: " + e.ToString(), TGServerService.EventID.Authentication); + } + TGServerService.WriteAccess(operationContext.ServiceSecurityContext.WindowsIdentity.Name, actions, authSuccess); + return authSuccess; + } + } +} diff --git a/TGServerService/InterfaceBase.cs b/TGServerService/InterfaceBase.cs index 21503fa982..64c42b0019 100644 --- a/TGServerService/InterfaceBase.cs +++ b/TGServerService/InterfaceBase.cs @@ -16,6 +16,7 @@ namespace TGServerService //called when the service is started public TGStationServer() { + FindTheDroidsWereLookingFor(); InitChat(); InitByond(); InitCompiler(); diff --git a/TGServerService/Properties/Settings.Designer.cs b/TGServerService/Properties/Settings.Designer.cs index f3db2a924e..6bbfbe6e30 100644 --- a/TGServerService/Properties/Settings.Designer.cs +++ b/TGServerService/Properties/Settings.Designer.cs @@ -250,5 +250,17 @@ namespace TGServerService.Properties { this["CertificateURL"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string AuthorizedGroupName { + get { + return ((string)(this["AuthorizedGroupName"])); + } + set { + this["AuthorizedGroupName"] = value; + } + } } } diff --git a/TGServerService/Properties/Settings.settings b/TGServerService/Properties/Settings.settings index 3a443fafea..ee0d0945b7 100644 --- a/TGServerService/Properties/Settings.settings +++ b/TGServerService/Properties/Settings.settings @@ -59,5 +59,8 @@ localhost + + + \ No newline at end of file diff --git a/TGServerService/ServerService.cs b/TGServerService/ServerService.cs index ef921fc2b3..bfafc579b9 100644 --- a/TGServerService/ServerService.cs +++ b/TGServerService/ServerService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Security.Cryptography.X509Certificates; @@ -13,6 +14,7 @@ namespace TGServerService //only deprecate events, do not reuse them public enum EventID { + Authentication = 1, ChatCommand = 100, ChatConnectFail = 200, ChatProviderStartFail = 300, @@ -95,6 +97,11 @@ namespace TGServerService ActiveService.EventLog.WriteEntry(message, EventLogEntryType.Warning, (int)id); } + public static void WriteAccess(string username, IList actions, bool authSuccess) + { + ActiveService.EventLog.WriteEntry(String.Format("Access from: {0}. Actions: {1}", username, String.Join(", ", actions)), authSuccess ? EventLogEntryType.SuccessAudit : EventLogEntryType.FailureAudit, (int)EventID.Authentication); + } + ServiceHost host; //the WCF host void MigrateSettings(int oldVersion, int newVersion) @@ -142,16 +149,18 @@ namespace TGServerService } Environment.CurrentDirectory = Config.ServerDirectory; - host = new ServiceHost(typeof(TGStationServer), new Uri[] { new Uri("net.pipe://localhost"), new Uri(String.Format("https://localhost:{0}", Server.HTTPSPort)) }) + var instance = new TGStationServer(); + + host = new ServiceHost(instance, new Uri[] { new Uri("net.pipe://localhost"), new Uri(String.Format("https://localhost:{0}", Server.HTTPSPort)) }) { CloseTimeout = new TimeSpan(0, 0, 5) - }; //construction runs here + }; foreach (var I in Server.ValidInterfaces) AddEndpoint(I); host.Credentials.ServiceCertificate.SetCertificate(StoreLocation.LocalMachine, StoreName.My, X509FindType.FindBySubjectName, Config.CertificateURL); - host.Credentials.UserNameAuthentication.IncludeWindowsGroups = true; + host.Authorization.ServiceAuthorizationManager = instance; host.Open(); //...or maybe here, doesn't really matter } diff --git a/TGServerService/TGServerService.csproj b/TGServerService/TGServerService.csproj index 5bd5b49e98..253a8dfc23 100644 --- a/TGServerService/TGServerService.csproj +++ b/TGServerService/TGServerService.csproj @@ -67,6 +67,7 @@ + @@ -83,6 +84,7 @@ + diff --git a/TGServiceInterface/Server.cs b/TGServiceInterface/Server.cs index 9056c42580..08e848e3f0 100644 --- a/TGServiceInterface/Server.cs +++ b/TGServiceInterface/Server.cs @@ -9,7 +9,7 @@ namespace TGServiceInterface /// /// List of types that can be used with GetComponen /// - public static readonly IList ValidInterfaces = new List { typeof(ITGByond), typeof(ITGChat), typeof(ITGCompiler), typeof(ITGConfig), typeof(ITGDreamDaemon), typeof(ITGRepository), typeof(ITGSService), typeof(ITGConnectivity) }; + public static readonly IList ValidInterfaces = new List { typeof(ITGByond), typeof(ITGChat), typeof(ITGCompiler), typeof(ITGConfig), typeof(ITGDreamDaemon), typeof(ITGRepository), typeof(ITGSService), typeof(ITGConnectivity), typeof(ITGAdministration) }; /// /// Base name of the communication pipe @@ -111,6 +111,7 @@ namespace TGServiceInterface /// /// As opposed to VerifyConnection(), this check user credentials + /// Requires a prior call to /// /// true if credentials are valid, false otherwise public static bool Authenticate() @@ -125,8 +126,29 @@ namespace TGServiceInterface return false; } } + + /// + /// As opposed to Authentication() this returns true if the current login can use the interface. + /// Requires a prior call to + /// + /// true if the connection may use the interface, false otherwise + public static bool AuthenticateAdmin() + { + try + { + GetComponent().GetCurrentAuthorizedGroup(); + return true; + } + catch + { + return false; + } + } } + /// + /// Used for testing connections to the service without authentication + /// [ServiceContract] public interface ITGConnectivity { @@ -137,6 +159,28 @@ namespace TGServiceInterface void VerifyConnection(); } + /// + /// Manage the group that is used to access the service, can only be used by an administrator + /// + [ServiceContract] + public interface ITGAdministration + { + /// + /// Returns the name of the windows group allowed to use the service other than administrator + /// + /// The name of the windows group allowed to use the service other than administrator, "ADMIN" if it's unset, null on failure + [OperationContract] + string GetCurrentAuthorizedGroup(); + + /// + /// Searches the windows machine for the group named , sets it as the authorized group if it's found + /// + /// The name of the group to search for or null to clear the setting + /// The full name of the group that is now authorized on success, null on failure, "ADMIN" on clearing + [OperationContract] + string SetAuthorizedGroup(string groupName); + } + /// /// Interface for managing the service ///