diff --git a/src/Yavsc/AuthorizationHandlers/ViewFileHandler.cs b/src/Yavsc/AuthorizationHandlers/ViewFileHandler.cs index bd690cce..73293b14 100644 --- a/src/Yavsc/AuthorizationHandlers/ViewFileHandler.cs +++ b/src/Yavsc/AuthorizationHandlers/ViewFileHandler.cs @@ -1,29 +1,37 @@ using Microsoft.AspNet.Authorization; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using Yavsc.Services; using Yavsc.ViewModels.Auth; -namespace Yavsc.AuthorizationHandlers { +namespace Yavsc.AuthorizationHandlers +{ - public class ViewFileHandler : AuthorizationHandler { + public class ViewFileHandler : AuthorizationHandler + { readonly IFileSystemAuthManager _authManager; + private readonly ILogger _logger; - public ViewFileHandler (IFileSystemAuthManager authManager) { + public ViewFileHandler(IFileSystemAuthManager authManager, ILoggerFactory logFactory) + { _authManager = authManager; + _logger = logFactory.CreateLogger(); } - protected override void Handle (AuthorizationContext context, ViewRequirement requirement, ViewFileContext fileContext) { - // TODO file access rules - if (fileContext.Path.StartsWith ("/pub/")) - context.Succeed (requirement); - else { - if (!fileContext.Path.StartsWith ("/")) - context.Fail (); - else { - var rights = _authManager.GetFilePathAccess (context.User, fileContext.Path); - if ((rights & FileAccessRight.Read) > 0) - context.Succeed (requirement); - else context.Fail (); - } + protected override void Handle(AuthorizationContext context, ViewRequirement requirement, ViewFileContext fileContext) + { + + var rights = _authManager.GetFilePathAccess(context.User, fileContext.File); + _logger.LogInformation("Got access value : " + rights); + if ((rights & FileAccessRight.Read) > 0) + { + _logger.LogInformation("Allowing access"); + context.Succeed(requirement); + } + else + { + _logger.LogInformation("Denying access"); + context.Fail(); } } } diff --git a/src/Yavsc/Makefile b/src/Yavsc/Makefile index f7502c9b..60967cb9 100644 --- a/src/Yavsc/Makefile +++ b/src/Yavsc/Makefile @@ -11,7 +11,7 @@ BASERESX= Resources/Yavsc.Models.IT.Fixing.Resources.resx \ Resources/Yavsc.YavscLocalisation.resx BASERESXGEN=$(BASERESX:.resx=.Designer.cs) MCS_OPTIONS=-debug -MONO_OPTIONS="--debug" +MONO_OPTIONS=--debug include $(MAKEFILE_DIR)/dnx.mk include $(MAKEFILE_DIR)/versioning.mk diff --git a/src/Yavsc/Models/ApplicationDbContext.cs b/src/Yavsc/Models/ApplicationDbContext.cs index b3992ef1..487f10d4 100644 --- a/src/Yavsc/Models/ApplicationDbContext.cs +++ b/src/Yavsc/Models/ApplicationDbContext.cs @@ -289,6 +289,7 @@ namespace Yavsc.Models public DbSet ChatRoomAccess { get; set; } + [Obsolete("use pazof.rules from .access files")] public DbSet CircleAuthorizationToFile { get; set; } public DbSet InstrumentRating { get; set; } diff --git a/src/Yavsc/Services/FileSystemAuthManager.cs b/src/Yavsc/Services/FileSystemAuthManager.cs index 85c391b4..a2cd59d2 100644 --- a/src/Yavsc/Services/FileSystemAuthManager.cs +++ b/src/Yavsc/Services/FileSystemAuthManager.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.OptionsModel; using System.IO; using rules; using Microsoft.Data.Entity; +using Microsoft.AspNet.FileProviders; namespace Yavsc.Services { @@ -46,45 +47,38 @@ namespace Yavsc.Services _logger = loggerFactory.CreateLogger(); SiteSettings = sitesOptions.Value; aclfileName = SiteSettings.AccessListFileName; - ruleSetParser = new RuleSetParser(true); + ruleSetParser = new RuleSetParser(false); } - public FileAccessRight GetFilePathAccess(ClaimsPrincipal user, string normalizedFullPath) + public FileAccessRight GetFilePathAccess(ClaimsPrincipal user, IFileInfo file) { + var parts = file.PhysicalPath.Split(Path.DirectorySeparatorChar); + var cwd = Environment.CurrentDirectory.Split(Path.DirectorySeparatorChar).Length; - // Assert (normalizedFullPath!=null) - var parts = normalizedFullPath.Split('/'); - - // below 4 parts, no file name. - if (parts.Length < 4) return FileAccessRight.None; + // below 4 parts behind cwd, no file name. + if (parts.Length < cwd + 4) return FileAccessRight.None; var fileDir = string.Join("/", parts.Take(parts.Length - 1)); var fileName = parts[parts.Length - 1]; + var cusername = user.GetUserName(); - var firstFileNamePart = parts[3]; - if (firstFileNamePart == "pub" && aclfileName != fileName) - { - _logger.LogInformation("Serving public file."); - return FileAccessRight.Read; - } - if (user == null) return FileAccessRight.None; + if (string.IsNullOrEmpty(cusername)) return FileAccessRight.None; - var funame = parts[2]; - var cusername = user.GetUserName(); + var funame = parts[cwd+1]; if (funame == cusername) { - _logger.LogInformation("Serving file to owner."); return FileAccessRight.Read | FileAccessRight.Write; } - if (aclfileName == fileName) + + if (aclfileName == fileName) return FileAccessRight.None; - - _logger.LogInformation($"Access to {normalizedFullPath} for {cusername}"); ruleSetParser.Reset(); var cuserid = user.GetUserId(); - var fuserid = _dbContext.Users.Single(u => u.UserName == funame).Id; + + var fuserid = _dbContext.Users.SingleOrDefault(u => u.UserName == funame).Id; + if (string.IsNullOrEmpty(fuserid)) return FileAccessRight.None; var circles = _dbContext.Circle.Include(mb => mb.Members).Where(c => c.OwnerId == fuserid).ToArray(); foreach (var circle in circles) { @@ -93,31 +87,24 @@ namespace Yavsc.Services else ruleSetParser.Definitions.Add(circle.Name, Out); } - // _dbContext.Circle.Select(c => c.OwnerId == ) - for (int dirlevel = parts.Length - 1; dirlevel>0; dirlevel--) + for (int dirlevel = parts.Length - 1; dirlevel > cwd + 1; dirlevel--) { - var aclfi = new FileInfo(Path.Combine(Environment.CurrentDirectory, fileDir, aclfileName)); + fileDir = string.Join(Path.DirectorySeparatorChar.ToString(), parts.Take(dirlevel)); + + + var aclfin = Path.Combine(fileDir, aclfileName); + var aclfi = new FileInfo(aclfin); if (!aclfi.Exists) continue; ruleSetParser.ParseFile(aclfi.FullName); } // TODO default user scoped file access policy - - if (ruleSetParser.Rules.Allow(user.GetUserName())) - return FileAccessRight.Read; - - var ucl = user.Claims.Where(c => c.Type == YavscClaimTypes.CircleMembership).Select(c => long.Parse(c.Value)).Distinct().ToArray(); - var uclString = string.Join(",", ucl); - _logger.LogInformation($"{uclString} "); - foreach ( - var cid in ucl - ) + if (ruleSetParser.Rules.Allow(cusername)) { - var ok = _dbContext.CircleAuthorizationToFile.Any(a => a.CircleId == cid && a.FullPath == fileDir); - if (ok) return FileAccessRight.Read; + return FileAccessRight.Read; } + return FileAccessRight.None; - return FileAccessRight.None; } public string NormalizePath(string path) diff --git a/src/Yavsc/Services/IFileSystemAuthManager.cs b/src/Yavsc/Services/IFileSystemAuthManager.cs index 26cc3560..c550182f 100644 --- a/src/Yavsc/Services/IFileSystemAuthManager.cs +++ b/src/Yavsc/Services/IFileSystemAuthManager.cs @@ -1,6 +1,7 @@ using System; using System.Security.Claims; using System.Security.Principal; +using Microsoft.AspNet.FileProviders; namespace Yavsc.Services { @@ -23,9 +24,9 @@ namespace Yavsc.Services /// /// /// - FileAccessRight GetFilePathAccess(ClaimsPrincipal user, string normalizedFullPath); + FileAccessRight GetFilePathAccess(ClaimsPrincipal user, IFileInfo file); void SetAccess (long circleId, string normalizedFullPath, FileAccessRight access); } -} \ No newline at end of file +} diff --git a/src/Yavsc/Startup/SendFileWrapper.cs b/src/Yavsc/Startup/SendFileWrapper.cs new file mode 100644 index 00000000..538554fa --- /dev/null +++ b/src/Yavsc/Startup/SendFileWrapper.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Features; +using Microsoft.Extensions.Logging; + +namespace Yavsc +{ + + class YaSendFileWrapper : IHttpSendFileFeature + { + private readonly Stream _output; + + private readonly ILogger _logger; + + internal YaSendFileWrapper(Stream output, ILogger logger) + { + _output = output; + _logger = logger; + } + + public async Task SendFileAsync(string fileName, long offset, long? length, CancellationToken cancel) + { + cancel.ThrowIfCancellationRequested(); + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException("fileName"); + } + if (!File.Exists(fileName)) + { + throw new FileNotFoundException(string.Empty, fileName); + } + FileInfo fileInfo = new FileInfo(fileName); + if (offset < 0 || offset > fileInfo.Length) + { + throw new ArgumentOutOfRangeException("offset", offset, string.Empty); + } + if (length.HasValue && (length.Value < 0 || length.Value > fileInfo.Length - offset)) + { + throw new ArgumentOutOfRangeException("length", length, string.Empty); + } + FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, maxbufferlen, FileOptions.Asynchronous | FileOptions.SequentialScan); + try + { + fileStream.Seek(offset, SeekOrigin.Begin); + _logger.LogInformation(string.Format("Copying bytes range:{0},{1} filename:{2} ", offset, (!length.HasValue) ? null : (offset + length), fileName)); + // await CopyToAsync(fileStream, _output, length, cancel); + await CopyToAsync(fileStream, _output); + } + finally + { + fileStream.Dispose(); + } + } + + private const int maxbufferlen = 65536; + + private byte[] buffer = new byte[maxbufferlen]; + + private async Task CopyToAsync(FileStream fileStream, Stream output) + { + await Task.Run(()=> fileStream.CopyTo(output, maxbufferlen)); + } + } +} diff --git a/src/Yavsc/Startup/Startup.FileServer.cs b/src/Yavsc/Startup/Startup.FileServer.cs index 6499b77e..aec4ef57 100644 --- a/src/Yavsc/Startup/Startup.FileServer.cs +++ b/src/Yavsc/Startup/Startup.FileServer.cs @@ -1,10 +1,13 @@ +using System; using System.IO; using System.Security.Claims; +using System.Threading.Tasks; using Microsoft.AspNet.Authorization; using Microsoft.AspNet.Builder; using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.StaticFiles; using Microsoft.Extensions.Logging; using Yavsc.Helpers; @@ -13,18 +16,60 @@ using Yavsc.ViewModels.Auth; namespace Yavsc { + + public static class YaFileSServerExtenstions + { + public static IApplicationBuilder UseYaFileServer(this IApplicationBuilder builder, FileServerOptions options) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + if (options == null) + { + throw new ArgumentNullException("options"); + } + if (options == null) + { + throw new ArgumentNullException("options"); + } + if (options.EnableDefaultFiles) + { + builder = builder.UseDefaultFiles(options.DefaultFilesOptions); + } + if (options.EnableDirectoryBrowsing) + { + builder = builder.UseDirectoryBrowser(options.DirectoryBrowserOptions); + } + return builder.UseYaSendFileFallback().UseStaticFiles(options.StaticFileOptions); + } + + public static IApplicationBuilder UseYaSendFileFallback(this IApplicationBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + return builder.UseMiddleware(new object[0]); + } + } + public partial class Startup { - public static FileServerOptions UserFilesOptions { get; private set; } - public static FileServerOptions GitOptions { get; private set; } + public static FileServerOptions UserFilesOptions { get; private set; } + public static FileServerOptions GitOptions { get; private set; } + + public static FileServerOptions AvatarsOptions { get; set; } + + static IAuthorizationService AuthorizationService { get; set; } - public static FileServerOptions AvatarsOptions { get; set; } public void ConfigureFileServerApp(IApplicationBuilder app, SiteSettings siteSettings, IHostingEnvironment env, IAuthorizationService authorizationService) { - var userFilesDirInfo = new DirectoryInfo( siteSettings.Blog ); - AbstractFileSystemHelpers.UserFilesDirName = userFilesDirInfo.FullName; + AuthorizationService = authorizationService; + var userFilesDirInfo = new DirectoryInfo(siteSettings.Blog); + AbstractFileSystemHelpers.UserFilesDirName = userFilesDirInfo.FullName; if (!userFilesDirInfo.Exists) userFilesDirInfo.Create(); @@ -34,21 +79,11 @@ namespace Yavsc RequestPath = PathString.FromUriComponent(Constants.UserFilesPath), EnableDirectoryBrowsing = env.IsDevelopment(), }; - UserFilesOptions.EnableDefaultFiles=true; - UserFilesOptions.StaticFileOptions.ServeUnknownFileTypes=true; - - /* TODO needs a better design, at implementation time (don't use database, but in memory data) */ - UserFilesOptions.StaticFileOptions.OnPrepareResponse += async context => - { - var uname = context.Context.User?.GetUserName(); - var path = context.Context.Request.Path; - var result = await authorizationService.AuthorizeAsync(context.Context.User, new ViewFileContext - { UserName = uname, File = context.File, Path = path } , new ViewRequirement()); - if (!result) { - context.Context.Response.StatusCode = 403; - context.Context.Abort(); - } - }; + UserFilesOptions.EnableDefaultFiles = true; + UserFilesOptions.StaticFileOptions.ServeUnknownFileTypes = true; + + UserFilesOptions.StaticFileOptions.OnPrepareResponse = OnPrepareUserFileResponse; + var avatarsDirInfo = new DirectoryInfo(Startup.SiteSetup.Avatars); if (!avatarsDirInfo.Exists) avatarsDirInfo.Create(); AvatarsDirName = avatarsDirInfo.FullName; @@ -72,8 +107,8 @@ namespace Yavsc }; GitOptions.DefaultFilesOptions.DefaultFileNames.Add("index.md"); GitOptions.StaticFileOptions.ServeUnknownFileTypes = true; - _logger.LogInformation( $"{GitDirName}"); - GitOptions.StaticFileOptions.OnPrepareResponse+= OnPrepareGitRepoResponse; + _logger.LogInformation($"{GitDirName}"); + GitOptions.StaticFileOptions.OnPrepareResponse += OnPrepareGitRepoResponse; app.UseFileServer(UserFilesOptions); @@ -82,12 +117,28 @@ namespace Yavsc app.UseFileServer(GitOptions); app.UseStaticFiles(); } + + + private async void OnPrepareUserFileResponse(StaticFileResponseContext context) + { + var uname = context.Context.User?.GetUserName(); + var path = context.Context.Request.Path; + var result = await AuthorizationService.AuthorizeAsync(context.Context.User, + new ViewFileContext{ UserName = uname, File = context.File, Path = path }, new ViewRequirement()); + if (!result) + { + _logger.LogInformation("403"); + // TODO prettier + context.Context.Response.StatusCode = 403; + context.Context.Response.Redirect("/Home/Status/403"); + } + } static void OnPrepareGitRepoResponse(StaticFileResponseContext context) { if (context.File.Name.EndsWith(".ansi.log")) { - context.Context.Response.Redirect("/Git"+context.Context.Request.Path); + context.Context.Response.Redirect("/Git" + context.Context.Request.Path); } } } diff --git a/src/Yavsc/Startup/Startup.cs b/src/Yavsc/Startup/Startup.cs index f7c4b1aa..e6303b17 100755 --- a/src/Yavsc/Startup/Startup.cs +++ b/src/Yavsc/Startup/Startup.cs @@ -331,7 +331,7 @@ namespace Yavsc loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); _logger = loggerFactory.CreateLogger(); - app.UseStatusCodePagesWithReExecute("/Home/Status/{0}"); + if (env.IsDevelopment()) { @@ -410,6 +410,7 @@ namespace Yavsc ConfigureOAuthApp(app); ConfigureFileServerApp(app, SiteSetup, env, authorizationService); + app.UseRequestLocalization(localizationOptions.Value, (RequestCulture)new RequestCulture((string)"en-US")); ConfigureWorkflow(); @@ -498,6 +499,9 @@ namespace Yavsc } }); + + // FIXME + app.UseStatusCodePages(); CheckApp(env, loggerFactory); } diff --git a/src/Yavsc/Startup/YaSendFileMiddleware.cs b/src/Yavsc/Startup/YaSendFileMiddleware.cs new file mode 100644 index 00000000..467e5de5 --- /dev/null +++ b/src/Yavsc/Startup/YaSendFileMiddleware.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; +using Microsoft.Extensions.Logging; + +namespace Yavsc +{ + public class YaSendFileMiddleware + { + + + private readonly RequestDelegate _next; + + private readonly ILogger _logger; + + // + // Résumé : + // Creates a new instance of the SendFileMiddleware. + // + // Paramètres : + // next: + // The next middleware in the pipeline. + // + // loggerFactory: + // An Microsoft.Extensions.Logging.ILoggerFactory instance used to create loggers. + public YaSendFileMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) + { + if (next == null) + { + throw new ArgumentNullException("next"); + } + if (loggerFactory == null) + { + throw new ArgumentNullException("loggerFactory"); + } + _next = next; + _logger = loggerFactory.CreateLogger(); + } + + public Task Invoke(HttpContext context) + { + if (context.Response.StatusCode < 400 || context.Response.StatusCode >= 600 ) + { + if (context.Features.Get() == null) + { + context.Features.Set((IHttpSendFileFeature)new YaSendFileWrapper(context.Response.Body, _logger)); + } + } + return _next(context); + } + } +} diff --git a/src/Yavsc/project.json b/src/Yavsc/project.json index e10b84b9..c2eb1b17 100644 --- a/src/Yavsc/project.json +++ b/src/Yavsc/project.json @@ -153,7 +153,7 @@ "target": "project", "type": "build" }, - "pazof.rules": "1.1.1" + "pazof.rules": "1.1.3" }, "commands": { "ef": "EntityFramework.Commands",