Secure Media

Topics: Customizing Orchard, General, Writing modules
Mar 26, 2013 at 6:06 AM
Edited Mar 26, 2013 at 6:07 AM

For a client I need to create a way of managing secure media (primarily PDF documents) so their clients can access the files through the front end.

I imagine I can just create a Content Type with a Media picker field, then restrict access to those Content Types using the current Orchard fruits. Easy.

However the documents cannot be accessed publicly through the URL, unless the user is authenticated. I've done similar before on WebForm sites by using IIS file protection handlers etc; however I really would prefer an Orchard-friendly way if possible.

For those much smarter than I (not hard really), could you offer your wisdom on this topic?

Mar 26, 2013 at 6:10 AM
The media module is designed for publicly accessible files. If you need protected resoruces, you should store the binaries into the database and use regular content item permissions
Mar 26, 2013 at 6:31 AM
Edited Mar 26, 2013 at 6:32 AM
Wow! Lightning LeRoy! Quickest fingers of the west!

Okay, so if I were to create a SecureMedia part (or field or whatever), when the ContentItem is fetched, will Orchard load this binary from the database each time? Thinking worst-case, some of the PDFs could be quite large (10-20 MB), which is a lot to load through SQL Server. The client's site will be hosted on a shared server, also sharing it's database on a shared SQL server.

Admittedly, I don't think the site will receive high traffic for the secure files, however I believe it's important to consider these issues just in case. If all the users logged on at the same time to download the latest 20 MB PDF report (all print ready and filled with 300 dpi stock images), the ol' SQL Server might freak out and do something stupid.
Mar 26, 2013 at 7:09 AM
Well, you could I suppose also store them in the file system, on a protected directory, and TransmitFile after you've verified credentials.
Mar 26, 2013 at 8:05 AM
We store files that need to be 'behind walls' in the App_Data folder in the site' folder.

We use a custom storage provider for that, AppDataStorageProvider, based on the FileSystemStorageProvider. I guess you could do something similar.
Mar 26, 2013 at 4:02 PM
There's already an App_Data "file system provider", IAppDataFolder.
Mar 26, 2013 at 11:57 PM
Okay cool, (for the sake of documentation) so I should be able to write a fairly simple module which allows file uploads to the App_Data folder implementing IAppDataFolder and deliver said files using TransmitFile.

Sweet, now I've got an idea on how it could work, it's time to get down and dirty with pen and paper to figure out the specifics. Thanks for your help guys!
Mar 27, 2013 at 1:30 AM
Oh wow, of all places, not App_data. Please. That's absolutely crazy. Never, ever, for any reason, serve anything from app_data. That is about the most dangerous thing you could do. Just don't do it.

@AimOrchard: if you published a module that does that on the gallery, please tell me its name and I'll delete it. I might also ban you permanently from ever publishing anything on the gallery. Kidding, but barely.
Mar 27, 2013 at 1:35 AM
BertrandLeRoy wrote:
Oh wow, of all places, not App_data. Please. That's absolutely crazy. Never, ever, for any reason, serve anything from app_data. That is about the most dangerous thing you could do. Just don't do it.
How would you suggest to store the documents in the file system within Orchard Bertrand? Would it be advisable to create a "Secure Media" folder in the root and simply TransmitFile the media from there? What is the best convention?
Mar 27, 2013 at 4:02 AM
Yes, that would be much better: something that only serves that purpose and none other.
Mar 27, 2013 at 4:18 AM
Edited Mar 27, 2013 at 4:18 AM
justrhysism wrote:
Okay cool, (for the sake of documentation) so I should be able to write a fairly simple module which allows file uploads to the App_Data folder implementing IAppDataFolder and deliver said files using TransmitFile.
So, for the record (and anyone looking at this in the future), completely disregard this. Bad idea.
Mar 27, 2013 at 6:52 AM
Err, that was kind of rude Bertrand... Well no worries, none of our modules have this in them @ the gallery.

We put such files in App_Data as there is already a folder for site specific data that shouldn't be available publicly, like the settings.

Also, your remark only holds if you would allow semi-direct access to the data contained there (for example you have a download controller that accepts a file name and you would just allow to download it without any decent checks)

To dl files from it, we work with ids that are mappable to those specific files: we simply do not offer a method to download files by file name.

Maybe a bad idea by default, but I'd defend our usage as being secure... Not every coder is the same you know, some people do spend the extra time to secure things...

Anyway, for people reading this, whenever you see a remark from us posted here, keep in mind that we do not have a simple orchard website: it is doing plenty of stuff in the background, hosts multiple WCF services, contains our API that is being used by our software and will soon contain a custom wcf server that acts as an end point for our custom 'always on' system that we'll be implementing soon in our software.

So yes, I'll talk to a colleague about your remark Bertrand, maybe we'll switch to another solution now, just to 'comply' with general security rules, I just wished you put it more friendly instead of the threatening us with being perm banned from the gallery... Even if it is a joke, it sure is not a funny one...
Mar 27, 2013 at 7:09 AM
The framework takes special precautions protecting app_data. By providing access to even parts of it, you're going around those precautions and opening a backdoor. Don't do that. Instead, create your own protected folder for that usage. Hell is paved with good intentions.

And learn to take a joke, especially when it's clearly labeled as such.
Mar 27, 2013 at 7:11 AM
I'm more annoyed by the fact that you did state it was a 'very' bad idea, without stating 'why'. You did so now, and I thank you for that.
Mar 27, 2013 at 11:26 PM
Hi everyone,

I have a module I built for local use I call "Rework.Filestream" which provides a framework to stream a "secure file". All you need to do is set up a web.config in your "secure folder" preventing visitors from directly browsing to the folder by url. After that, it uses role based security to control who can access the files in the folder. Everything works good (I think so at least) except currently the role security is not unique by content type but is global (i.e. you can only have one secure document folder per website and access control is not based by content type). To make it better, it really should have the secure folder setting handled on the content type and have the role based security exposed at the content type level (instead of globally).

I also have a module I call "Rework.MediaPlayer" which allows you to do a similar thing with secure video file streaming (html5 and flash fallback). The module is functional though it has a similar limitation to the one above, one security setting/folder globally.

The one thing I am displeased about (though I see no way around it) is that there is no way I know of to have the module itself deploy a web.config so it will still involve a user putting in place their own web.config file for each folder they want to secure.

With all that said, if you think these will be helpful (and I would also love some help in improving the modules) I can release them under open source on codeplex. Let me know if they sounds useful.
Oct 22, 2013 at 7:19 PM
BertrandLeRoy wrote:
The media module is designed for publicly accessible files. If you need protected resoruces, you should store the binaries into the database and use regular content item permissions
How would you go about storing "the binaries into the database"? I have a scenario where I'd like to use content permissions so using regular content would be fine. I just need to be able to attach files to content and store them in the database.
Oct 26, 2013 at 7:17 AM
By using the binary column type I suppose.
Oct 28, 2013 at 1:16 PM
Right. No, I get that. But my hangup lies in the details.

Do I need to create a new field for a file upload? A content part? Not real sure how the driver would look.

Like, if I was writing my own application, I would have no issue saving a file to the database. I just have no clue how to do that inside of Orchard.
Oct 28, 2013 at 4:34 PM
Maybe look at existing code that does this, such as the media library.
Oct 30, 2013 at 2:46 PM
Thank you for the reply. I've been looking over the MediaLibrary module.

I also have done the content part/fields tutorials. I've scoured for documentation. I'm having difficulty bringing everything full circle.

So, I created the following pretty much dead-on what the tutorials did: SecureMediaRecord, Part, Driver, Handler and the migration. I want to upload a file and save the file stream to the database in a binary field along w/ the filename, a title and description field. Where do you do the save part? In a controller?

I'm trying to start this whole thing very simple so I can understand how everything works together.
Oct 30, 2013 at 3:10 PM
Sure, a controller can do the trick.
Oct 30, 2013 at 3:43 PM
Ok, great. Glad to see I'm on the right track. Now, does it need to be named something specific for conventions? The action methods as well?

I'm trying not to be a pain. I'm really that raw here.
Oct 30, 2013 at 3:48 PM
The controller class name needs to end with "Controller". That's about it.
Oct 31, 2013 at 3:08 PM
I have my secure media part wrapped in a secure media content type that's marked as "createable". This puts the link in the admin area for Create New under Page and Projection.

I created an INavigationProvider for the admin area which puts a new link in the admin area as well. But what I'd really like to do is use the create new content link that gets generated by the "createable" flag for the content type.

My question would be then, how do you inject your own controller or process that allows you to do the saving of the new content type yourself? Because mine bombs in the IRepository, and never hits anything I create. When debugging, I notice that inside the ContentItem, the SecureMediaPart stuff is all null. So I'm guessing I need to inject something, somewhere that will bind up the properties of the SecureMediaPart object to the values entered in the view.

Basically, I have the Views/Editor/Parts/SecureMedia.cshtml view and I have all the stuff there I need. But when you save or publish, it goes...... where?
Nov 2, 2013 at 7:54 AM
Now I'm confused. Why do you want this to be done by a controller? Why not part drivers?
Nov 2, 2013 at 12:46 PM
I wrote this module back in April. It should have everything you need :
Nov 4, 2013 at 1:57 PM
If you use file storage, are they still secured by the content permissions then?
Nov 4, 2013 at 2:02 PM
Yup. You need to set the permissions on the content object and then those permissions will be carried over to the field.

Nov 4, 2013 at 3:14 PM
I tried to install on a fresh Orchard install and I got an error saying:

The assembly reference 'Microsoft.WindowsAzure.ServiceRuntime, Version=, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL' could not be loaded for module 'CloudConstruct.SecureFileField'.
Nov 4, 2013 at 3:17 PM
Hmm it is looking for the Azure SDK 1.8 assembly. You will need to install that. I am unsure why the published version did not come with it. If the dll is in the module bin directory then just re-add the reference.

Nov 4, 2013 at 3:25 PM
Yea leave it to me to confuse the people who know what they're doing.

Let me backtrack a bit.

I have a content type defined called SecureMediaContentType. Then I wrote my own custom content part called SecureMedia. I added that part to the content type in addition to content item permissions.

This is the driver code:
public class SecureMediaDriver 
        : ContentPartDriver<SecureMediaPart>
        protected override DriverResult Display(
            SecureMediaPart part, string displayType, dynamic shapeHelper)

            return ContentShape("Parts_SecureMedia", () => shapeHelper.Parts_SecureMedia(
                File: part.FileData,
                FileName: part.FileName,
                Title: part.Title,
                Description: part.Description));

        protected override DriverResult Editor(
            SecureMediaPart part, dynamic shapeHelper)

            return ContentShape("Parts_SecureMedia_Edit",
                () => shapeHelper.EditorTemplate(
                    TemplateName: "Parts/SecureMedia",
                    Model: part,
                    Prefix: Prefix));
Since I marked it as "createable", it added a link under the "new" admin menu. When I use that menu to create a new SecureMediaContentType, it "works", but just doesn't save the file data. It saves the other fields in my SecureMediaPart, just not the file data.

So, what I did was instead of going that route and trying to figure out what was doing the saving, I created an INavigationProvider that routed to my own SecureMediaAdminController. Then I wrote my own code to save the content part. This worked. I was able to get the file data to save. But now I have to figure out how to save the content item permissions with that....

OR, I need to figure out why the first attempt didn't save the file data.
Nov 4, 2013 at 3:59 PM
You would need to lookup the content item's permissions and apply them in your controller by hand. So only serve the file if the user has the permissions that are in the associated permissions part of your content item.

I think your best bet is to just install the Azure 1.8 SDK and my module will do everything you need.
Nov 4, 2013 at 4:01 PM
And I'm completely cool with that. Except I think it's missing a route for the SecureFileFieldController. I get a 404 when trying to view the actual file. The Display view works ok. There's a link to the file. But the link points to the SecureFileFieldController and gets a 404.
Nov 4, 2013 at 4:40 PM
Is there an error in the log? I assume you followed this post to setup the content item?
Nov 4, 2013 at 5:13 PM
Looks like a 1.7 issue. Looking into it now. I also published a new version of 1.4 to the gallery with the included Azure .dll for other on 1.6.
Nov 4, 2013 at 5:33 PM
I just installed it in a fresh 1.7 instance and all is good on my end. You created the local directory right?
Nov 4, 2013 at 6:29 PM
Yea, I followed the instructions. Saves ok. I see the saved PDF in my directory I created. But still getting a 404 when trying to view the content. The IIS error page lists the physical location for my site incorrectly though, which is weird.

But this is the URL for the secure media:
Nov 4, 2013 at 6:31 PM
Any errors in the logs directory? What happens if you debug?
Nov 4, 2013 at 6:44 PM
No errors.

It never hits the break point when viewing. Not even in the controller's constructor.

Does it matter if my directory is outside of the web application? i.e. on the c:\ root?
Nov 4, 2013 at 6:58 PM
Oh der... The link it generates doesn't include the orchard directory.
Nov 4, 2013 at 6:59 PM
Is it possible to change the secure url to include that?
Nov 4, 2013 at 7:15 PM
The controller is required to serve the file from the directory. It is strange to me that the Controller is not being hit at all and there are no errors in the logs. If you have skype I am happy to help. arra.derderian
Nov 4, 2013 at 7:35 PM
No, that's all good. It's that my web app is actually a virtual directory under another site. So the secure media link is pointing to the root of the web instead of my orchard site. I changed the view to pre-pend "/orchardlocal" to the link and it works.

I was just curious as to if I could customize the SecureUrl and SharedAccessUrl so that I didn't have to do that in the view.
Nov 4, 2013 at 10:54 PM
There is no way to do that in there now, but the views are a good place to override it I would think.
Nov 5, 2013 at 12:56 PM
Thank you for all your help. This looks like it will do the trick.
Nov 5, 2013 at 12:57 PM
Glad I could help!
Feb 11 at 7:21 PM
I love the SecureFileField, but I do have another question...

What if I wanted to stream the file to the browser in the Display() action in the driver? The way the controller does. Is there a DriverResult that'll jive w/ that? Or is it too late in the pipeline to do something like that?
Feb 12 at 2:29 PM
I don't think you can do that. (This is just me saying that.) The Driver returns a DriverResult (a shape) which is really to build up your model to be returned to the View. You would be disrupting the Orchard pipeline by returning something different. Perhaps you could modify the core so Display() could return a FileResult as well, but you would need to investigate.

The other way is in the View file, you could possibly modify the response there and stream the file from that location. So instead of showing the link with markup, you would change the response type and add the file to the response stream.
Feb 12 at 9:17 PM
Are you trying to display the document on the page, or send it directly to the user's browser for download?

Feb 13 at 1:39 PM
Let's assume for now that I'm only dealing with PDF type documents, and yes I'd like to stream it directly to the browser as in content-type: application/pdf and writing it directly to the response stream. Basically what the controller does when you click the link from the Display view.

For other document types (pictures, videos), I can just check the extension and display the appropriate HTML element.

What I did as a work-around for now is, I modified the SecureFile.cshtml view to check the file extension and if it's a PDF, I just do a location.href to the URL in the model. There's a slight moment when you actually see the Display view before it switches to the PDF, but it's acceptable for now.
Feb 13 at 1:41 PM
I think you would need to override the Orchard DriverResult somehow and add the ability to not return to a view but to a FileResult.