DNN Community Blog

The Community Blog is a personal opinion of community members and by no means the official standpoint of DNN Corp or DNN Platform. This is a place to express personal thoughts about DNNPlatform, the community and its ecosystem. Do you have useful information that you would like to share with the DNN Community in a featured article or blog? If so, please contact community@dnnsoftware.com.

 


Authorization in the new DNN Services Framework

I’ve been playing around with the new DNN Service Framework a little since the CTP 1 came out. As a module developer this is a very interesting addition to the framework (and one I’ve been asking for, for a while). In fact, I think it is the most significant change in 6.2. But then, I’ve always been into DNN because of the power of the framework.

As so many new additions to the framework, this starts out as a request from the community that then gets specced, scoped and what have you. Then we see it appear in the CTP/Beta with a note “by the way, we added feature XYZ”. Great. How does it work? And a demo comes soon after to show how it should work. Now, explaining what a new button does in the UI is one thing. Explaining how a completely new “framework” addition does is quite another. As always we begin with a “Hello World” example (as in the linked post above). But as a professional module developer you very quickly have to move beyond that. And my first stop is always: security. How does this feature open up the possibility to provide controlled access to my module’s resources? So in this post I want to move beyond the hello word example.

Goal

I’d like to build a RESTful service that allows you to retrieve the contents of Text/Html modules throughout the site. This means accessing contents as it were a tree of tabs (pages) and then modules. So “GET …/23/345” will get the module’s contents for module 345 on tab 23. If I omit the module id I should be returned a list of Text/Html modules (i.e. a list of subnodes). So “GET …/23” should get a list of Text/Html modules on tab 23. Finally if I omit the tab ID I’d like to see a list of tabs. So the base “GET” to my service should get a list of tabs. Now I’d also like this to be fully protected. So I don’t see tabs I’m not entitled to see, nor modules I’m not entitled to see.

Step 1: Create controller and first route

The first step is to open our project (in this case the Html module) and add a service controller which will do the magic. The snippet below I’ve added as “ServiceController.cs” to the Html module project.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using DotNetNuke.Web.Services;
using DotNetNuke.Security.Permissions;

namespace DotNetNuke.Modules.Html
{
 public class ServiceController : DnnController, IServiceRouteMapper
 {

  public void RegisterRoutes(ServicesRoutingManager routeManager)
  {
   routeManager.MapRoute("Html", "default", "", new { controller = "Service", action = "GetTabs" }, new[] { "DotNetNuke.Modules.Html" });
  }

  [DnnAuthorize(AllowAnonymous = true)]
  public ActionResult GetTabs()
  {
   Dictionary<string, string> tabs = new Dictionary<string, string>();
   foreach (DotNetNuke.Entities.Tabs.TabInfo t in DotNetNuke.Entities.Tabs.TabController.GetPortalTabs(PortalSettings.PortalId, -1, false, "", false, false, true, true, false))
   {
    tabs.Add(t.TabID.ToString(), t.TabName);
   }
   return Json(tabs, JsonRequestBehavior.AllowGet);
  }

 }
}

As was explained in the other post you need to (1) inherit from DnnController, (2) implement IServiceRouteMapper and then (3) Create a method RegisterRoutes which will register your module’s routes with DNN. Here my first route is called “default”, does not take anything in the query (third parameter) and instructs the routing engine to look for “ServiceController” in the “DotNetNuke.Modules.Html” namespace and find method (action) “GetTabs”. We then create this method “GetTabs” such that it returns a list of Tabs in Json. So far so good. If you compile this you’ll see something like this appear when you call your service route:

Request Response
GET DesktopModules/Html/API
{"55":"Home","57":"Module"}

This is a list of just two tabs: “Home” and “Module”. Now what about security? You’ll notice there are no admin tabs present. This is because I was not logged in and the GetPortalTabs method above includes the CheckViewPermission. If I log in as admin you’d get this:

Request Response
GET DesktopModules/Html/API
{"55":"Home","57":"Module","59":"Site Settings","60":"Pages","61":"Extensions","62":"Languages","63":"Skins","64":"Security Roles",
"65":"User Accounts","66":"Vendors","67":"Site Log","68":"Newsletters","69":"File Manager","70":"Recycle Bin","71":"Log Viewer",
"72":"Site Wizard","73":"Google Analytics","74":"Taxonomy","75":"Search Engine SiteMap"}

OK, so what is happening here? Well, we’re letting the user go to the method regardless of login status. So a response is always generated. However, when we’re logged in we receive a cookie from DNN and that gets picked up on this request and DNN knows who we are and sets the user in the context. So that is really just like with any regular web request. There is no new magic here. But it is good to know that this still works. Now we’re going to make things a little harder. For in the next step we will ask for a list of modules on a single tab that are Text/Html modules.

Step 2: Getting the modules

We need to create a second route and we need to create a new method to get the modules.

   routeManager.MapRoute("Html", "modules", "{tabid}", new { controller = "Service", action = "GetModules" }, new { tabid = @"\d*" }, new[] { "DotNetNuke.Modules.Html" });

...

  [DnnAuthorize(AllowAnonymous = true)]
  public ActionResult GetModules(int tabid)
  {
   DotNetNuke.Entities.Modules.ModuleController mc = new DotNetNuke.Entities.Modules.ModuleController();
   Dictionary<string, string> modules = new Dictionary<string, string>();
   foreach (DotNetNuke.Entities.Modules.ModuleInfo m in GetModulesByPageAndDefinition(PortalSettings.PortalId, tabid, "Text/HTML"))
   {
    if (ModulePermissionController.HasModuleAccess(Security.SecurityAccessLevel.View, "", m))
    {
     modules.Add(m.ModuleID.ToString(), m.ModuleTitle);
    }
   }
   return Json(modules, JsonRequestBehavior.AllowGet);
  }

  public List GetModulesByDefinition(int portalID, string friendlyName)
  {
   return DotNetNuke.Common.Utilities.CBO.FillCollection(DotNetNuke.Data.DataProvider.Instance().GetModuleByDefinition(portalID, friendlyName));
  }

  public IEnumerable GetModulesByPageAndDefinition(int portalID, int tabID, string friendlyName)
  {
   return GetModulesByDefinition(portalID, friendlyName).Where(x => x.TabID == tabID);
  }

The second route becomes a little more complex. It still points to this class and will look for the method “GetModules”. But it’ll only fire if there is a set of digits after our base route. These digits get put into a variable called “tabid”. This parameter is passed into our method “Modules” and we get a list of Text/Html modules which the current user has view access to. In my test site this works out as follows:

Request Response
GET DesktopModules/Html/API/57
{"382":"Test text module"}

There is just one module on this page and it’s called “Test text module” as you can see. But we omitted one thing: any user can request this route and still get an answer. Instead, I’d prefer to have access to the page checked before we even get to the code. This is done with the authorization attribute. As we saw in the mentioned post, we have a default attribute called DnnAuthorize which takes as parameters Roles, AllowAnonymous, and RequiresHost. This is the core logic for it:

   IPrincipal user = context.User;
   if (!AllowAnonymous)
   {
    if (user == null || !user.Identity.IsAuthenticated)
    {
     return false;
    }
   }

   if (RequiresHost)
   {
    if (!CurrentUser.IsSuperUser)
    {
     return false;
    }
   }

   if (_rolesSplit.Any())
   {
    if (!_rolesSplit.Any(CurrentUser.IsInRole))
    {
     return false;
    }
   }

As you can see this does some really basic authorization. The red herring here is the Roles parameter. The roles are in plain English text and so there are two reasons you’ll probably not use that parameter: (1) you rarely code for roles, instead you code for permissions, and (2) anything in text is subject to change in other languages. In case you’re confused: it is not uncommon for a French site to have removed the “Administrators” role and replaced it with “Administrateurs”. And voila, your code no longer works as expected. So the DnnAuthorize attribute is of little use in real-world authorization. What we need to do is to create our own version of an authorization attribute.

Step 3: Roll your own authorization attribute

Now we’re getting to the nitty-gritty of this. Let’s grab a spanner, follow me to the engine room and see if we can do this. The trick is this. Authorization cannot be done while you’re running the method. It is done before by the MVC framework. It took me a while to get my head round to this. So in essence you’ll be creating code that sits in another class which does just the authorization. Nothing else. I’ll create an attribute to handle the authorization of the user to the tab he/she is requesting the modules from. We’ll call this the HtmlTabAuthorize attribute:

using DotNetNuke.Common.Utilities;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Modules.Internal;
using DotNetNuke.Security;
using DotNetNuke.Security.Permissions;
using DotNetNuke.Entities.Tabs;

namespace DotNetNuke.Modules.Html
{
 public class HtmlTabAuthorizeAttribute : DotNetNuke.Web.Services.AuthorizeAttributeBase
 {
  protected override bool AuthorizeCore(HttpContextBase context)
  {
   int tabid = -1;
   string url = context.Request.RawUrl;
   Match m = Regex.Match(url, "Html/API/(\\d+).*");
   if (m.Success)
   {
    Int32.TryParse(m.Groups[1].Value, out tabid);
   }
   TabInfo t = CBO.FillObject<TabInfo>(DotNetNuke.Data.DataProvider.Instance().GetTab(tabid));
   if (t != null)
   {
    return TabPermissionController.CanViewPage(t);
   }
   return false;
  }
 }
}

As you can see we have a main entry point which is a call to “AuthorizeCore” which hands us the context of the call. So that’s all we have. We need to somehow get at the tab id and it is not handed over to us automatically as with the GetModules method. What I’ve done here is to regex it out of the url. I then get the tab and subsequently let DNN decide if the current user can view that tab. It is simple enough and it illustrates how you need to carefully plan your routes.

Now we add this attribute to the GetModules method:

  [DnnAuthorize(AllowAnonymous = true)]
  [HtmlTabAuthorizeAttribute()]
  public ActionResult GetModules(int tabid)
  {
...

As you can see these attributes can be stacked. I needed to include the “let anonymous users through” bit of DnnAuthorize (I’m working with CTP 2 here). It seems superfluous, but if not included the authorization will only succeed if you’re host. The result is a neat login popup if you try to access a page you don’t have access to. Enter your DNN credentials and you’ll get the right reply.

image

So what’s with that popup? Ah. That, my friend, is the all new digest authentication kicking in. Digest? Yes, digest. Let me explain.

Intermezzo: Digest authentication

For the security buffs out there: DotNetNuke now includes its own digest authentication implementation. This is pretty cool. It means we finally have something safer than basic authentication that we can use for these services. In a nutshell the difference comes down to this. Basic authentication sends your login details over to the server in clear text. That is: your username and your password. That is an obvious security risk. The fact is that most sites run this way. Webforms and otherwise. The only way to hide the login details is to go https, but that is a bridge too far for many. Any time you enter your password on a page which is plain old http, you know it is going over the wire. And not as asterisks! So apart from https are there any alternatives? Yes, digest for instance. This authentication mechanism is based on you signing your username and password together with some unique info from the server which the server then verifies. The great thing is that only the signature makes it across. Not the password itself. And of course the signature is different for every time the conversation takes place. Want to know more:

https://en.wikipedia.org/wiki/Digest_authentication

The great thing about this is that you could now code an app (i.e. like on an iPhone) that stores your password in the device and authenticates you on your DNN site without passing this over the air. That is essential IMO to future mobile development for DNN as mobile device traffic is particularly vulnerable to snooping.

Step 4: Wiring up access to the module content

Now we apply what we know and complete our (relatively) simple module content browser. We add a route and method for the module’s content:

   routeManager.MapRoute("Html", "module", "{tabid}/{moduleid}", new { controller = "Service", action = "GetContents" }, new { tabid = @"\d*", moduleid = @"\d*" }, new[] { "DotNetNuke.Modules.Html" });

...

  [DnnAuthorize(AllowAnonymous = true)]
  [HtmlModuleAuthorizeAttribute(AccessLevel = Security.SecurityAccessLevel.View)]
  public ActionResult GetContents(int tabid, int moduleid)
  {
   var objHtmlText = (HtmlTextInfo)(DotNetNuke.Common.Utilities.CBO.FillObject(DataProvider.Instance().GetTopHtmlText(moduleid, true), typeof(HtmlTextInfo)));
   return Json(objHtmlText.Content, JsonRequestBehavior.AllowGet);
  }

And we add a new authorization attribute:

 public class HtmlModuleAuthorizeAttribute : DotNetNuke.Web.Services.AuthorizeAttributeBase
 {
  public HtmlModuleAuthorizeAttribute()
  {
   AccessLevel = SecurityAccessLevel.Host;
  }

  public SecurityAccessLevel AccessLevel { get; set; }

  protected override bool AuthorizeCore(HttpContextBase context)
  {
   var activeModule = context.FindModuleInfo();

   if (activeModule != null)
   {
    return HasModuleAccess(AccessLevel, "", activeModule);
   }

   return false;
  }

  public static bool HasModuleAccess(SecurityAccessLevel accessLevel, string permissionKey, ModuleInfo moduleConfiguration)
  {
   bool isAuthorized = false;
   UserInfo userInfo = UserController.GetCurrentUserInfo();
   if (userInfo != null && userInfo.IsSuperUser)
   {
    isAuthorized = true;
   }
   else
   {
    switch (accessLevel)
    {
     case SecurityAccessLevel.Anonymous:
      isAuthorized = true;
      break;
     case SecurityAccessLevel.View:
      if (ModulePermissionController.CanViewModule(moduleConfiguration))
      {
       isAuthorized = true;
      }
      break;
     case SecurityAccessLevel.Edit:
      if (TabPermissionController.CanAddContentToPage())
      {
       isAuthorized = true;
      }
      else
      {
       if (string.IsNullOrEmpty(permissionKey))
       {
        permissionKey = "CONTENT,DELETE,EDIT,EXPORT,IMPORT,MANAGE";
       }
       if (moduleConfiguration != null && ModulePermissionController.CanViewModule(moduleConfiguration) &&
           (ModulePermissionController.HasModulePermission(moduleConfiguration.ModulePermissions, permissionKey) || ModulePermissionController.HasModulePermission(moduleConfiguration.ModulePermissions, "EDIT")))
       {
        isAuthorized = true;
       }
      }
      break;
     case SecurityAccessLevel.Admin:
      isAuthorized = TabPermissionController.CanAddContentToPage();
      break;
     case SecurityAccessLevel.Host:
      break;
    }
   }
   return isAuthorized;
  }
 }

 public static class HttpContextExtensions
 {
  private const string ModuleIdKey = "ModuleId";
  private const string TabIdKey = "TabId";

  public static int FindTabId(this HttpContextBase context)
  {
   int res = -1;
   string url = context.Request.RawUrl;
   Match m = Regex.Match(url, "Html/API/(\\d+).*");
   if (m.Success)
   {
    Int32.TryParse(m.Groups[1].Value, out res);
   }
   return res;
  }

  public static int FindModuleId(this HttpContextBase context)
  {
   int res = -1;
   string url = context.Request.RawUrl;
   Match m = Regex.Match(url, "Html/API/\\d+/(\\d+).*");
   if (m.Success)
   {
    Int32.TryParse(m.Groups[1].Value, out res);
   }
   return res;
  }

  public static ModuleInfo FindModuleInfo(this HttpContextBase context)
  {
   var tabId = context.FindTabId();
   var moduleId = context.FindModuleId();

   if (moduleId != Null.NullInteger && tabId != Null.NullInteger)
   {
    return TestableModuleController.Instance.GetModule(moduleId, tabId);
   }

   return null;
  }
 }

This code is based on the DnnModuleAuthorizeAttribute which is in the Core. The latter, though, requires you to include ModuleId and TabId as querystring parameters. This was not the way we wanted to route this. We wanted a straight RESTful implementation. So here I’ve done the parsing out of the tab id and module id from the url directly instead of relying on the core to do this based on those parameters.

I also had to copy in and modify ModulePermissionController.HasModuleAccess as in the check for View permissions is erroneously just wants to use what it considers the context TabId. This is not set in our case so will default to the Home tab.

Request Response
GET DesktopModules/Html/API/57/382
"&lt;p&gt;This is my test content&lt;/p&gt;"

Wrapping up

It is hard to hide my enthusiasm for this new part of the framework. As a core team member I was privy to this information a long time ago, but I’ve had to sit tight for a while before I got my hands on it. In the meantime I contributed the digest authentication code to the project which I’ve used for a long time now in the WebDAV implementation of Document Exchange. I strongly believe the result is an API which opens the door to secure mobile development against third party modules in DotNetNuke. Wow. That is a future most developers are probably eager to get involved in. Now if only someone would convince Apple/Google to abandon Objective-C/Java in favour of .net …

Next step would be to implement the PUT verb. After all, we want to make this a read/write service in some point in the future. Maybe hook in your own desktop editor for your DNN. Inspired?

Comments

Comment Form

Only registered users may post comments.

NewsArchives


September 2014 (19)
August 2014 (25)
July 2014 (17)
June 2014 (10)
May 2014 (6)
April 2014 (9)
March 2014 (3)
February 2014 (4)
January 2014 (8)
December 2013 (5)
November 2013 (2)
October 2013 (9)
September 2013 (10)
August 2013 (8)
July 2013 (4)
June 2013 (8)
May 2013 (13)
April 2013 (2)
March 2013 (7)
February 2013 (7)
January 2013 (10)
December 2012 (6)
November 2012 (20)
October 2012 (12)
September 2012 (27)
August 2012 (29)
July 2012 (22)
June 2012 (17)
May 2012 (23)
April 2012 (24)
March 2012 (27)
February 2012 (21)
January 2012 (12)
December 2011 (18)
November 2011 (20)
October 2011 (27)
September 2011 (17)
August 2011 (18)
July 2011 (45)
June 2011 (22)
May 2011 (23)
April 2011 (19)
March 2011 (36)
February 2011 (19)
January 2011 (22)
December 2010 (29)
November 2010 (37)
October 2010 (32)
September 2010 (43)
August 2010 (46)
July 2010 (37)
June 2010 (46)
May 2010 (29)
April 2010 (38)
March 2010 (27)
February 2010 (33)
January 2010 (34)
December 2009 (13)
November 2009 (20)
October 2009 (29)
September 2009 (18)
August 2009 (29)
July 2009 (19)
June 2009 (18)
May 2009 (23)
April 2009 (16)
March 2009 (13)
February 2009 (20)
January 2009 (25)
December 2008 (25)
November 2008 (29)
October 2008 (34)
September 2008 (33)
August 2008 (36)
July 2008 (31)
June 2008 (25)
May 2008 (26)
April 2008 (33)
March 2008 (31)
February 2008 (24)
January 2008 (18)
December 2007 (27)
November 2007 (51)
October 2007 (24)
September 2007 (32)
August 2007 (24)
July 2007 (20)
June 2007 (28)
May 2007 (27)
April 2007 (24)
March 2007 (47)
February 2007 (21)
January 2007 (41)
December 2006 (21)
November 2006 (16)
October 2006 (24)
September 2006 (36)
August 2006 (30)
July 2006 (31)
June 2006 (37)
May 2006 (13)
April 2006 (13)
March 2006 (18)
February 2006 (20)
January 2006 (13)
December 2005 (6)
November 2005 (15)
October 2005 (15)
September 2005 (16)
August 2005 (7)
April 2005 (1)
March 2004 (4)
February 2004 (6)
January 2004 (1)
November 2003 (4)
October 2003 (22)
September 2003 (22)
August 2003 (15)
July 2003 (14)

Copyright 2014 by DNN Corp | Terms of Use | Privacy | Design by Parker Moore Design