Skip to content

Commit

Permalink
Added 'view' permissions for media folders. (#15173)
Browse files Browse the repository at this point in the history
Co-authored-by: Zoltán Lehóczky <zoltan.lehoczky@lombiq.com>
Co-authored-by: Hisham Bin Ateya <hishamco_2007@yahoo.com>
  • Loading branch information
3 people committed Apr 25, 2024
1 parent 2b30e68 commit 9cf1f78
Show file tree
Hide file tree
Showing 32 changed files with 1,116 additions and 23 deletions.
1 change: 1 addition & 0 deletions src/OrchardCore.Cms.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
//"OrchardCore_Media": {
// "SupportedSizes": [ 16, 32, 50, 100, 160, 240, 480, 600, 1024, 2048 ],
// "MaxBrowserCacheDays": 30,
// "MaxSecureFilesBrowserCacheDays": 0,
// "MaxCacheDays": 365,
// "ResizedCacheMaxStale": "01:00:00", // The time before a stale item is removed from the resized media cache, if not provided there is no cleanup.
// "RemoteCacheMaxStale": "01:00:00", // The time before a stale item is removed from the remote media cache, if not provided there is no cleanup.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ function initializeMediaApplication(displayMediaApplication, mediaApplicationUrl
name: $('#t-mediaLibrary').text(),
path: '',
folder: '',
isDirectory: true
isDirectory: true,
canCreateFolder: $('#allowNewRootFolders').val() === 'true'
};

mediaApp = new Vue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Vue.component('folder', {
<span v-on:click.stop="toggle" class="expand" :class="{opened: open, closed: !open, empty: empty}"><i v-if="open" class="fa-solid fa-chevron-${document.dir == "ltr" ? "right" : "left"}"></i></span>
<div class="folder-name ms-2">{{model.name}}</div>
<div class="btn-group folder-actions" >
<a v-cloak href="javascript:;" class="btn btn-sm" v-on:click="createFolder" v-if="isSelected || isRoot"><i class="fa-solid fa-plus" aria-hidden="true"></i></a>
<a v-cloak href="javascript:;" class="btn btn-sm" v-on:click="deleteFolder" v-if="isSelected && !isRoot"><i class="fa-solid fa-trash" aria-hidden="true"></i></a>
<a v-cloak href="javascript:;" class="btn btn-sm" v-on:click="createFolder" v-if="canCreateFolder && (isSelected || isRoot)"><i class="fa-solid fa-plus" aria-hidden="true"></i></a>
<a v-cloak href="javascript:;" class="btn btn-sm" v-on:click="deleteFolder" v-if="canDeleteFolder && isSelected && !isRoot"><i class="fa-solid fa-trash" aria-hidden="true"></i></a>
</div>
</a>
</div>
Expand Down Expand Up @@ -48,6 +48,12 @@ Vue.component('folder', {
},
isRoot: function () {
return this.model.path === '';
},
canCreateFolder: function () {
return this.model.canCreateFolder !== undefined ? this.model.canCreateFolder : true;
},
canDeleteFolder: function () {
return this.model.canDeleteFolder !== undefined ? this.model.canDeleteFolder : true;
}
},
mounted: function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class AdminController : Controller
private readonly IChunkFileUploadService _chunkFileUploadService;
private readonly IFileVersionProvider _fileVersionProvider;
private readonly IServiceProvider _serviceProvider;
private readonly AttachedMediaFieldFileService _attachedMediaFieldFileService;

public AdminController(
IMediaFileStore mediaFileStore,
Expand All @@ -48,7 +49,8 @@ public class AdminController : Controller
IUserAssetFolderNameProvider userAssetFolderNameProvider,
IChunkFileUploadService chunkFileUploadService,
IFileVersionProvider fileVersionProvider,
IServiceProvider serviceProvider)
IServiceProvider serviceProvider,
AttachedMediaFieldFileService attachedMediaFieldFileService)
{
_mediaFileStore = mediaFileStore;
_mediaNameNormalizerService = mediaNameNormalizerService;
Expand All @@ -61,6 +63,7 @@ public class AdminController : Controller
_chunkFileUploadService = chunkFileUploadService;
_fileVersionProvider = fileVersionProvider;
_serviceProvider = serviceProvider;
_attachedMediaFieldFileService = attachedMediaFieldFileService;
}

[Admin("Media", "Media.Index")]
Expand All @@ -74,7 +77,7 @@ public async Task<IActionResult> Index()
return View();
}

public async Task<ActionResult<IEnumerable<IFileStoreEntry>>> GetFolders(string path)
public async Task<ActionResult<IEnumerable<MediaFolderViewModel>>> GetFolders(string path)
{
if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia))
{
Expand All @@ -101,7 +104,21 @@ public async Task<ActionResult<IEnumerable<IFileStoreEntry>>> GetFolders(string
var allowed = _mediaFileStore.GetDirectoryContentAsync(path)
.WhereAwait(async e => e.IsDirectory && await _authorizationService.AuthorizeAsync(User, Permissions.ManageMediaFolder, (object)e.Path));

return Ok(await allowed.ToListAsync());
return Ok(await allowed.Select(folder =>
{
var isSpecial = IsSpecialFolder(folder.Path);
return new MediaFolderViewModel()
{
Name = folder.Name,
Path = folder.Path,
DirectoryPath = folder.DirectoryPath,
IsDirectory = true,
LastModifiedUtc = folder.LastModifiedUtc,
Length = folder.Length,
CanCreateFolder = !isSpecial,
CanDeleteFolder = !isSpecial
};
}).ToListAsync());
}

public async Task<ActionResult<IEnumerable<object>>> GetMediaItems(string path, string extensions)
Expand Down Expand Up @@ -136,7 +153,8 @@ public async Task<ActionResult<IEnumerable<object>>> GetMediaItems(string path,

public async Task<ActionResult<object>> GetMediaItem(string path)
{
if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia))
if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia)
|| (HttpContext.IsSecureMediaEnabled() && !await _authorizationService.AuthorizeAsync(User, SecureMediaPermissions.ViewMedia, (object)(path ?? string.Empty))))
{
return Forbid();
}
Expand All @@ -160,7 +178,8 @@ public async Task<ActionResult<object>> GetMediaItem(string path)
[MediaSizeLimit]
public async Task<IActionResult> Upload(string path, string extensions)
{
if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia))
if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia)
|| (HttpContext.IsSecureMediaEnabled() && !await _authorizationService.AuthorizeAsync(User, SecureMediaPermissions.ViewMedia, (object)(path ?? string.Empty))))
{
return Forbid();
}
Expand Down Expand Up @@ -308,7 +327,8 @@ public async Task<IActionResult> DeleteMedia(string path)
public async Task<IActionResult> MoveMedia(string oldPath, string newPath)
{
if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia)
|| !await _authorizationService.AuthorizeAsync(User, Permissions.ManageMediaFolder, (object)oldPath))
|| !await _authorizationService.AuthorizeAsync(User, Permissions.ManageMediaFolder, (object)oldPath)
|| !await _authorizationService.AuthorizeAsync(User, Permissions.ManageMediaFolder, (object)newPath))
{
return Forbid();
}
Expand Down Expand Up @@ -482,8 +502,11 @@ public object CreateFileResult(IFileStoreEntry mediaFile)
};
}

public IActionResult MediaApplication(MediaApplicationViewModel model)
public async Task<IActionResult> MediaApplication(MediaApplicationViewModel model)
{
// Check if the user has access to new folders. If not, we hide the "create folder" button from the root folder.
model.AllowNewRootFolders = !HttpContext.IsSecureMediaEnabled() || await _authorizationService.AuthorizeAsync(User, SecureMediaPermissions.ViewMedia, (object)"_non-existent-path-87FD1922-8F88-4A33-9766-DA03E6E6F7BA");

return View(model);
}

Expand Down Expand Up @@ -553,5 +576,8 @@ private async Task PreCacheRemoteMedia(IFileStoreEntry mediaFile, Stream stream
localStream?.Dispose();
}
}

private bool IsSpecialFolder(string path)
=> string.Equals(path, _mediaOptions.AssetsUsersFolder, StringComparison.OrdinalIgnoreCase) || string.Equals(path, _attachedMediaFieldFileService.MediaFieldsFolder, StringComparison.OrdinalIgnoreCase);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public override IDisplayResult Edit(MediaField field, BuildFieldEditorContext co
}
model.Paths = JConvert.SerializeObject(itemPaths, JOptions.CamelCase);
model.TempUploadFolder = _attachedMediaFieldFileService.MediaFieldsTempSubFolder;
model.TempUploadFolder = _attachedMediaFieldFileService.GetMediaFieldsTempSubFolder();
model.Field = field;
model.Part = context.ContentPart;
model.PartFieldDefinition = context.PartFieldDefinition;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Threading.Tasks;
using OrchardCore.Environment.Cache;
using OrchardCore.Media.Core.Events;

namespace OrchardCore.Media.Events;

internal sealed class SecureMediaFileStoreEventHandler : MediaEventHandlerBase
{
private readonly ISignal _signal;

public SecureMediaFileStoreEventHandler(ISignal signal)
{
_signal = signal;
}

public override Task MediaCreatedDirectoryAsync(MediaCreatedContext context)
{
if (context.Result)
{
SignalDirectoryChange();
}

return Task.CompletedTask;
}

public override Task MediaDeletedDirectoryAsync(MediaDeletedContext context)
{
if (context.Result)
{
SignalDirectoryChange();
}

return Task.CompletedTask;
}

private void SignalDirectoryChange() => _signal.DeferredSignalToken(nameof(SecureMediaPermissions));
}
11 changes: 11 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Media/Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,14 @@
],
Category = "Content Management"
)]

[assembly: Feature(
Id = "OrchardCore.Media.Security",
Name = "Secure Media",
Description = "Adds permissions to restrict access to media folders.",
Dependencies =
[
"OrchardCore.Media"
],
Category = "Content Management"
)]
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OrchardCore.Media.Services;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Web.Middleware;
using SixLabors.ImageSharp.Web.Processors;
Expand Down Expand Up @@ -83,6 +84,26 @@ public void Configure(ImageSharpMiddlewareOptions options)
return Task.CompletedTask;
};

var onPrepareResponse = options.OnPrepareResponseAsync;
options.OnPrepareResponseAsync = async context =>
{
if (onPrepareResponse is not null)
{
await onPrepareResponse(context);
}
// Override cache control for secure files
if (context.IsSecureMediaRequested())
{
var mediaOptions = context.RequestServices.GetRequiredService<IOptions<MediaOptions>>().Value;
var secureCacheControl = mediaOptions.MaxSecureFilesBrowserCacheDays == 0
? "no-store"
: "public, must-revalidate, max-age=" + TimeSpan.FromDays(mediaOptions.MaxSecureFilesBrowserCacheDays).TotalSeconds.ToString();
context.Response.Headers.CacheControl = secureCacheControl;
}
};
}

private static void ValidateTokenlessCommands(ImageCommandContext context, MediaOptions mediaOptions)
Expand Down

0 comments on commit 9cf1f78

Please sign in to comment.