Open Closed

File Management Module Download Problem #5956


User avatar
0
cangunaydin created
  • ABP Framework version: v7.4.0
  • UI Type: Angular
  • Database System: EF Core (SQL Server, Oracle, MySQL, PostgreSQL, etc..)
  • Tiered (for MVC) or Auth Server Separated (for Angular): yes

Hello I recently upgraded to Abp v7.4.0, I am using file management module with abp, I customize it according to my needs and it was working fine. Now i have a problem with file download. I think there is a bug related with it. Since i customize it i couldn't be sure but I didn't override download part before, so it is likely from the new version.

Anyway here is the problem. For download to work first angular is doing a backend call to get a token. Then doing get request by using javascript window.open here is the file management module angular code.

  downloadFile(file: FileInfo) {
    return this.fileDescriptorService.getDownloadToken(file.id).pipe(
      tap((res) => {
        window.open(
          `${this.apiUrl}/api/file-management/file-descriptor/download/${file.id}?token=${res.token}`,
          '_self'
        );
      })
    );
  }

"this.fileDescriptorService.getDownloadToken" call is an authenticated but file-descriptor/download call is anonymous call.

On the backend side, when IDistributedCache is used it sets the token for the current tenant. so it normalizes the cache key. here is the code for it.

 public virtual async Task<DownloadTokenResultDto> GetDownloadTokenAsync(Guid id)
 {
     var token = Guid.NewGuid().ToString();

     await DownloadTokenCache.SetAsync(
         token,
         new FileDownloadTokenCacheItem { FileDescriptorId = id },
         new DistributedCacheEntryOptions
         {
             AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60)
         });

     return new DownloadTokenResultDto
     {
         Token = token
     };
 }

here the current tenant id is set. So the problem is when you do the second call to /api/file-management/file-descriptor/download/ endpoint with window.open since it is anonymous and token is not set it doesn't get the current tenant id. here is the code for it.

 [AllowAnonymous]
 public virtual async Task<IRemoteStreamContent> DownloadAsync(Guid id, string token)
 {
     var downloadToken = await DownloadTokenCache.GetAsync(token);
     if (downloadToken == null || downloadToken.FileDescriptorId != id)
     {
         throw new AbpAuthorizationException("Invalid download token: " + token);
     }

     FileDescriptor fileDescriptor;
     using (DataFilter.Disable<IMultiTenant>())
     {
         fileDescriptor = await FileDescriptorRepository.GetAsync(id);
     }
     var stream = await BlobContainer.GetAsync(id.ToString());
     return new RemoteStreamContent(stream, fileDescriptor?.Name);
 }

here you get authorization exception since cache key is not normalized over here. even if i override and change the current tenant id to null, it gets the token but this time BlobContainer can not find the file since this file belongs to tenant. What i came up with is to send tenantId from user interface as a query string and override FileDescriptorController like this.

also i override the angular part and inject the new service as a download service. sth like

@Injectable()
export class CreativeDownloadService {
  apiName = 'FileManagement';

  get apiUrl() {
    return this.environment.getApiUrl(this.apiName);
  }

  constructor(
    private restService: RestService,
    private fileDescriptorService: FileDescriptorService,
    private environment: EnvironmentService,
    private configStateService: ConfigStateService
  ) {}

  downloadFile(file: FileInfo) {
    const currentUser = this.configStateService.getOne("currentUser"); 
    return this.fileDescriptorService.getDownloadToken(file.id).pipe(
      tap((res) => {
        window.open(
          `${this.apiUrl}/api/file-management/file-descriptor/download/${file.id}?token=${res.token}&__tenant=${currentUser?.tenantId}`,
          '_self'
        );
      })
    );
  }
}

and then provide it.

I don't know how this was working before. I wonder if new version changed sth, by the way i use redis cache. Also sth I didn't understand is according to docs

giving query string __tenant should set CurrentTenant but it doesn't do so. Is this related with [AllowAnonymous] attribute? Thanks for the help and waiting for your reply.


10 Answer(s)
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    I will check it

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    I can't reproduce the problem.

    Could you provide the full steps to reproduce? thanks.

  • User Avatar
    0
    cangunaydin created

    Hello, If you want, I can send you a sample app if you give me an email address ok here are the steps.

    1. Create the new project from abp cli abp new Doohlink -t app-pro -u angular -dbms PostgreSQL --separate-auth-server -m maui -csf

    2. Add Volo.FileManagement module (run the command inside aspnet-core folder) abp add-module Volo.FileManagement

    3. Arrange Postgres and Redis. Change appsettings.json according to that. (I use docker containers for that.)

    4. Run Dbmigrator.

    5. Run the Application (AuthServer and HttpApi.Host)

    6. do yarn install in angular app.

    7. Configure the angular app. Add Config Module and Feature Module.

    8. run angular app with yarn start

    9. now you should see file management menu item.

    10. upload an image.

    11. Then try to download that image.

    12. you will see an authentication error.

    i hope this is enough information, as i say if you can not reproduce i can send you the sample app.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hello, If you want, I can send you a sample app if you give me an email address ok here are the steps.

    Yes, please. my emali is shiwei.liang@volosoft.com

  • User Avatar
    0
    cangunaydin created

    I have sent the email, i have also added docker compose file for postgres and redis. you can check it out if you want.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    It looks no problem

  • User Avatar
    0
    cangunaydin created

    Hello, Can you try to impersonate from the admin side for the tenant and try to download? I think that's the problem.

  • User Avatar
    0
    cangunaydin created

    And I still think that since it is an anonymous call, it shouldn't depend on "Current Tenant Id" while you are downloading the file. Cause you already have a token id to download the file.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi

    I can confirm the problem, we will fix it in the next patch version.

    This is a temporary solution:

    [Serializable]
    [IgnoreMultiTenancy]
    public class MyFileDownloadTokenCacheItem
    {
        public Guid FileDescriptorId { get; set; }
        
        public Guid? TenantId { get; set; }
    }
    
    [RequiresFeature(FileManagementFeatures.Enable)]
    [Authorize(FileManagementPermissions.FileDescriptor.Default)]
    [ExposeServices(typeof(IFileDescriptorAppService))]
    public class MyFileDescriptorAppService : FileDescriptorAppService
    {
        private IDistributedCache<MyFileDownloadTokenCacheItem, string> _tokenCache { get; set; }
        
        public MyFileDescriptorAppService(IFileManager fileManager, IFileDescriptorRepository fileDescriptorRepository,
            IBlobContainer<FileManagementContainer> blobContainer,
            IDistributedCache<FileDownloadTokenCacheItem, string> downloadTokenCache,
            IDistributedCache<MyFileDownloadTokenCacheItem, string> tokenCache) : base(fileManager,
            fileDescriptorRepository, blobContainer, downloadTokenCache)
        {
            _tokenCache = tokenCache;
        }
    
        [AllowAnonymous]
        public override async Task<IRemoteStreamContent> DownloadAsync(Guid id, string token)
        {
            var downloadToken = await _tokenCache.GetAsync(token);
            if (downloadToken == null || downloadToken.FileDescriptorId != id)
            {
                throw new AbpAuthorizationException("Invalid download token: " + token);
            }
    
            using (CurrentTenant.Change(downloadToken.TenantId))
            {
                var fileDescriptor = await FileDescriptorRepository.GetAsync(id);
                var stream = await BlobContainer.GetAsync(id.ToString());
                return new RemoteStreamContent(stream, fileDescriptor?.Name);
            }
    
        }
    
        public override  async Task<DownloadTokenResultDto> GetDownloadTokenAsync(Guid id)
        {
            var token = Guid.NewGuid().ToString();
    
            await _tokenCache.SetAsync(
                token,
                new MyFileDownloadTokenCacheItem()
                {
                    FileDescriptorId = id,
                    TenantId = CurrentTenant.Id
                },
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60)
                });
    
            return new DownloadTokenResultDto
            {
                Token = token
            };
        }
    }
    
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Your ticket was refunded

Made with ❤️ on ABP v9.1.0-preview. Updated on December 10, 2024, 06:38