- 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'
);
})
);
}
}
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)
-
0
Hi,
I will check it
-
0
Hi,
I can't reproduce the problem.
Could you provide the full steps to reproduce? thanks.
-
0
Hello, If you want, I can send you a sample app if you give me an email address ok here are the steps.
Create the new project from abp cli
abp new Doohlink -t app-pro -u angular -dbms PostgreSQL --separate-auth-server -m maui -csf
Add Volo.FileManagement module (run the command inside aspnet-core folder)
abp add-module Volo.FileManagement
Arrange Postgres and Redis. Change appsettings.json according to that. (I use docker containers for that.)
Run Dbmigrator.
Run the Application (AuthServer and HttpApi.Host)
do yarn install in angular app.
Configure the angular app. Add Config Module and Feature Module.
run angular app with yarn start
upload an image.
i hope this is enough information, as i say if you can not reproduce i can send you the sample app.
-
0
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
-
0
I have sent the email, i have also added docker compose file for postgres and redis. you can check it out if you want.
-
0
-
0
Hello, Can you try to impersonate from the admin side for the tenant and try to download? I think that's the problem.
-
0
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.
-
0
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 }; } }
-
0
Your ticket was refunded