Traeger-GmbH / release-server

An server application for managing your own release artifacts via a REST API.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Check filesystem permissions during startup

f-porter opened this issue · comments

In order to avoid errors during runtime that result from wrong filesystem permissions these permissions shall be checked during the server startup.

To keep the logic of checking the permissions inside the FsReleaseArtifactRepository class, one can implement the check inside its constructor and create the singleton instances of FsReleaseArtifactService and FsReleaseArtifactRepository during startup.

This can be encapsulated in an extension method on IServiceCollection:

namespace Microsoft.Extensions.DependencyInjection
{
    public static class ReleaseServerServiceCollectionExtensions
    {
        public static IServiceCollection AddFsReleaseArtifactService(this IServiceCollection services)
        {
            services.AddSingleton<IReleaseArtifactService>(serviceProvider =>
            {
                var releaseArtifactRepository = new FsReleaseArtifactRepository(
                        serviceProvider.GetRequiredService<Logging.ILogger<FsReleaseArtifactRepository>>(),
                        serviceProvider.GetRequiredService<IConfiguration>()
                    );
                return new FsReleaseArtifactService(
                        releaseArtifactRepository,
                        serviceProvider.GetRequiredService<Logging.ILogger<FsReleaseArtifactService>>()
                    );
            });
            return services;
        }
    }
}

To add the services on startup just services.AddReleaseArtifactService() would have to be called in ConfigureServices() method of class Startup

@f-porter do you have packages / functions in your mind, how to check the directory permissions?
Windows permissions are not the same like Linux.

A simple check could also be done as follows (pseudo code):

private void CheckPermissions(string directoryToCheck)
{
   File.Create(Path.Combine(directoryToCheck, "testFile"));
   File.ReadAllBytes(Path.Combine(directoryToCheck, "testFile"));
   File.Delete(Path.Combine(directoryToCheck, "testFile"));
}

So, you can test the read and write access permission for Windows & Linux in one function (if you don't have the permission -> an exception will be thrown & the application does not start).

What do you think about this proposal? Or do you know more convenient possibilities.

I think there is a more convenient way to do this. @dscharnagl do you have any ideas how to achieve this?

Unfortunately there is no "nice" .NET API to accomplish this task. Apart from that prototype I'd like to give you some advices regarding your "snippet":

  • "Check" is not part of a good naming approach, because it is too generic
  • Repeating string operations: String operations are heavy and have a performance impact
  • Repeating API calls: Path.Combine is called three times, which isn't necessary (re-use the parameter)
  • File.Create returns a FileStream which is never disposed of
  • File.ReadAllBytes unnecessarily allocates additional memory and creates another stream

Summary: I know it's just a "snippet", but at the edge the code would't work as expected.

an exception will be thrown & the application does not start

This is not a good approach, because of an application shall not driven by exceptions. Always prefer return values instead of exceptions. Exceptions shall avoided as often as possible. An exception shall only be thrown in case of an issue in your code / use of an API, such as invalid parameters.

We already had such a requirement in one of our products as well. Here you will find the implementation I'd like to recommend:

public static bool CanWriteDirectory(string path)
{
    bool writable = false;

    try {
        path = Path.GetFullPath(path);

        if (Directory.Exists(path)) {
            // Generate a file name to reduce the risk of a file that might already exist.
            path = Path.Combine(path, Guid.NewGuid().ToString("N") + "_test.file");

            // It is important that we use FileMode.CreateNew instead of Create; otherwise
            // we might overwrite an already existing file. Also, we need to use
            // FileShare.Delete() so that we can delete the file while we have opened it,
            // to ensure no other process can write data to that file between the time when
            // we close it and then delete it.
            using (var stream = new FileStream(
                    path,
                    FileMode.CreateNew,
                    FileAccess.ReadWrite,
                    FileShare.Delete)) {
                // Delete it before closing it, to ensure no other process can access it.
                File.Delete(path);
            }

            writable = true;
        }
    }
#pragma warning disable CA1031 // Do not catch general exception types
    catch {
        // Ignore.
    }
#pragma warning restore CA1031 // Do not catch general exception types

    return writable;
}

Instead of "killing" your application in case there a desired/required/configured directory is not writable you shall give the user of your application a more professional feedback using console outputs and/or logging. An application which crashes is by default not a quite kind application regarding its mediated user experience.

Thank you @dscharnagl ! :)

The snippet was only a draft and not meant as "productive" code. Also a simple exception throwing is not a final approach for an application. With this, I'm absolutely with you.

Your implementation snippet is exactly doing, what i meant (incl the productivity behaviour) :)

I will take this for the implementation.

@f-porter i implemented the check of the filesystem permission.

Your suggestion of the extension method of IServiceCollection didn't work. That's because if you add services as singelton, they will only then instanced, when you neet them (after firing a request).

Instead of this, i used a kind of "warmup" in the Configure function:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //Warmup the ReleaseArtifactRepository to check the file permissions before continuing
    app.ApplicationServices.GetService<IReleaseArtifactRepository>();
....
}

This kind of "warmup" instantiates the FsReleaseArtifactRepository, that runs the permission checks. For the permission check, i used the code snippet above (thanks @dscharnagl).

If the permissions does not fit, i actually log an error an terminate the application (process) like as follows:
System.Diagnostics.Process.GetCurrentProcess().Kill();

I don't like this kind of handling and don't have a good feling with it.
So, i have to ask you two:
Do you know a better way of terminating the application instead of the command above?

I also used Environment.Exit(-1);, but it hang during termination of the application.

I will wait for opening a PR until we clarified the application termination.

Thank you guys! ;)

I suppose that you misunderstood my proposal where to put the code for checking the permissions.
I would not check the permissions inside the constructor of FsReleaseArtifactRepository but in the extension method AddFsReleaseArtifactService() of IServiceCollection:

namespace Microsoft.Extensions.DependencyInjection
{
    public static class ReleaseServerServiceCollectionExtensions
    {
        public static IServiceCollection AddFsReleaseArtifactService(this IServiceCollection services)
        {

            //  
            // Check filesystem permissions here.
            //

            services.AddSingleton<IReleaseArtifactService>(serviceProvider =>
            {
                var releaseArtifactRepository = new FsReleaseArtifactRepository(
                        serviceProvider.GetRequiredService<Logging.ILogger<FsReleaseArtifactRepository>>(),
                        serviceProvider.GetRequiredService<IConfiguration>()
                    );
                return new FsReleaseArtifactService(
                        releaseArtifactRepository,
                        serviceProvider.GetRequiredService<Logging.ILogger<FsReleaseArtifactService>>()
                    );
            });
            return services;
        }
    }
}

Furthermore it is not good practice to just kill the running process from inside any component. It leads to unexpected behavior and is not predictable if the application is killed on different locations of the code.
Just log the error and throw a System.UnauthorizedAccessException if the permissions do not match the requirements. The callee then has the choice if he wants to handle this kind of exception.

As discussed with @f-porter the following changes were made:

  • CanWriteDirectory() is now a DirectoryInfo extension method
  • The permission check is now handled in the IServiceCollection extension method (see snippet above)
  • If the permissions are not set as supposed, the application throws a System.UnauthorizedException that will be catched in the main method.
  • Because of the missing ability to use a logger in the ConfigureServices() function (Startup.cs), we decided to log the Exception message in the catch block to the console.