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

UpdateDocumentAssetCommand: throwing permission error when executing with elevated permissions/system user execution context as BackgroundTask #538

Closed
otavioferraz opened this issue May 8, 2023 · 4 comments
Labels

Comments

@otavioferraz
Copy link

Hi there!

I'm trying to implement an immediate background task (through Hangfire) that is called by a PageAdded/PagePublished message handler and generates a Word or Pdf document of a page when it is published. The documents are stored in the document assets repository via execution of AddDocumentAssetCommand or UpdateDocumentAssetCommand (when a document already exists for that page/version in the repository and the page is re-published - in order to maintain only one document per page/version).

The code is working fine with AddDocumentAssetCommand, but is throwing a PermissionValidationFailedException with UpdateDocumentAssetCommand - ImageAssetDeletePermission required. I've tried to execute the code with Elevated Permissions and by creating a System User Execution Context and both fails.

These are the details of the exception:

Cofoundry.Domain.PermissionValidationFailedException
Permission Validation Check Failed. Permission Type: Cofoundry.Domain.ImageAssetDeletePermission. UserId:

Cofoundry.Domain.PermissionValidationFailedException: Permission Validation Check Failed. Permission Type: Cofoundry.Domain.ImageAssetDeletePermission. UserId:
at Cofoundry.Domain.Internal.PermissionValidationService.EnforcePermission(IPermissionApplication permission, IUserContext userContext)
at Cofoundry.Domain.Internal.PermissionValidationService.EnforcePermission(IEnumerable1 permissions, IUserContext userContext) at Cofoundry.Domain.Internal.ExecutePermissionValidationService.Validate[TCommand](TCommand command, ICommandHandler1 commandHandler, IExecutionContext executionContext)
at Cofoundry.Domain.CQS.Internal.CommandExecutor.ExecuteCommandAsync[TCommand](TCommand command, IExecutionContext executionContext)
at Cofoundry.Domain.CQS.Internal.CommandExecutor.ExecuteCommandAsync[TCommand](TCommand command, IExecutionContext executionContext)
at Cofoundry.Domain.CQS.Internal.CommandExecutor.ExecuteAsync(ICommand command, IExecutionContext executionContext)
at Cofoundry.Domain.Internal.UpdateDocumentAssetCommandHandler.ExecuteAsync(UpdateDocumentAssetCommand command, IExecutionContext executionContext)
at Cofoundry.Domain.CQS.Internal.CommandExecutor.ExecuteCommandAsync[TCommand](TCommand command, IExecutionContext executionContext)
at Cofoundry.Domain.CQS.Internal.CommandExecutor.ExecuteCommandAsync[TCommand](TCommand command, IExecutionContext executionContext)
at Cofoundry.Domain.CQS.Internal.CommandExecutor.ExecuteAsync(ICommand command, IExecutionContext executionContext)
at OtavioFerraz.WebApp.Cofoundry.MessageHandlers.ResumePage.BackgroundTasks.ResumeDocumentGenerateBackgroundTask.AddOrUpdateDocumentAsync(String title, StreamFileSource file) in /Users/otavioferraz/Projects/OtavioFerraz/OtavioFerraz.WebApp/Cofoundry/MessageHandlers/ResumePage/BackgroundTasks/ResumeDocumentGenerateBackgroundTask.cs:line 131
at OtavioFerraz.WebApp.Cofoundry.MessageHandlers.ResumePage.BackgroundTasks.ResumeDocumentGenerateBackgroundTask.GenerateDocAsync(Int32 pageId) in /Users/otavioferraz/Projects/OtavioFerraz/OtavioFerraz.WebApp/Cofoundry/MessageHandlers/ResumePage/BackgroundTasks/ResumeDocumentGenerateBackgroundTask.cs:line 55
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()

This is the code for the BackgroundTask:

public async Task GenerateDocAsync(int pageId)
{
var resumePage = await _domainRepository.ExecuteQueryAsync(new GetResumePageDetailsByIdQuery(pageId));
EntityNotFoundException.ThrowIfNull(resumePage, pageId);

        var jsonData = JObject.FromObject(resumePage.Content);

        var adobeClient = new AdobePdfServicesClient(_configuration);

        var resumeDocStream = await adobeClient.MergeDocumentAsync(_resumeDocumentSettings.TemplateUrl, jsonData, OutputFormat.DOCX);

        var resumeDocId = await AddOrUpdateDocumentAsync(
            $"CV Otávio Ferraz {resumePage.Locale.ToUpper()} DOC-P{resumePage.PageId}-V{resumePage.Version}",
            new StreamFileSource("tempfile.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", () => resumeDocStream));

        var resumeDocument = await _advancedContentRepository
            .DocumentAssets()
            .GetById(resumeDocId)
            .AsRenderDetails()
            .ExecuteAsync();

        var mailTemplate = new ResumeDocumentMailTemplate();
        mailTemplate.Document = resumeDocument;
        mailTemplate.Page = resumePage.FullUrlPath;

        await _mailService.SendAsync(_resumeDocumentSettings.NotificationToAddress, mailTemplate);
    }

    public async Task GeneratePdfAsync(int pageId)
    {
        var resumePage = await _domainRepository.ExecuteQueryAsync(new GetResumePageDetailsByIdQuery(pageId));
        EntityNotFoundException.ThrowIfNull(resumePage, pageId);

        var jsonData = JObject.FromObject(resumePage.Content);

        var adobeClient = new AdobePdfServicesClient(_configuration);

        var resumePdfStream = await adobeClient.MergeDocumentAsync(_resumeDocumentSettings.TemplateUrl, jsonData, OutputFormat.PDF);

        var resumePdfId = await AddOrUpdateDocumentAsync(
            $"CV Otávio Ferraz {resumePage.Locale.ToUpper()} PDF-P{resumePage.PageId}-V{resumePage.Version}",
            new StreamFileSource("tempfile.pdf", "application/pdf", () => resumePdfStream));

        var resumeDocument = await _advancedContentRepository
            .DocumentAssets()
            .GetById(resumePdfId)
            .AsRenderDetails()
            .ExecuteAsync();

        var mailTemplate = new ResumeDocumentMailTemplate();
        mailTemplate.Document = resumeDocument;
        mailTemplate.Page = resumePage.FullUrlPath;

        await _mailService.SendAsync(_resumeDocumentSettings.NotificationToAddress, mailTemplate);
    }

    private async Task<int> AddOrUpdateDocumentAsync(string title, StreamFileSource file)
    {
        var currentDocumentId = await _domainRepository.ExecuteQueryAsync(new DoesDocumentExistsQuery(title));

        if (currentDocumentId == 0)
        {
            var newDocumentId = await _advancedContentRepository
                .WithElevatedPermissions()
                .DocumentAssets()
                .AddAsync(new AddDocumentAssetCommand()
                {
                    File = file,
                    Title = title
                });

            return newDocumentId;
        }
        else
        {
            //await _advancedContentRepository
            //    .WithElevatedPermissions()
            //    .DocumentAssets()
            //    .UpdateAsync(new UpdateDocumentAssetCommand()
            //    {
            //        DocumentAssetId = currentDocumentId,
            //        File = file,
            //        Title = title
            //    });

            var executionContext = await _executionContextFactory.CreateSystemUserExecutionContextAsync();

            await _commandExecutor.ExecuteAsync(new UpdateDocumentAssetCommand()
            {
                DocumentAssetId = currentDocumentId,
                File = file,
                Title = title
            }, executionContext);

            return currentDocumentId;
        }
    }

I assume, for some reason, the elevated permissions or system user execution context are not being carried over to the QueueAssetFileDeletionCommand, which requires both ImageAssetDeletePermission and DocumentAssetDeletePermission. Or should it be implemented a different way?

Thanks in advance!

@otavioferraz
Copy link
Author

After a quick further inspection into the current implementation of the UpdateDocumentAssetCommandHandler, it seems that the deleteOldFileCommand execution is being done without passing through the execution context it was injected by the command handler (Line 71).

An easy fix would be replace the line 71 by the one below:

await _commandExecutor.ExecuteAsync(deleteOldFileCommand, executionContext);

Can this fix be implemented on the next minor release?

@HeyJoel HeyJoel added the bug label May 9, 2023
@HeyJoel
Copy link
Member

HeyJoel commented May 9, 2023

Hi @otavioferraz, yes that looks like the issue. I'll see if I can find time to put in a release with the fix soon, but until then you can easily integrate the fix yourself by copying the UpdateDocumentAssetCommandHandler code into your solution, applying the fix and then registering it with the DI container as an override:

public class ExampleOverrideRegistration : IDependencyRegistration
{
    public void Register(IContainerRegister container)
    {
        container.Register<ICommandHandler<UpdateDocumentAssetCommand>, UpdateDocumentAssetCommandHandlerFix>(RegistrationOptions.Override());
    }
}

Full code below:

namespace ExampleFix;

using Cofoundry.Core.Data;
using Cofoundry.Core.DependencyInjection;
using Cofoundry.Core.MessageAggregator;
using Cofoundry.Core.Validation;
using Cofoundry.Domain.CQS;
using Cofoundry.Domain.Data;
using Microsoft.EntityFrameworkCore;

public class UpdateDocumentAssetCommandHandlerFix
    : ICommandHandler<UpdateDocumentAssetCommand>
    , IPermissionRestrictedCommandHandler<UpdateDocumentAssetCommand>
{
    private readonly CofoundryDbContext _dbContext;
    private readonly EntityAuditHelper _entityAuditHelper;
    private readonly EntityTagHelper _entityTagHelper;
    private readonly DocumentAssetCommandHelper _documentAssetCommandHelper;
    private readonly ITransactionScopeManager _transactionScopeFactory;
    private readonly IMessageAggregator _messageAggregator;
    private readonly ICommandExecutor _commandExecutor;

    public UpdateDocumentAssetCommandHandlerFix(
        CofoundryDbContext dbContext,
        EntityAuditHelper entityAuditHelper,
        EntityTagHelper entityTagHelper,
        DocumentAssetCommandHelper documentAssetCommandHelper,
        ITransactionScopeManager transactionScopeFactory,
        IMessageAggregator messageAggregator,
        ICommandExecutor commandExecutor
        )
    {
        _dbContext = dbContext;
        _entityAuditHelper = entityAuditHelper;
        _entityTagHelper = entityTagHelper;
        _documentAssetCommandHelper = documentAssetCommandHelper;
        _transactionScopeFactory = transactionScopeFactory;
        _messageAggregator = messageAggregator;
        _commandExecutor = commandExecutor;
    }

    public async Task ExecuteAsync(UpdateDocumentAssetCommand command, IExecutionContext executionContext)
    {
        bool hasNewFile = command.File != null;

        var documentAsset = await _dbContext
            .DocumentAssets
            .Include(a => a.DocumentAssetTags)
            .ThenInclude(a => a.Tag)
            .FilterById(command.DocumentAssetId)
            .SingleOrDefaultAsync();

        documentAsset.Title = command.Title;
        documentAsset.Description = command.Description ?? string.Empty;
        documentAsset.FileName = FilePathHelper.CleanFileName(command.Title);

        if (string.IsNullOrWhiteSpace(documentAsset.FileName))
        {
            throw ValidationErrorException.CreateWithProperties("Document title is empty or does not contain any safe file path characters.", nameof(command.Title));
        }

        _entityTagHelper.UpdateTags(documentAsset.DocumentAssetTags, command.Tags, executionContext);
        _entityAuditHelper.SetUpdated(documentAsset, executionContext);

        using (var scope = _transactionScopeFactory.Create(_dbContext))
        {
            if (hasNewFile)
            {
                var deleteOldFileCommand = new QueueAssetFileDeletionCommand()
                {
                    EntityDefinitionCode = DocumentAssetEntityDefinition.DefinitionCode,
                    FileNameOnDisk = documentAsset.FileNameOnDisk,
                    FileExtension = documentAsset.FileExtension
                };

                await _commandExecutor.ExecuteAsync(deleteOldFileCommand, executionContext);
                await _documentAssetCommandHelper.SaveFile(command.File, documentAsset);
                documentAsset.FileUpdateDate = executionContext.ExecutionDate;
            }

            await _dbContext.SaveChangesAsync();

            scope.QueueCompletionTask(() => OnTransactionComplete(documentAsset, hasNewFile));

            await scope.CompleteAsync();
        }
    }

    private Task OnTransactionComplete(DocumentAsset documentAsset, bool hasNewFile)
    {
        return _messageAggregator.PublishAsync(new DocumentAssetUpdatedMessage()
        {
            DocumentAssetId = documentAsset.DocumentAssetId,
            HasFileChanged = hasNewFile
        });
    }

    public IEnumerable<IPermissionApplication> GetPermissions(UpdateDocumentAssetCommand command)
    {
        yield return new DocumentAssetUpdatePermission();
    }
}

public class ExampleOverrideRegistration : IDependencyRegistration
{
    public void Register(IContainerRegister container)
    {
        container.Register<ICommandHandler<UpdateDocumentAssetCommand>, UpdateDocumentAssetCommandHandlerFix>(RegistrationOptions.Override());
    }
}

@HeyJoel HeyJoel closed this as completed in dcb6f06 May 9, 2023
@HeyJoel
Copy link
Member

HeyJoel commented May 9, 2023

Fixed, will be release in 0.11.4

@HeyJoel
Copy link
Member

HeyJoel commented May 18, 2023

0.11.4 has now been released with the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants