diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 41a91b5a25..c8774659b6 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -91,11 +91,6 @@ namespace Tgstation.Server.Host.Components /// readonly IJobManager jobManager; - /// - /// The for the - /// - readonly ICredentialsProvider credentialsProvider; - /// /// The for the /// @@ -111,6 +106,11 @@ namespace Tgstation.Server.Host.Components /// readonly IPlatformIdentifier platformIdentifier; + /// + /// The for the . + /// + readonly IRepositoryFactory repositoryFactory; + /// /// Construct an /// @@ -128,10 +128,10 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of /// The value of /// The value of /// The value of + /// The value of . public InstanceFactory( IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, @@ -147,10 +147,10 @@ namespace Tgstation.Server.Host.Components IPostWriteHandler postWriteHandler, IWatchdogFactory watchdogFactory, IJobManager jobManager, - ICredentialsProvider credentialsProvider, INetworkPromptReaper networkPromptReaper, IGitHubClientFactory gitHubClientFactory, - IPlatformIdentifier platformIdentifier) + IPlatformIdentifier platformIdentifier, + IRepositoryFactory repositoryFactory) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); @@ -166,10 +166,10 @@ namespace Tgstation.Server.Host.Components this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); - this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider)); this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper)); this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); } /// @@ -187,7 +187,13 @@ namespace Tgstation.Server.Host.Components var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, postWriteHandler, platformIdentifier, loggerFactory.CreateLogger()); var eventConsumer = new EventConsumer(configuration); - var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager, eventConsumer, credentialsProvider, loggerFactory.CreateLogger(), loggerFactory.CreateLogger()); + var repoManager = new RepositoryManager( + repositoryFactory, + repoIoManager, + eventConsumer, + loggerFactory.CreateLogger(), + loggerFactory.CreateLogger(), + metadata.RepositorySettings); try { var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger()); @@ -244,12 +250,21 @@ namespace Tgstation.Server.Host.Components throw; } } - #pragma warning restore CA1506 +#pragma warning restore CA1506 /// - public Task StartAsync(CancellationToken cancellationToken) => byondInstaller.CleanCache(cancellationToken); + public Task StartAsync(CancellationToken cancellationToken) + { + CheckSystemCompatibility(); + return byondInstaller.CleanCache(cancellationToken); + } /// public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + /// Test that the is functional. + /// + private void CheckSystemCompatibility() => repositoryFactory.CreateInMemory().Dispose(); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 28fbc31558..07bd7aa148 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -10,6 +10,8 @@ using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components { @@ -46,6 +48,16 @@ namespace Tgstation.Server.Host.Components /// readonly IServerControl serverControl; + /// + /// The for the + /// + readonly IPlatformIdentifier platformIdentifier; + + /// + /// The for the + /// + readonly ISystemIdentityFactory systemIdentityFactory; + /// /// The for the /// @@ -75,8 +87,19 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of + /// The value of . + /// The value of . /// The value of - public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger logger) + public InstanceManager( + IInstanceFactory instanceFactory, + IIOManager ioManager, + IDatabaseContextFactory databaseContextFactory, + IApplication application, + IJobManager jobManager, + IServerControl serverControl, + IPlatformIdentifier platformIdentifier, + ISystemIdentityFactory systemIdentityFactory, + ILogger logger) { this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); @@ -84,6 +107,8 @@ namespace Tgstation.Server.Host.Components this.application = application ?? throw new ArgumentNullException(nameof(application)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); + this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); serverControl.RegisterForRestart(this); @@ -212,6 +237,7 @@ namespace Tgstation.Server.Host.Components { try { + CheckSystemCompatibility(); var factoryStartup = instanceFactory.StartAsync(cancellationToken); await databaseContext.Initialize(cancellationToken).ConfigureAwait(false); await jobManager.StartAsync(cancellationToken).ConfigureAwait(false); @@ -266,5 +292,15 @@ namespace Tgstation.Server.Host.Components downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null; return Task.CompletedTask; } + + /// + /// Check we have a valid system identity. + /// + private void CheckSystemCompatibility() + { + using (var systemIdentity = systemIdentityFactory.GetCurrent()) + if (!systemIdentity.CanCreateSymlinks) + throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!"); + } } } diff --git a/src/Tgstation.Server.Host/Components/Repository/CredentialsProvider.cs b/src/Tgstation.Server.Host/Components/Repository/CredentialsProvider.cs deleted file mode 100644 index 80112f3e47..0000000000 --- a/src/Tgstation.Server.Host/Components/Repository/CredentialsProvider.cs +++ /dev/null @@ -1,53 +0,0 @@ -using LibGit2Sharp; -using LibGit2Sharp.Handlers; -using Microsoft.Extensions.Logging; -using System; -using Tgstation.Server.Host.Jobs; - -namespace Tgstation.Server.Host.Components.Repository -{ - /// - sealed class CredentialsProvider : ICredentialsProvider - { - /// - /// The for the - /// - readonly ILogger logger; - - /// - /// Construct a - /// - /// The value of - public CredentialsProvider(ILogger logger) - { - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public CredentialsHandler GenerateHandler(string username, string password) => (a, b, supportedCredentialTypes) => - { - var hasCreds = username != null; - var supportsUserPass = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.UsernamePassword); - var supportsAnonymous = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.Default); - - logger.LogTrace("Credentials requested. Present: {0}. Supports anonymous: {1}. Supports user/pass: {2}", hasCreds, supportsAnonymous, supportsUserPass); - if (supportsUserPass) - { - if (hasCreds) - return new UsernamePasswordCredentials - { - Username = username, - Password = password - }; - } - - if (supportsAnonymous) - return new DefaultCredentials(); - - if (hasCreds) - throw new JobException("Remote does not support anonymous authentication!"); - - throw new JobException("Server does not support anonymous or username/password authentication!"); - }; - } -} diff --git a/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs b/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs index c130b02a78..496a015187 100644 --- a/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs +++ b/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs @@ -13,6 +13,6 @@ namespace Tgstation.Server.Host.Components.Repository /// The optional username to use in the /// The optional password to use in the /// A new - CredentialsHandler GenerateHandler(string username, string password); + CredentialsHandler GenerateCredentialsHandler(string username, string password); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/IRepositoryFactory.cs new file mode 100644 index 0000000000..1b9d7b8c41 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/IRepositoryFactory.cs @@ -0,0 +1,37 @@ +using LibGit2Sharp; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// Factory for creating s. + /// + interface IRepositoryFactory : ICredentialsProvider + { + /// + /// Create an in-memeory . + /// + /// A new in-memory . + LibGit2Sharp.IRepository CreateInMemory(); + + /// + /// Load a from a given . + /// + /// The full path to the . + /// The for the operation. + /// A resulting in, the loaded . + Task CreateFromPath(string path, CancellationToken cancellationToken); + + /// + /// Clone a remote . + /// + /// The of the remote. + /// The . + /// The full path to the cloned . + /// The for the operation. + /// A representing the running operation. + Task Clone(Uri url, CloneOptions cloneOptions, string path, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index dd64bcdb19..d180d541d7 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -175,7 +175,7 @@ namespace Tgstation.Server.Host.Components.Repository progressReporter(50 + ((int)(50 * ((float)sentBytes) / totalBytes))); return !cancellationToken.IsCancellationRequested; }, - CredentialsProvider = credentialsProvider.GenerateHandler(username, password) + CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password) }; /// @@ -262,7 +262,7 @@ namespace Tgstation.Server.Host.Components.Repository return !cancellationToken.IsCancellationRequested; }, OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, - CredentialsProvider = credentialsProvider.GenerateHandler(username, password) + CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password) }, logMessage); } catch (UserCancelledException) { } @@ -375,7 +375,7 @@ namespace Tgstation.Server.Host.Components.Repository return !cancellationToken.IsCancellationRequested; }, OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, - CredentialsProvider = credentialsProvider.GenerateHandler(username, password) + CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password) }, "Fetch origin commits"); } catch (UserCancelledException) diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryFactory.cs new file mode 100644 index 0000000000..69865ce950 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryFactory.cs @@ -0,0 +1,87 @@ +using LibGit2Sharp; +using LibGit2Sharp.Handlers; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Jobs; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + sealed class RepositoryFactory : IRepositoryFactory + { + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + public RepositoryFactory(ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public LibGit2Sharp.IRepository CreateInMemory() + { + logger.LogTrace("Creating in-memory repository..."); + return new LibGit2Sharp.Repository(); + } + + /// + public Task CreateFromPath(string path, CancellationToken cancellationToken) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + logger.LogTrace("Creating repostory at {0}...", path); + return Task.Factory.StartNew( + () => (LibGit2Sharp.IRepository)new LibGit2Sharp.Repository(path), + cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Current); + } + + /// + public Task Clone(Uri url, CloneOptions cloneOptions, string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => + { + try + { + logger.LogTrace("Cloning {0} into {1}...", url, path); + LibGit2Sharp.Repository.Clone(url.ToString(), path, cloneOptions); + } + catch (UserCancelledException ex) + { + logger.LogTrace("Suppressing clone cancellation exception: {0}", ex); + cancellationToken.ThrowIfCancellationRequested(); + } + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + + /// + public CredentialsHandler GenerateCredentialsHandler(string username, string password) => (a, b, supportedCredentialTypes) => + { + var hasCreds = username != null; + var supportsUserPass = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.UsernamePassword); + var supportsAnonymous = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.Default); + + logger.LogTrace("Credentials requested. Present: {0}. Supports anonymous: {1}. Supports user/pass: {2}", hasCreds, supportsAnonymous, supportsUserPass); + if (supportsUserPass && hasCreds) + return new UsernamePasswordCredentials + { + Username = username, + Password = password + }; + + if (supportsAnonymous) + return new DefaultCredentials(); + + if (!hasCreds) + throw new JobException("Remote does not support anonymous authentication!"); + + throw new JobException("Server does not support anonymous or username/password authentication!"); + }; + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 4feeb46897..6bcdaf40bc 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -18,6 +18,11 @@ namespace Tgstation.Server.Host.Components.Repository /// public bool CloneInProgress { get; private set; } + /// + /// The for the + /// + readonly IRepositoryFactory repositoryFactory; + /// /// The for the /// @@ -28,11 +33,6 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly IEventConsumer eventConsumer; - /// - /// The for the - /// - readonly ICredentialsProvider credentialsProvider; - /// /// The created s /// @@ -56,20 +56,26 @@ namespace Tgstation.Server.Host.Components.Repository /// /// Construct a /// - /// The value of + /// The value of . /// The value of /// The value of - /// The value of /// The value of /// The value of - public RepositoryManager(RepositorySettings repositorySettings, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger repositoryLogger, ILogger logger) + /// The value of + public RepositoryManager( + IRepositoryFactory repositoryFactory, + IIOManager ioManager, + IEventConsumer eventConsumer, + ILogger repositoryLogger, + ILogger logger, + RepositorySettings repositorySettings) { - this.repositorySettings = repositorySettings ?? throw new ArgumentNullException(nameof(repositorySettings)); + this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); - this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider)); this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.repositorySettings = repositorySettings ?? throw new ArgumentNullException(nameof(repositorySettings)); semaphore = new SemaphoreSlim(1); } @@ -89,7 +95,7 @@ namespace Tgstation.Server.Host.Components.Repository throw new ArgumentNullException(nameof(progressReporter)); logger.LogInformation("Begin clone {0} (Branch: {1})", url, initialBranch); - lock (this) + lock (semaphore) { if (CloneInProgress) throw new InvalidOperationException("The repository is already being cloned!"); @@ -104,30 +110,28 @@ namespace Tgstation.Server.Host.Components.Repository if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false)) try { - await Task.Factory.StartNew(() => + var cloneOptions = new CloneOptions { - string path = null; - try + OnProgress = (a) => !cancellationToken.IsCancellationRequested, + OnTransferProgress = (a) => { - path = LibGit2Sharp.Repository.Clone(url.ToString(), ioManager.ResolvePath("."), new CloneOptions - { - OnProgress = (a) => !cancellationToken.IsCancellationRequested, - OnTransferProgress = (a) => - { - var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2)); - progressReporter((int)percentage); - return !cancellationToken.IsCancellationRequested; - }, - RecurseSubmodules = true, - OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, - RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested, - BranchName = initialBranch, - CredentialsProvider = credentialsProvider.GenerateHandler(username, password) - }); - } - catch (UserCancelledException) { } - cancellationToken.ThrowIfCancellationRequested(); - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2)); + progressReporter((int)percentage); + return !cancellationToken.IsCancellationRequested; + }, + RecurseSubmodules = true, + OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, + RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested, + BranchName = initialBranch, + CredentialsProvider = repositoryFactory.GenerateCredentialsHandler(username, password) + }; + + await repositoryFactory.Clone( + url, + cloneOptions, + ioManager.ResolvePath("."), + cancellationToken) + .ConfigureAwait(false); } catch { @@ -164,40 +168,29 @@ namespace Tgstation.Server.Host.Components.Repository public async Task LoadRepository(CancellationToken cancellationToken) { logger.LogTrace("Begin LoadRepository..."); - lock (this) + lock (semaphore) if (CloneInProgress) throw new InvalidOperationException("The repository is being cloned!"); - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - LibGit2Sharp.Repository repo = null; - await Task.Factory.StartNew(() => - { + using (var context = await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) try { - logger.LogTrace("Creating LibGit2Sharp.Repository..."); - repo = new LibGit2Sharp.Repository(ioManager.ResolvePath(".")); + var repo = await repositoryFactory.CreateFromPath(ioManager.ResolvePath("."), cancellationToken).ConfigureAwait(false); + + if (repo == null) + return null; + + return new Repository(repo, ioManager, eventConsumer, repositoryFactory, repositoryLogger, () => + { + logger.LogTrace("Releasing semaphore due to Repository disposal..."); + semaphore.Release(); + }); } catch (RepositoryNotFoundException e) { logger.LogDebug("Repository not found!"); logger.LogTrace("Exception: {0}", e); + return null; } - catch - { - semaphore.Release(); - throw; - } - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); - if (repo == null) - { - semaphore.Release(); - return null; - } - - return new Repository(repo, ioManager, eventConsumer, credentialsProvider, repositoryLogger, () => - { - logger.LogTrace("Releasing semaphore due to Repository disposal..."); - semaphore.Release(); - }); } /// diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index bb372eae36..55021ccf9f 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -347,7 +347,7 @@ namespace Tgstation.Server.Host.Core }); // configure component services - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/Security/ISystemIdentity.cs b/src/Tgstation.Server.Host/Security/ISystemIdentity.cs index 9dc72788c8..bd880617bc 100644 --- a/src/Tgstation.Server.Host/Security/ISystemIdentity.cs +++ b/src/Tgstation.Server.Host/Security/ISystemIdentity.cs @@ -19,6 +19,11 @@ namespace Tgstation.Server.Host.Security /// string Username { get; } + /// + /// If this system identity has permissions to create symlinks. + /// + bool CanCreateSymlinks { get; } + /// /// Clone the creating another copy that must have called on it /// diff --git a/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs index 11ba06ebe7..06371c4fe6 100644 --- a/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs @@ -9,6 +9,12 @@ namespace Tgstation.Server.Host.Security /// public interface ISystemIdentityFactory { + /// + /// Retrieves a representing the user executing tgstation-server. + /// + /// A representing the user executing tgstation-server. + ISystemIdentity GetCurrent(); + /// /// Create a for a given /// diff --git a/src/Tgstation.Server.Host/Security/PosixSystemIdentity.cs b/src/Tgstation.Server.Host/Security/PosixSystemIdentity.cs new file mode 100644 index 0000000000..67f0e3ac83 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/PosixSystemIdentity.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Security +{ + /// + /// for POSIX systems. + /// + sealed class PosixSystemIdentity : ISystemIdentity + { + /// + public string Uid => throw new NotImplementedException(); + + /// + public string Username => throw new NotImplementedException(); + + /// + public bool CanCreateSymlinks => true; + + /// + public ISystemIdentity Clone() => throw new NotImplementedException(); + + /// + public void Dispose() + { + } + + /// + public Task RunImpersonated(Action action, CancellationToken cancellationToken) => throw new NotSupportedException(); + } +} diff --git a/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs index 3857b42467..c5a19348a5 100644 --- a/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs @@ -11,6 +11,9 @@ namespace Tgstation.Server.Host.Security /// TODO: Blocked by https://github.com/dotnet/corefx/issues/3187 sealed class PosixSystemIdentityFactory : ISystemIdentityFactory { + /// + public ISystemIdentity GetCurrent() => new PosixSystemIdentity(); + /// public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => throw new NotImplementedException(); diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs index 5125613971..aad76f2360 100644 --- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs +++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs @@ -17,6 +17,9 @@ namespace Tgstation.Server.Host.Security /// public string Username => userPrincipal?.Name ?? identity.Name; + /// + public bool CanCreateSymlinks => canCreateSymlinks ?? throw new NotSupportedException(); + /// /// The for the /// @@ -27,6 +30,11 @@ namespace Tgstation.Server.Host.Security /// readonly UserPrincipal userPrincipal; + /// + /// Backing field for . + /// + readonly bool? canCreateSymlinks; + /// /// Construct a using a /// @@ -34,6 +42,7 @@ namespace Tgstation.Server.Host.Security public WindowsSystemIdentity(WindowsIdentity identity) { this.identity = identity ?? throw new ArgumentNullException(nameof(identity)); + canCreateSymlinks = new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } /// diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs index 3ce60b3bbd..a3480ddda6 100644 --- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs @@ -41,6 +41,9 @@ namespace Tgstation.Server.Host.Security this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// + public ISystemIdentity GetCurrent() => new WindowsSystemIdentity(WindowsIdentity.GetCurrent()); + /// public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => Task.Factory.StartNew(() => { diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index a2fafe355d..a224e71881 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -15,9 +15,7 @@ using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host { /// -#pragma warning disable CA1001 // Types that own disposable fields should be disposable sealed class Server : IServer, IServerControl -#pragma warning restore CA1001 // Types that own disposable fields should be disposable { /// public bool RestartRequested { get; private set; } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs new file mode 100644 index 0000000000..8d34683556 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs @@ -0,0 +1,78 @@ +using LibGit2Sharp; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Repository.Tests +{ + /// + /// Tests for . + /// + [TestClass] + public sealed class TestRepositoryFactory + { + static IRepositoryFactory CreateFactory() => new RepositoryFactory(Mock.Of>()); + + static Task TestRepoLoading( + string path, + IRepositoryFactory repositoryFactory = null) => + (repositoryFactory ?? CreateFactory()) + .CreateFromPath(path, default); + + [TestMethod] + public void TestConstructionThrows() => Assert.ThrowsException(() => new RepositoryFactory(null)); + + [TestMethod] + public void TestInMemoryRepoCreation() + { + new RepositoryFactory(Mock.Of>()).CreateInMemory().Dispose(); + } + + [TestMethod] + public async Task TestCloning() + { + var tempDir = Path.GetTempFileName(); + File.Delete(tempDir); + try + { + var factory = CreateFactory(); + await factory.Clone( + new Uri("https://github.com/Cyberboss/Test"), + new CloneOptions + { + CredentialsProvider = factory.GenerateCredentialsHandler(null, null) + }, + tempDir, + default); + + using (var repo = await TestRepoLoading(tempDir)) + { + var gitObject = repo.Lookup("f636418bf47d238d33b0e4a34f0072b23a8aad0e"); + Assert.IsNotNull(gitObject); + var commit = gitObject.Peel(); + + Assert.AreEqual("Update Test.md", commit.Message); + } + } + finally + { + // Takes a while to release the repo handle sometimes... + for (var i = 0; i < 5; ++i) + { + try + { + Directory.Delete(tempDir, true); + break; + } + catch (UnauthorizedAccessException) + { + await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false); + } + } + } + } + } +}