Active Directory - Adding a user to a group from a non domain-joined computer throws PrincipalException

When using the new System.DirectoryServices.AccountManagement namespace, one might be inclined (as was I) to use the following code to add a user to a group (exception and comments handling removed):

 using (GroupPrincipal groupPrincipal = 
    GroupPrincipal.FindByIdentity(
        domainContext, 
        System.DirectoryServices.AccountManagement.IdentityType.Name, 
        groupName))
{
    if (groupPrincipal != null)
    {
        using (UserPrincipal userPrincipal = 
            UserPrincipal.FindByIdentity(
                domainContext, 
                System.DirectoryServices.AccountManagement.IdentityType.UserPrincipalName, 
                UPN))
        {
            if (userPrincipal != null)
            {
                groupPrincipal.GroupScope = GroupScope.Global;
                groupPrincipal.Members.Add(userPrincipal);
                groupPrincipal.Save();
            }
        }
    }
}

This works just fine if your machine is joined to the domain you're trying to provision. However, if your machine is not joined, the groupPrincipal.Save(); call throws a PrincipalException with an error code 1355 ("Information about the domain could not be retrieved (1355)").

Joining the domain solves this issue.

So what if joining the domain isn't an option?

In this case I found resorting to good old System.DirectoryServices fixes the issue, using the following code fragment:

 using (UserPrincipal userPrincipal =
    UserPrincipal.FindByIdentity(domainContext, UPN))
{
    if (userPrincipal != null)
    {
        using (GroupPrincipal groupPrincipal =
            GroupPrincipal.FindByIdentity(domainContext, groupName))
        {
            if (groupPrincipal != null)
            {
                if (!userPrincipal.IsMemberOf(groupPrincipal))
                {
                    string userSid = string.Format("<SID={0}>", userPrincipal.ToSidString());
                    DirectoryEntry groupDirectoryEntry =
                        (DirectoryEntry)groupPrincipal.GetUnderlyingObject();
                    groupDirectoryEntry.Properties["member"].Add(userSid);
                    groupDirectoryEntry.CommitChanges();
                }
            }
        }
    }
}

ToSidString is an extension method which translates the "objectSid" property. Thanks to Richard for the way better implementation! (ignore "Enforce")

 public static string ToSidString(this Principal principal)
{
    Enforce.IsNotNull<Principal>(principal, "principal");

    SecurityIdentifier sid = principal.Sid;
    if (sid == null || sid.BinaryLength == 0)
    {
        return null;
    }

    byte[] buffer = new byte[sid.BinaryLength];
    sid.GetBinaryForm(buffer, 0);

    string[] hexBytes = Array.ConvertAll(buffer, b => b.ToString("X2", NumberFormatInfo.InvariantInfo));

    return string.Concat(hexBytes);
}

HTH!

Comments

  • Anonymous
    January 05, 2010
    You don't need to access the underlying object to get the Sid - it's exposed as a property on the Principal class: public static string ToSidString(this Principal principal) {    if (null == principal) throw new ArgumentNullException("principal");    SecurityIdentifier sid = principal.Sid;    if (null == sid || 0 == sid.BinaryLength) return null;    byte[] buffer = new byte[sid.BinaryLength];    sid.GetBinaryForm(buffer, 0);    string[] hexBytes = Array.ConvertAll(buffer, b => b.ToString("X2", NumberFormatInfo.InvariantInfo));    return string.Concat(hexBytes); }

  • Anonymous
    January 05, 2010
    The comment has been removed

  • Anonymous
    August 28, 2013
    Thanks for the info.  I also found that groupDirectoryEntry.Properties["member"].Add(userPrincipal.DistinguishedName); also works.