Monday, May 31, 2010

Problem with LdapMembershipProvider and users which belong to more than 1000 AD groups

Recently I investigated unclear issue on production environment: from appropriate moment special user’s account which was used for special purposes (some kind of “common” user account) stopped login into Sharepoint portal. We have web application which was extended to Internet zone with FBA using LdapMembershipProvider and LdapRoleProvider, i.e. all users are stored in AD. What was strange is that on local development environment common account still worked well.

In FBA zone we use custom login page with standard ASP.Net Login control. By default it uses the following code when user is authenticated:

   1: private void AuthenticateUsingMembershipProvider(AuthenticateEventArgs e)
   2: {
   3:     e.Authenticated = LoginUtil.GetProvider(this.MembershipProvider).
   4: ValidateUser(this.UserNameInternal, this.PasswordInternal);
   5: }
I.e. LdapMembershipProvider.ValidateUser() method returned false. I checked that user name and password were specified correctly. After unsuccessful login attempt the following record appeared in Sharepoint logs in 12/logs folder:

Logon failure: unknown user name or bad password.

at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)

at System.DirectoryServices.DirectoryEntry.Bind()

at System.DirectoryServices.DirectoryEntry.get_NativeObject()

at Microsoft.Office.Server.Security.LdapMembershipProvider.ValidateUser(String name, String password)

I checked LdapMembershipProvider.ValidateUser() method in Reflector. It failed on the following lines:

   1: public override bool ValidateUser(string name, string password)
   2: {
   3:     ...
   4:     if (((username != null) && (username.Length > 0)) &&
   5: ((password != null) && (password.Length > 0)))
   6:     {
   7:         entry = new DirectoryEntry(DS.GetLDAPURL(this._LDAPServer,
   8: this._LDAPPort, "RootDSE"), username, password);
   9:         entry.AuthenticationType = LDAP.GetDSAuth(this._LDAPUseSSL);
  10:         object nativeObject = entry.NativeObject;
  11:         return true;
  12:     }
  13:     return false;
  14: }

So our “common” user account failed to bind to directory entry using its user name and password because of some reasons. And this was really strange because other users still was able to login.

I added this common account into Domain Admins group and tried to login into WFE directly under this account using Remote Desktop. But Windows didn’t allow me to login with the following message:

During a logon attempt, the user's security context accumulated too many security IDs.

This was actually the reason: we have too many groups membership for our common account (because of special business requirements). After this I found this article in Microsoft knowledge base which explains the reasons of this issue and that this behavior is by design:

This behavior occurs because Windows systems contain a limit that prevents a user's security access token from containing more than 1000 security identifiers (SIDs). This means that when a user is being validated for access rights to establish a new session with a server, that user must not be a member of more than 1000 groups in that server's domain. If this limit is exceeded, access to the server is denied, and the error code 1384 is returned to the user.

Microsoft has confirmed that this is a problem in the Microsoft products that are listed in the "Applies to" section.This behavior is by design.

As “Applies To” section mentions that this behavior valid also for Windows Vista and Server 2008 I think that there is no actually workaround for this at the current moment (I’m not sure about Windows 7 and Server 2008 R2). So we removed unnecessary groups memberships from our common user account in order to allow login into Sharepoint portal under this account.

Update 2010-06-02: note that Windows takes into account total number of groups which user belongs to recursively (i.e. not only those groups which user belongs to directly). E.g. User1 is member of Group1 and Group1 is in turn member of Group2_1, Group2_2, …, Group2_1000. In this case User1 will be member of 1001 groups (1 Group1 + 1000 Group2_n).

Saturday, May 22, 2010

Cross-site and cross-site collection navigation in Sharepoint - part 1

In one of my previous posts I wrote about basic information about navigation architecture in Sharepoint. With this article I continue series of posts about navigation in Sharepoint. Here I will use information described in the previous post, so I recommend to read it before this one.

In real life Sharepoint applications it is common practice to have consistent global navigation (or top navigation bar) across site. It is achievable out of the box with Sharepoint navigation architecture. But it is also common situation when site collection (SPSite) contains multiple sites (SPWeb) and there is a requirement to have consistent navigation when users goes through sites within site collection (it can be navigation items from one of the particular sites, e.g. site collection root site, or it can be navigation items retrieved from custom storage like xml file, database, etc). More complicated case – is when navigation should be preserved within multiple site collections.

This is problem scope which I would like to address in the several posts including this one. I will go from simple to complex and start from most simple scenario: when we have single site collection and several sites under it. First case will assume that we are limited by WSS only. It means neither site collection root web nor its subsites are publishing sites. Publishing sites (sites with publishing infrastructure) have their own navigation API (see previous post) – and it is much more complicated task to preserve cross-publishing sites navigation. I will describe it in next part of navigation-related posts (Update 2010-12-19: see Cross-site and cross-site collection navigation in Sharepoint - part 2: publishing sites).

Sites which were created using one of WSS template initially use one of the following navigation data providers:

  • SPNavigationProvider - provides a base class for Windows SharePoint Services site-map providers that are specialized for SharePoint site navigation;
  • SPSiteMapProvider - provides the SiteMapNode objects that constitute the global content breadcrumb, which represents objects in the site hierarchy above the current site;
  • SPContentMapProvider - provides methods and properties for implementing a site map provider for contents of a Windows SharePoint Services site. This class provides the SiteMapNode objects that constitute the content breadcrumb, where “content” referes to the lists, folders, items, and list forms composing the breadcrumb;
  • SPXmlContentMapProvider - provides methods and properties for implementing a site map provider for contents of a Windows SharePoint Services site. This class provides the SiteMapNode objects that constitute the content breadcrumb, where “content” referes to the lists, folders, items, and list forms composing the breadcrumb.

In order to cover 2 mentioned cases (navigation is preserved from one of the subsites and navigation is preserved globally) we need consider 2 of these navigation providers: SPNavigationProvider  and SPXmlContentMapProvider.

SPNavigationProvider  will help us if need to keep navigation from one particular site. If we will look the code of SPNavigationProvider using reflector we will found that it has public virtual Web property which returns current SPWeb instance:

   1: public class SPNavigationProvider : SiteMapProvider
   2: {
   3:     ...
   4:     protected virtual SPWeb Web
   5:     {
   6:         get
   7:         {
   8:             return SPControl.GetContextWeb(HttpContext.Current);
   9:         }
  10:     }
  11: }

But what is more important is that SPNavigationProvider uses this property as a source for navigation data, i.e. it will return collection of SiteMapNode instances for this web site. So in order to make navigation consistent within all sites we need to implement custom navigation provider by inheriting SPNavigationProvider and override its Web property

   1: public class SingleWebNavigationProvider : SPNavigationProvider
   2: {
   3:     ...
   4:     protected override SPWeb Web
   5:     {
   6:         get
   7:         {
   8:             return SPContext.Current.Site.RootWeb;
   9:         }
  10:     }
  11: }

Here for example purposes site collection root site is used as a source of navigation data, i.e. custom navigation provider SingleWebNavigationProvider will always return navigation items from this SPWeb. In order to use this custom navigation provider in site we need to register it in web.config file of web application:

   1: <siteMap defaultProvider="SPNavigationProvider" enabled="true">
   2:   <providers>
   3:     ...
   4:     <add name="SingleWebNavigationProvider"
   5: type="NamespaceName.SingleWebNavigationProvider, AssemblyName" />
   6:   </providers>
   7: </siteMap>

And then modify master page:

   1: <asp:SiteMapDataSource
   2:   ShowStartingNode="False"
   3:   SiteMapProvider="SingleWebNavigationProvider"
   4:   id="topSiteMap"
   5:   runat="server"/>

After that whenever site you go – navigation data will be used from site collection root web. If you want to preserve navigation across site collections – you need to override Web property so it should return the same site regardless of current site collection (e.g. you can add URL of source web site using custom property of SingleWebNavigationProvider in web.config file and override Initialize(…) method) and modify master pages of all site collections in order to use SingleWebNavigationProvider in them.

Second case – preserving navigation globally can be achieved by using SPXmlContentMapProvider. Specify web application-relative path in its siteMapFile property in web.config:

   1: <siteMap defaultProvider="SPNavigationProvider" enabled="true">
   2:   <providers>
   3:     ...
   4:     <add name="SPXmlContentMapProvider" siteMapFile="_app_bin/layouts.sitemap"
   5: type="Microsoft.SharePoint.Navigation.SPXmlContentMapProvider, Microsoft.SharePoint,
   6: Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
   7:   </providers>
   8: </siteMap>

and modify master page:

   1: <asp:SiteMapDataSource
   2:   ShowStartingNode="true"
   3:   SiteMapProvider="SPXmlContentMapProvider"
   4:   id="topSiteMap"
   5:   runat="server"/>

After that you will have the same static navigation across all sites. If you want to preserve this navigation across site collections you will also need to modify their master pages.

This is all what I wanted to tell about in part 1. In next part I will show how to preserve cross-site and cross-site collection navigation for publishing sites. Update 2010-12-19: see Cross-site and cross-site collection navigation in Sharepoint - part 2: publishing sites.

Thursday, May 20, 2010

Localization of jQuery.undoable plugin

Before to talk about exact subject of this post I would like to say some words about modality in web applications. As you probably know Phil Haack introduced undoable plugin for jQuery which allows to improve user experience for web apps by getting rid of modal confirm dialogs. You can find demo of this plugin here. I really like that because I think that using of modal dialogs in web applications should be minimized. For me as end user (not as developer) site which implemented with minimum modality looks much more friendly and fun rather then web app with huge amount of modal dialogs. I’m not saying that we should get rid of modal dialog entirely – but we should consider using of more user friendly solutions in those places where modal dialogs aren’t really needed.

Phil’s plugin helps in one of these places: it allows to avoid confirmation dialogs in scenarios where you need to implement a list of deletable items. And it is even more suitable if you need to make deleting cancelable. This is the end of a little preface and now I will describe process of localization of undoable plugin.

Suppose that we have localized web application and we have list of images (table’s header and links texts are on Russian):

image

We want to make it deletable. The process of creating it via jQuery.undoable plugin is quite straightforward and it is explained in Phil’s post and his demos. I will not repeat it. Just will show the end result:

image

All is fine except status message which is shown on English and undo link text near it. Localization of status message (The item has been removed) is quite simple: we need to override OTB formatStatus function of undoable plugin:

   1:  
   2: <script type="text/javascript">
   1:  
   2:     $(function() {
   3:  
   4:         $('a.delete').undoable({
   5:  
   6:             formatStatus: function(data) {
   7:                 data.subject = '<%= Resources.ImageDeleted %>';
   8:                 data.predicate = '';
   9:                 return this.formatStatus.call(this, data);
  10:             }
  11:  
  12:         });
  13:  
  14:     });
</script>

Here Resources.ImageDeleted is the property from strongly typed Resources.resx file. Now result looks better:

image

Localization of undo link text is more tricky. Since it is hardcoded in OTB showUndo function without modification of plugin code I didn’t find a better way to fix it rather than overriding the whole showUndo function by copy-paste it from jquery.undoable.js and replace hardcoded “undo” text with resource string (Phil’s plugin is flexible enough and we can override OTB functions by custom ones). The resulting script is the following:

   1: <script type="text/javascript">
   1:  
   2:     $(function() {
   3:         $('a.delete').undoable({
   4:  
   5:             formatStatus: function(data) {
   6:                 data.subject = '<%= Resources.ImageDeleted %>';
   7:                 data.predicate = '';
   8:                 return this.formatStatus.call(this, data);
   9:             },
  10:  
  11:             // copy paste from original plugin but with localized undo text
  12:             showUndo: function(target, data, options) {
  13:                 var message =
  14:                     (options.formatStatus || this.formatStatus).call(this, data);
  15:  
  16:                 if (target[0].tagName === 'TR') {
  17:                     var colSpan = target.children('td').length;
  18:                     target.after('<tr class="undoable"><td class="status" colspan="' +
  19: (colSpan - 1) + '">' + message + '</td><td class="undo"><a href="#' + data.id +
  20: '"><%= Resources.Undo %></a></td></tr>');
  21:                 }
  22:                 else {
  23:                     var tagName = target[0].tagName;
  24:                     var classes = target.attr('class');
  25:                     target.after('<' + tagName + ' class="undoable ' + classes +
  26: '"><p class="status">' + message + '</p><p class="undo"><a href="#' + data.id +
  27: '"><%= Resources.Undo %></a></p></' + tagName + '>');
  28:                 }
  29:  
  30:                 var undoable = target.next();
  31:  
  32:                 if (options.showingStatus) {
  33:                     options.showingStatus(undoable);
  34:                 }
  35:  
  36:                 undoable.hide().fadeIn('slow').show();
  37:                 if (target.inlineStyling) {
  38:                     (options.applyUndoableStyle ||
  39: $.fn.undoable.applyUndoableStyle).call($.fn.undoable, undoable);
  40:                 }
  41:                 return undoable;
  42:             }
  43:  
  44:         });
  45:  
  46:     });
  47:  
</script>

And the final result is what I wanted:

image

Now we have both status message and undo link text localized. But my opinion is that localization of undo link text is the point of further improvements. Nevertheless thanks to Phil for this plugin.

Thursday, May 6, 2010

Fix dotless for IIS 7 with integrated pipeline mode

Recently I installed ASP.Net MVC site with dotless on production server. Site worked well under Visual Studio built-in web server but after installation on IIS css styles were not applied. In our project we use file with .less.css extension in order to keep intellisense, syntax highlighting and in order to avoid problems with non-standard file extensions on web server. As documentation says we need to register dotless handler for such files in web.config:

   1: <configSections>
   2:     ....
   3:     <section name="dotless"
   4:         type="dotless.Core.configuration.DotlessConfigurationSectionHandler,
   5:             dotless.Core, Version=1.0.0.1, Culture=neutral"/>
   6: </configSections>
   7: <system.web>
   8:     <httpHandlers>
   9:         ...
  10:         <add type="dotless.Core.LessCssHttpHandler,dotless.Core"
  11:             validate="false" path="*.less.css" verb="*"/>
  12:     </httpHandlers>
  13: </system.web>
  14: ...
  15: <dotless minifyCss="false" cacheEnabled="false"/>

And as I said it worked for built-in web server. But when I entered URL http://example.com/css/styles.less.css on production – I get original file without any modifications. I.e. all dotless artifacts like variables (@foo), mixins, etc. were not expanded to valid css.

The problem is that for integrated pipeline mode we need to add one more record into <system.webServer> section in web.config file:

   1: <system.webServer>
   2:     ...
   3:     <handlers>
   4:         ...
   5:         <add name="dotless"
   6:             type="dotless.Core.LessCssHttpHandler,dotless.Core"
   7:             path="*.less.css" verb="*"/>
   8:     </handlers>
   9: </system.webServer>

After this dotless file was successfully transformed into valid css.

Update 2010-05-08: jamesfoster notified that he updated http://www.dotlesscss.org with it