Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added 'view' permissions for media folders. #15173

Merged
merged 46 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
cf07183
Adds 'view' permissions for media folders.
gvkries Jan 23, 2024
eb23458
Removes an unnecessary static field.
gvkries Jan 29, 2024
281fed5
Fix typo.
gvkries Jan 30, 2024
526d453
Merge branch 'main' into gvkries/secure-media-9369
gvkries Feb 3, 2024
9e271dd
Added documentation.
gvkries Apr 3, 2024
9158ba0
Added sample configuration.
gvkries Apr 3, 2024
223ecd6
Update src/OrchardCore.Modules/OrchardCore.Media/Processing/MediaImag…
gvkries Apr 3, 2024
b30fc42
Update src/OrchardCore.Modules/OrchardCore.Media/Services/MediaOption…
gvkries Apr 3, 2024
8021896
Update src/OrchardCore.Modules/OrchardCore.Media/Startup.cs
gvkries Apr 3, 2024
87f58cb
Update src/OrchardCore.Modules/OrchardCore.Media/SecureMediaPermissio…
gvkries Apr 3, 2024
4f85d29
Update src/OrchardCore.Modules/OrchardCore.Media/SecureMediaPermissio…
gvkries Apr 3, 2024
4051858
Update src/OrchardCore.Modules/OrchardCore.Media/SecureMediaPermissio…
gvkries Apr 3, 2024
c90c09e
Update src/OrchardCore.Modules/OrchardCore.Media/Controllers/AdminCon…
gvkries Apr 3, 2024
1ca5a20
Update src/OrchardCore.Modules/OrchardCore.Media/Services/ViewMediaFo…
gvkries Apr 3, 2024
941c0c7
Update src/OrchardCore.Modules/OrchardCore.Media/Services/ViewMediaFo…
gvkries Apr 3, 2024
d231e42
Update src/OrchardCore.Modules/OrchardCore.Media/Services/ViewMediaFo…
gvkries Apr 3, 2024
0e2adfe
Update test/OrchardCore.Tests/Modules/OrchardCore.Media/SecureMedia/V…
gvkries Apr 3, 2024
e2aa19a
React to code review.
gvkries Apr 3, 2024
a37760c
Merge suggestions.
gvkries Apr 3, 2024
4f5d751
Merge main.
gvkries Apr 3, 2024
63fc430
Rebuild JS assets with the Gulp pipeline to fix merge errors.
gvkries Apr 3, 2024
bbc8b61
Use default browser caching policy when anonymous access is granted.
gvkries Apr 4, 2024
260c9c9
Update src/OrchardCore.Modules/OrchardCore.Media/Services/ViewMediaFo…
gvkries Apr 5, 2024
09cf9cc
Added caching of view folder permissions.
gvkries Apr 5, 2024
964f594
Simplify permission cache invalidation.
gvkries Apr 6, 2024
414ba84
Removed IMediaEventHandler2.
gvkries Apr 6, 2024
d15dc53
Marks SecureMediaPermissions dependency with the correct feature attr…
gvkries Apr 10, 2024
90f56a9
Better description for folder permissions.
gvkries Apr 10, 2024
c640264
Fixes the build process to comply with new code analysis rules.
gvkries Apr 10, 2024
7402e9d
Merge branch 'main' into gvkries/secure-media-9369
gvkries Apr 12, 2024
58437f0
Merge branch 'main' into gvkries/secure-media-9369
gvkries Apr 14, 2024
be0a79f
Added a comment.
gvkries Apr 16, 2024
df42cc2
Merge branch 'main' into gvkries/secure-media-9369
gvkries Apr 16, 2024
e868ac6
Merge branch 'main' into gvkries/secure-media-9369
Piedone Apr 16, 2024
8cf5ebf
Apply suggestions from code review
gvkries Apr 17, 2024
a1ef044
React to code review.
gvkries Apr 17, 2024
24db9fa
Fix authorization for folders with special characters like spaces.
gvkries Apr 17, 2024
9a7fe73
Added video to the docs.
gvkries Apr 17, 2024
e7d4733
Merge branch 'main' into gvkries/secure-media-9369
gvkries Apr 19, 2024
ee8e77b
Apply suggestions from code review
gvkries Apr 20, 2024
e6ef83c
Update src/OrchardCore/OrchardCore.Media.Core/Events/MediaEventHandle…
gvkries Apr 23, 2024
1f1381c
Renamed SecureMediaMarkerService to SecureMediaMarker.
gvkries Apr 23, 2024
06a89de
Corrects the visibility of members in SecureMediaPermissions.
gvkries Apr 23, 2024
524e9ac
Merge branch 'main' into gvkries/secure-media-9369
gvkries Apr 23, 2024
af69a47
Merge branch 'main' into gvkries/secure-media-9369
hishamco Apr 25, 2024
380ec79
Merge branch 'main' into gvkries/secure-media-9369
hishamco Apr 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 AdminController(
IUserAssetFolderNameProvider userAssetFolderNameProvider,
IChunkFileUploadService chunkFileUploadService,
IFileVersionProvider fileVersionProvider,
IServiceProvider serviceProvider)
IServiceProvider serviceProvider,
AttachedMediaFieldFileService attachedMediaFieldFileService)
{
_mediaFileStore = mediaFileStore;
_mediaNameNormalizerService = mediaNameNormalizerService;
Expand All @@ -61,6 +63,7 @@ public AdminController(
_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");
hishamco marked this conversation as resolved.
Show resolved Hide resolved

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(
Piedone marked this conversation as resolved.
Show resolved Hide resolved
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
Loading