I have an MVC 5 application that allows users to download files that are stored in the database. I am using the FileContentResult action method to do this.
I can restrict access to this method throughout the application, but a smart user can figure out the action URL and paste something like this (localhost:50000/Home/FileDownload?id=13) into their browser and have access to download any file by just changing the parameter.
I want to restrict users from doing this. Only allow the Administrator role AND users that have a specific permission that can only be determined by a database call to download files.
What I am looking for is that If an user uses the URL to download a file and does not have the proper permissions, I want to redirect the user with a message.
I would like to do something like the code below or similar, but I get the following error: Cannot implicitly convert type 'System.Web.Mvc.RedirectToRouteResult' to 'System.Web.Mvc.FileContentResult'
I understand that I can not use return RedirectToAction("Index") here, just looking for some ideas on how to handle this problem.
public FileContentResult FileDownload(int id)
{
//Check user has file download permission
bool UserHasPermission = Convert.ToInt32(context.CheckUserHasFileDownloadPermission(id)) == 0 ? false : true;
if (User.IsInRole("Administrator") || UserHasPermission)
{
//declare byte array to get file content from database and string to store file name
byte[] fileData;
string fileName;
//create object of LINQ to SQL class
//using LINQ expression to get record from database for given id value
var record = from p in context.UploadedFiles
where p.Id == id
select p;
//only one record will be returned from database as expression uses condtion on primary field
//so get first record from returned values and retrive file content (binary) and filename
fileData = (byte[])record.First().FileData.ToArray();
fileName = record.First().FileName;
//return file and provide byte file content and file name
return File(fileData, "text", fileName);
}
else
{
TempData["Message"] = "Record not found";
return RedirectToAction("Index");
}
}
Since both FileContentResult
and RedirectToRouteResult
are inherited from ActionResult
, simply use ActionResult
instead of FileContentResult
for your action's return type:
public ActionResult FileDownload(int id)
{
if(IsUserCanDownloadFile()) // your logic here
{
// fetch the file
return File(fileData, "text", fileName);
}
return RedirectToAction("Index");
}
Or if you prefer attributes, you could write your very own authorize attribute to check permissions:
public class FileAccessAttribute : AuthorizeAttribute
{
private string _keyName;
public FileAccessAttribute (string keyName)
{
_keyName = keyName;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
// imagine you have a service which could check the Permission
return base.AuthorizeCore(httpContext)
|| (this.ContainsKey
&& _permissionService.CanDownload(httpContext.User.Identity.GetUserId(),
int.Parse(this.KeyValue.ToString()));
}
private bool ContainsKey
{
get
{
// for simplicity I just check route data
// in real world you might need to check query string too
return ((MvcHandler)HttpContext.Current.Handler).RequestContext
.RouteData.Values.ContainsKey(_keyName);
}
}
private object KeyValue
{
get
{
return ((MvcHandler)HttpContext.Current.Handler)
.RequestContext.RouteData.Values[_keyName];
}
}
}
Now you could decorate the custom attribute on your actions:
[FileAccess("id", Roles ="Administrator")]
public FileContentResult FileDownload(int id)
{
// fetch the file
return File(fileData, "text", fileName);
}