Thursday, October 7, 2010

Analogue of “NT Authority/Authenticated Users” group for FBA in Sharepoint

As you probably know in Sharepoint the single web application can be extended on several authentications zones. Administrator can specify authentication type for each zone: Windows (NTLM) or FBA (via Central Administration > Application Management > Authentication providers). So internal users will be able to login the system using their windows accounts while external users, which access system by Internet – will use FBA. When NTLM authentication is used there is one convenient group “NT Authority/Authenticated Users” which represents all Windows-authenticated users who have access to your network (see this blog post for details). Why it is convenient? Because with this group administrators are able to manage permissions for all authenticated users at once.

But when you use FBA there is no such suitable group which all authenticated FBA users belong to. As you know FBA is implemented by membership and role providers (inheritors of standard MembershipProvider and RoleProvider from System.Web.dll). Actual implementation of these providers depends on physical storage of users accounts (most common are Sql database and Active Directory). If standard Sql database aspnet_db is used as backend storage for FBA then the following standard providers are available:

  • System.Web.Security.SqlMembershipProvider
  • System.Web.Security.SqlRoleProvider

If you use Active Directory you can use the following providers:

  • Microsoft.Office.Server.Security.LdapMembershipProvider
  • Microsoft.Office.Server.Security.LdapRoleProvider

As I said above whatever provider you use, there is no analogue of “NT Authority/Authenticated Users” group for FBA. In order to implement this functionality several ways may be used. But all of them are variations of one single idea: there is one special FBA group AllUsers – and administrators will use this group in order to manage permissions in Sharepoint (i.e. they will asign permissions for this group on various securable objects in MOSS). But how users will become members of this group? You can consider several ways:

  1. If you have full control on user creation (e.g. there is only one single self-registration page in whole system) then you can customize process of registration by adding custom code which will add newly created user to AllUsers group;
  2. If you have no full control on user creation process (i.e. if users are created automatically using BizTalk integration with external CRM system), you may implement Sharepoint job which will run by the scheduler and add all users to special FBA group AllUsers (which should be created before any actions with users). Advantage of this approach is that you will still use standard providers and real FBA group AllUsers, i.e. there will be no custom solutions for it. But from another point of view FBA users will not be member of AllUsers group immediately after their creation. There should be some time until Sharepoint job will run and adds new user accounts to AllUsers group.
  3. If you have no full control on user creation process but previous solution with asynchronous Sharepoint job is not suitable for you, then you can implement custom role provider which will “emulate” this special FBA group AllUsers.

In this post I will describe 3rd method and show how to implement custom role provider in case of Sql database user storage (i.e. if standard aspnet_db is used). But of course it will not limit you from creation of custom providers for Active Directory as ideas described here are general for all types of storages.

I.e. idea is the following: as we use standard Sql database aspnet_db we can still use standard SqlRoleProvider. But in order to add our “virtual” FBA group AllUsers we need to inherit our custom provider from this class and add necessary logic here. Lets look at public interface of SqlRoleProvider class and see what methods we should override in order to add our virtual FBA group. Here they are:

   1: public class SqlRoleProvider : RoleProvider
   2: {
   3:     public override string[] FindUsersInRole(string roleName, string usernameToMatch);
   4:     public override string[] GetAllRoles();
   5:     public override string[] GetRolesForUser(string username);
   6:     public override string[] GetUsersInRole(string roleName);
   7:     public override bool IsUserInRole(string username, string roleName);
   8:     public override bool RoleExists(string roleName);
   9:     ...
  10: }

So in order implemented mentioned idea we need to override these 6 methods consistently. By consistently I mean that they should not contradict with each other. E.g. if method GetRolesForUser() returns “AllUsers” group for user “john” it means that method GetUsersInRole() for role “AllUsers” group should return “john” as well as other users. Also method IsUserInRole() for “AllUsers” group for user “john” should return true, etc. Also we want to keep any existing logic of standard SqlRoleProvider class untouched in order to avoid side effects caused by custom role provider. Custom role provider should be implemented as much safely as possible.

After formalizing the requirements we can implement custom role provider based on SqlRoleProvider. Here is the code:

   1: public class CustomFBARoleProvider : SqlRoleProvider
   2: {
   3:     private const string ALL_USERS_GROUP_NAME = "AllUsers";
   4:  
   5:     public override string[] FindUsersInRole(string roleName, string usernameToMatch)
   6:     {
   7:         try
   8:         {
   9:             if (!this.isSpecialGroup(roleName))
  10:             {
  11:                 return base.FindUsersInRole(roleName, usernameToMatch);
  12:             }
  13:  
  14:             var userNames = getAllUsersSafely(usernameToMatch);
  15:             if (userNames.IsNullOrEmpty())
  16:             {
  17:                 userNames = new string[]{};
  18:             }
  19:             return userNames;
  20:         }
  21:         catch (Exception x)
  22:         {
  23:             return base.FindUsersInRole(roleName, usernameToMatch);
  24:         }
  25:     }
  26:  
  27:     public override string[] GetAllRoles()
  28:     {
  29:         try
  30:         {
  31:             string[] roles = base.GetAllRoles();
  32:             return this.ensureContainsAllUsers(roles);
  33:         }
  34:         catch (Exception x)
  35:         {
  36:             return base.GetAllRoles();
  37:         }
  38:     }
  39:  
  40:     public override string[] GetRolesForUser(string username)
  41:     {
  42:         try
  43:         {
  44:             if (string.IsNullOrEmpty(username))
  45:             {
  46:                 return base.GetRolesForUser(username);
  47:             }
  48:  
  49:             bool? userExists = this.isUserExists(username);
  50:             if (!userExists.HasValue || !userExists.Value)
  51:             {
  52:                 return base.GetRolesForUser(username);
  53:             }
  54:  
  55:             string[] roles = base.GetRolesForUser(username);
  56:             return this.ensureContainsAllUsers(roles);
  57:         }
  58:         catch (Exception x)
  59:         {
  60:             return base.GetRolesForUser(username);
  61:         }
  62:     }
  63:  
  64:     public override string[] GetUsersInRole(string roleName)
  65:     {
  66:         try
  67:         {
  68:             if (!this.isSpecialGroup(roleName))
  69:             {
  70:                 return base.GetUsersInRole(roleName);
  71:             }
  72:  
  73:             var userNames = getAllUsersSafely();
  74:             if (userNames.IsNullOrEmpty())
  75:             {
  76:                 userNames = new string[] { };
  77:             }
  78:             return userNames;
  79:         }
  80:         catch (Exception x)
  81:         {
  82:             return base.GetUsersInRole(roleName);
  83:         }
  84:     }
  85:  
  86:     public override bool IsUserInRole(string username, string roleName)
  87:     {
  88:         try
  89:         {
  90:             if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(roleName))
  91:             {
  92:                 return base.IsUserInRole(username, roleName);
  93:             }
  94:  
  95:             if (this.isSpecialGroup(roleName))
  96:             {
  97:                 bool? userExists = this.isUserExists(username);
  98:                 if (userExists.HasValue && userExists.Value)
  99:                 {
 100:                     return true;
 101:                 }
 102:             }
 103:             return base.IsUserInRole(username, roleName);
 104:         }
 105:         catch (Exception x)
 106:         {
 107:             return base.IsUserInRole(username, roleName);
 108:         }
 109:     }
 110:  
 111:     public override bool RoleExists(string roleName)
 112:     {
 113:         try
 114:         {
 115:             if (this.isSpecialGroup(roleName))
 116:             {
 117:                 return true;
 118:             }
 119:             return base.RoleExists(roleName);
 120:         }
 121:         catch (Exception x)
 122:         {
 123:             return base.RoleExists(roleName);
 124:         }
 125:     }
 126:  
 127:     private string[] getAllUsersSafely()
 128:     {
 129:         try
 130:         {
 131:             var membershipCollection = Membership.GetAllUsers();
 132:             if (membershipCollection == null || membershipCollection.Count == 0)
 133:             {
 134:                 return new string[]{};
 135:             }
 136:             var userNames = membershipCollection.Cast<MembershipUser>()
 137:                 .Select(m => m.UserName).ToArray();
 138:             return userNames;
 139:         }
 140:         catch (Exception x)
 141:         {
 142:             return new string[] { };
 143:         }
 144:     }
 145:  
 146:     private string[] getAllUsersSafely(string userNameToMatch)
 147:     {
 148:         try
 149:         {
 150:             var membershipCollection = Membership.FindUsersByName(userNameToMatch);
 151:             if (membershipCollection == null || membershipCollection.Count == 0)
 152:             {
 153:                 return new string[] { };
 154:             }
 155:             var userNames = membershipCollection.Cast<MembershipUser>()
 156:                 .Select(m => m.UserName).ToArray();
 157:             return userNames;
 158:         }
 159:         catch (Exception x)
 160:         {
 161:             return new string[] { };
 162:         }
 163:     }
 164:  
 165:     private bool? isUserExists(string username)
 166:     {
 167:         try
 168:         {
 169:             var user = Membership.GetUser(username, false);
 170:             return (user != null);
 171:         }
 172:         catch (Exception x)
 173:         {
 174:             return null;
 175:         }
 176:     }
 177:  
 178:     private string[] ensureContainsAllUsers(string[] roles)
 179:     {
 180:         if (roles.IsNullOrEmpty())
 181:         {
 182:             roles = new string[]{};
 183:         }
 184:         var rolesList = new List<string>(roles);
 185:         if (!rolesList.Contains(ALL_USERS_GROUP_NAME))
 186:         {
 187:             rolesList.Add(ALL_USERS_GROUP_NAME);
 188:         }
 189:         return rolesList.ToArray();
 190:     }
 191:  
 192:     private bool isSpecialGroup(string roleName)
 193:     {
 194:         return (string.Compare(roleName, ALL_USERS_GROUP_NAME, true) == 0);
 195:     }
 196: }

Here I used convenient extension method IsNullOrEmpty() for IEnumerable<T> decribed in this post of Phil Haack:

   1: public static class Extensions
   2: {
   3:     public static bool IsNullOrEmpty<T>(this IEnumerable<T> items)
   4:     {
   5:         // see http://haacked.com/archive/2010/06/10/checking-for-empty-enumerations.aspx
   6:         //return (items == null || items.Count() == 0);
   7:         return (items == null || !items.Any());
   8:     }
   9: }

Lets consider for example method IsUserInRole(). At first it checks on null or empty userName or roleName and if one of these conditions is true – call base implementation as we don’t want to handle special cases by ourselves. After this we check whether or not roleName is our special group “AllUsers” (method isSpecialGroup()) and if yes – we just check whether user exists or not. We don’t check user’s membership in any group because it is not important for us in case of “AllUsers” group. All we need to know is whether specified user exists or not. If user exists we return true – it means that user belongs to AllUsers group.

The rest of methods are implemented by the same schema – if “AllUsers” group is specified in params we just introduce special case for it. In all other cases base implementation is called. Doing this we ensure that existing functionality of SqlRoleProvider will be preserved.

After this the only thing which should be done is to install assembly with custom provider into GAC and replace standard SqlRoleProvider in web.config of your web application:

   1: <roleManager enabled="true" defaultProvider="RoleProviderName">
   2:   <providers>
   3:     <add connectionStringName="ConnectionStringName" applicationName="/" name="RoleProviderName" type="Example.CustomFBARoleProvider, Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken=..." />
   4:   </providers>
   5: </roleManager>

As I said above the same idea can be used for others role providers as well. Using described approach you will be able to “virtualize” AllUsers FBA group without having the real AllUsers group in your backend FBA storage.

No comments:

Post a Comment