6 votes

Étrange bug intermittent de gestion des erreurs lors de l'appel de UserPrinciapl.GetGroups dans System.DirectoryServices.AccountManagement

Contexte

Nous avons une application web asp.net 4.0 écrite en C# qui appelle un service web .net 3.5 écrit en C#. Le service web reçoit un identifiant d'utilisateur et renvoie une liste de données en fonction des groupes Active Directory auxquels l'utilisateur appartient.

Le service web utilise la version .net 3.5 de System.DirectoryServices.AccountManagement pour obtenir les Sids des groupes auxquels l'utilisateur appartient.

L'appel à UserPrincipal.GetGroups échoue par intermittence avec l'erreur ci-dessous. Il y a eu de très longues périodes de temps entre les occurrences, mais lorsqu'elles se sont produites, elles ont été répétées pendant plusieurs minutes. Le problème s'est produit pour différents utilisateurs AD.

La trace de la pile de cette exception n'avait aucun sens pour nous. Nous avons passé beaucoup de temps à examiner le code de Microsoft AD dans Reflector/ILSpy, mais nous n'avons pas pu aller au-delà de l'appel à IADsPathName.Retrieve.

Exception

System.NotSupportedException: Specified method is not supported.
at System.Web.HttpResponseStream.get_Position()
at System.Drawing.UnsafeNativeMethods.ComStreamFromDataStream.Seek(Int64 offset, Int32 origin)
at System.DirectoryServices.AccountManagement.UnsafeNativeMethods.IADsPathname.Retrieve(Int32 lnFormatType)
at System.DirectoryServices.AccountManagement.ADStoreCtx.LoadDomainInfo()
at System.DirectoryServices.AccountManagement.ADStoreCtx.get_DnsForestName()
at System.DirectoryServices.AccountManagement.ADStoreCtx.GetGroupsMemberOf(Principal p)
at System.DirectoryServices.AccountManagement.Principal.GetGroupsHelper()
at System.DirectoryServices.AccountManagement.Principal.GetGroups()
at Data.SoftwarePublishingItemData.GetSids(String requestedForUserId)
at Data.SoftwarePublishingItemData.GetSoftwarePublishingItems(IDatabaseContext dbContext, GetSoftwarePublishingItemsSettings settings, XBXmlDocument parameters)
at Web.GetSoftwarePublishingItems.GetFlexiFieldData(String xml)

Code à reproduire

Veuillez noter que la méthode CauseNotSupportedException imite le code qui ne s'exécute pas dans notre application mais dans un code situé ailleurs dans l'environnement que nous ne contrôlons pas.

class Program
{
    static void Main(string[] args)
    {
        CauseNotSupportedException();

        string samAccountName = "domain.user";

        using (var principalContext = new PrincipalContext(ContextType.Domain))
        {
            using (var userPrincipal = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, samAccountName))
            {
                if (userPrincipal == null)
                    throw new ActiveDirectoryObjectNotFoundException();

                using (var groups = userPrincipal.GetGroups())
                {
                    foreach (GroupPrincipal group in groups)
                    {
                        Console.WriteLine(group.Sid);
                    }
                }
            }
        }
    }

    public static void CauseNotSupportedException()
    {
        using (var b = new Bitmap(500, 500, PixelFormat.Format32bppArgb))
        {
            b.Save(new FakeStream(), ImageFormat.Png);
        }
    }
}

Implémentation de Stream pour imiter le comportement de HttpResponseStream

public class FakeStream : Stream
{
    public override bool CanRead { get { return false; } }
    public override bool CanSeek { get { return false; } }
    public override bool CanWrite { get { return true; } }

    public override void Flush() { }

    public override long Length { get { throw new NotSupportedException("No Seek"); } }

    public override long Position
    {
        get { throw new NotSupportedException("No Seek"); }
        set { throw new NotSupportedException("No Seek"); }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        throw new InvalidOperationException("Write only stream");
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        throw new NotSupportedException("net_noseek");
    }

    public override void SetLength(long value) { }

    public override void Write(byte[] buffer, int offset, int count) { }
}

Questions

  1. Si vous exécutez l'exemple ci-dessus, l'erreur qui se produit dans la méthode CauseNotSupportedException est déclenchée dans l'appel à GetGroups. Comment cela se fait-il ? Nous vous remercions de nous faire part de toute théorie ou de tout commentaire supplémentaire.
  2. Avez-vous des suggestions sur la façon d'approfondir la question ?
  3. Avez-vous d'autres suggestions que d'attraper l'exception et de réessayer ? C'est notre méthode de travail actuelle.

Gracias.

Clarification

Je ne suis pas sûr d'avoir été très clair dans mes explications, alors voici quelques précisions. Tout d'abord, je suis satisfait du code de l'annuaire actif qui obtient les Sids. Il fait ce que je veux qu'il fasse et je ne pense pas que le problème soit là en tant que tel. Le vrai problème est que lorsqu'une erreur se produit dans un autre code non lié (ce n'est pas dans notre application), l'erreur se manifeste dans l'appel GetGroups, d'où l'étrange trace de pile avec l'erreur survenant à l'origine dans System.Web.HttpResponseStream.get_Position(). Dans l'exemple d'application, NotSupportedException se produit dans CauseNotSupportedException mais le code ne s'arrête pas là, il s'arrête lors de l'appel à GetGroups. Si vous commentez CauseNotSupportedException() dans l'application exemple, l'erreur ne se produit jamais.

Je ne vois pas très bien comment cela peut se produire.

4voto

Simon Vane Points 408

Après un appel à l'assistance, Microsoft a publié un correctif pour ce problème. Voir le lien ci-dessous.

La cause déclarée est : "Ce problème se produit parce que l'espace de noms System.DirectoryServices.AccountManagement est une enveloppe fine pour l'API native Active Directory Service Interfaces (ADSI). L'interface IErrorInfo qui est mise en œuvre par l'interface IADsPathName répond aux exceptions qu'ADSI ne lève pas. Lorsqu'il n'y a pas d'exception ADSI sur la pile, l'interface IErrorInfo lance l'exception qui se trouve en haut de la pile, même si l'exception est gérée par un autre gestionnaire dans l'application."

http://support.microsoft.com/kb/2683913

Merci à ceux qui ont fait des suggestions.

0voto

DJ KRAZE Points 8546

Si vous utilisez .NET 3.5 ou une version ultérieure, vous pouvez utiliser la nouvelle fonction System.DirectoryServices.AccountManagement (S.DS.AM), ce qui rend les choses beaucoup plus faciles qu'auparavant.

Lisez tout à ce sujet ici : [Gestion des principes de sécurité de l'annuaire dans le .NET Framework 3.5][1].

Fondamentalement, vous devez avoir un "contexte principal" (typiquement votre domaine), un principal utilisateur, et ensuite vous obtenez ses groupes très facilement :

public List<GroupPrincipal> GetGroups(string userName)
{
   List<GroupPrincipal> result = new List<GroupPrincipal>();

   // establish domain context
   PrincipalContext yourDomain = new PrincipalContext(ContextType.Domain);

   // find your user
   UserPrincipal user = UserPrincipal.FindByIdentity(yourDomain, username);

   // if found - grab its groups
   if(user != null)
   {
      PrincipalSearchResult<Principal> groups = user.GetAuthorizationGroups();

      // iterate over all groups
      foreach(Principal p in groups)
      {
         // make sure to add only group principals or change this to add to a list or varible if needed.
         if(p is GroupPrincipal)
         {
             result.Add(p);
         }
      }
   }

   return result;
}

Afin d'accéder à certaines propriétés, qui ne sont pas surfacées sur le UserPrincipal vous devez creuser dans l'objet sous-jacent DirectoryEntry :

public string GetDepartment(Principal principal)
{
    string result = string.Empty;

    DirectoryEntry de = (principal.GetUnderlyingObject() as DirectoryEntry);

    if (de != null)
    {
       if (de.Properties.Contains("samAccountName"))
       {
          result = de.Properties["samAccountName"][0].ToString();
       }
    }

    return result;
}

//Change this Method to fit what ever your needs desire.. 
public string GetDepartment(string username)
{
    string result = string.Empty;

    // if you do repeated domain access, you might want to do this *once* outside this method, 
    // and pass it in as a second parameter!
    PrincipalContext yourDomain = new PrincipalContext(ContextType.Domain);

    // find the user
    UserPrincipal user = UserPrincipal.FindByIdentity(yourDomain, username);

    // if user is found
    if(user != null)
    {
       // get DirectoryEntry underlying it
       DirectoryEntry de = (user.GetUnderlyingObject() as DirectoryEntry);

       if (de != null)
       {
          if (de.Properties.Contains("department"))
          {
             result = de.Properties["department"][0].ToString();
          }
       }
    }

    return result;
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X