asp.net-mvc-5filecontentresult

MVC 5 FileContentResult Action Result Permission and Redirect


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");
        }           
    }

Solution

  • 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);
    }