Friday, July 2, 2010

Teach User Manager how to search by email

A couple of days ago one interesting issue popped out in my Inbox. Our customer wanted to search users by email address in User Manager application. Quite legitimate request. Especially if you have an integration with AD or CRM or whatever external security system with large number of users. I tried a couple of configuration changes with no luck. Then checked our support portal knowledge base and saw several request like that. I was surprised by the fact that you cannot search users by email.
After spending several hours investigating how search functionality in User Manager works and doing some "shaman dancing" around it, I ended up with a solution that requires to override a standard Sitecore.Security.Accounts.UserProvider and change its method called "GetUsersByName". As it turned out, search functionality of User Manager calls the GetUserByName method when one searches for a user.
This is how it looks:

namespace Sitecore.SharedSource.Security.Accounts
{
public class UserProvider : Sitecore.Security.Accounts.UserProvider
{
protected override IEnumerable GetUsersByName(int pageIndex, int pageSize, string userNameToMatch)
{
return FindUsers(pageIndex, pageSize, userNameToMatch);
}

protected IEnumerable FindUsers(int pageIndex, int pageSize, string userNameToMatch)
{
Assert.ArgumentNotNull(userNameToMatch, "userNameToMatch");
IEnumerable users = null;
string userWithNoWildcard = StringUtil.RemovePostfix(Settings.Authentication.VirtualMembershipWildcard, StringUtil.RemovePrefix(Settings.Authentication.VirtualMembershipWildcard, userNameToMatch));

if (Regex.IsMatch(userWithNoWildcard, @"^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$"))
{
users = findByEmail(pageIndex, pageSize, userWithNoWildcard);
}
else
{
users = findUsername(pageIndex, pageSize, userNameToMatch);
}
return users;
}

protected IEnumerable findUsername(int pageIndex, int pageSize, string username)
{
int total;
return new Enumerable(delegate
{
return Membership.FindUsersByName(username, pageIndex, pageSize, out total).GetEnumerator();
},
delegate(object user)
{
return User.FromName(((MembershipUser)user).UserName, false);
});
}

protected IEnumerable findByEmail(int pageIndex, int pageSize, string userToMatch)
{
int total;
return new Enumerable(delegate
{
return Membership.FindUsersByEmail(userToMatch, pageIndex, pageSize, out total).GetEnumerator();
},
delegate(object user)
{
return User.FromName(((MembershipUser)user).UserName, false);
});
}
}
}

That's it! Simple enough, isn't it.
Thanks to Alex Shyba who helped me with some useful code!

After compiling the code and moving a dll to the /bin folder, don't forget to change a reference in web.config file to point it to a new UserProvider class.


    <userManager defaultProvider="default" enabled="true">
      <providers>
        <clear />
        <add name="default">
          <x:attribute name="type">Sitecore.SharedSource.Security.Accounts.UserProvider, YOUR_DLL_HERE</x:attribute>
        </add>
      </providers>
    </userManager>

For those who consider this so easy so that it's now worth to create a VS project I've built a ready-to-go Sitecore package with a dll and include file inside. Just install it and give it a try ;).
This is a Sitecore package.

Enjoy!

10 comments:

Deeksha Prabhakar said...

Hi, I tried the email one, and it worked like a fab. Thank you

How to do it for 'Full Name' field in the User Manager?

Thanks,
Deeksha

Ivan said...

Hi Deeksha,
Sitecore membership provider uses default System.Web.Security.Membership methods to search for security accounts. The default one provides only FindUsersByName and FindUsersByEmail methods. If you want to search by any other criteria, you have to extend standard Sitecore membership provider.

Deeksha Prabhakar said...
This comment has been removed by the author.
Deeksha Prabhakar said...

Thanks Ivan. I added some custom code to yours and now we can search by 'FullName', 'UserName' & 'Email' in User Manager. Here is the code if someone is looking for.

The existing FindUsers method will be:


protected IEnumerable FindUsers(int pageIndex, int pageSize, string userNameToMatch)
{
Assert.ArgumentNotNull(userNameToMatch, "userNameToMatch");

string input = StringUtil.RemovePostfix(Settings.Authentication.VirtualMembershipWildcard, StringUtil.RemovePrefix(Settings.Authentication.VirtualMembershipWildcard, userNameToMatch));
if (Regex.IsMatch(input, @"^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$"))
{
return this.findByEmail(pageIndex, pageSize, input);
}

CustomMembership c = new CustomMembership();

IEnumerable fullNameEnumerable = c.GetUsersByFullName(pageIndex, pageSize, userNameToMatch);

IEnumerable userNameEnumerable = this.findUsername(pageIndex, pageSize, userNameToMatch);

List tempFullName = fullNameEnumerable.ToList();

List tempNameList = userNameEnumerable.ToList();
foreach (User ut in tempNameList)
{
if (!tempFullName.Contains(ut))
{
tempFullName.Add(ut);
}
}

IEnumerable res = tempFullName;
return res;
}


Added a new class called CustomMembership

public class CustomMembership
{
public CustomMembership()
{

}

public IEnumerable GetUsersByFullName(int pageIndex, int pageSize, string userNameToMatch)
{
IFilterable i = UserManager.GetUsers();

List uList = new List();
uList = i.ToList();

string nameToMatch = userNameToMatch.ToLower();
int len = nameToMatch.Length;
nameToMatch = nameToMatch.Substring(1, len - 2);

List usersList = new List();

foreach (User u in uList)
{
if (u.Profile.FullName.ToLower().Contains(nameToMatch))
{
usersList.Add(u);
}
}
IEnumerable myEnumerable = usersList;
return myEnumerable;
}
}


Thanks a lot!!!

Ivan said...

Great job, Deeksha! Thanks for sharing the code!

Anonymous said...

I found that GetUsersByName in my custom class only gets called when my user has the "Is Administrator" flag checked.

Have you tried your solution with a user who has only the sitecore\client users and sitecore\client account managing roles?

Anonymous said...

I confirmed that this article only provides half the solution. For non-admin users, you need to customize the security domain class.

Alternately, you could just implement your own UserManager.aspx. At this point, I'm wondering if that might be easier, and lower risk.

Anonymous said...

electronic cigarette, electronic cigarettes, ecig forum, electronic cigarette starter kit, ecig, electronic cigarette reviews

Jorge Lusar said...

I have done a blog post on how to do a Full Text Search in the Sitecore User Manager http://blog.jorgelusar.me/post/full-text-search-sitecore-user-manager/

Anonymous said...

I have to search user in both the columns ,I used below code
IEnumerable usersByEmail = FindByEmail(pageIndex, pageSize, userNameToMatch);
IEnumerable usersByName = FindByUsername(pageIndex, pageSize, userNameToMatch);

var result = usersByEmail.Union(usersByName);
//.GroupBy(x => x.DisplayName)
//.Select(grp => grp.First());

return result;

But I am getting one message "Callback error:- Specified method not supported " and no result.