0

I have a PowerShell script. It is to remove the user from the Administrators group. It's working fine for local and domain users both.

Remove-LocalGroupMember -Group "Administrators" -Member "Admin02"

I want to implement this in C#. I have tried the below code. But it's not working for the domain account.

using (PrincipalContext ctx = new PrincipalContext(ContextType.Machine))
{
    UserPrincipal user = UserPrincipal.FindByIdentity(ctx, IdentityType.Name, args[0]);
    GroupPrincipal group = GroupPrincipal.FindByIdentity(ctx, IdentityType.Name, "Administrators");

    if (user != null && group != null)
    {
        try
        {
            if (group.Members.Remove(user))
            {
                Console.WriteLine("User successfully removed from Local Administrators.");
            }
            else
            {
                Console.WriteLine("User is not a Local Administrator1.");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("User is not a Local Administrator.");
            Console.WriteLine(ex.ToString());
        }
    }
    else
    {
        Console.WriteLine("User was not found.");
    }
}

Its printing User was not found.

I have tried with ContextType.Domain as well.

Edit: Thank you @Gabriel Luci. for providing the solution. I tried his solution, but there is a challenge with it.

I have a domain user name T1/spreda (forward slash).

enter image description here

But when I run your code and see the value of member, it shows users like this (with back slash). I have written this code to check the user.

var group = new DirectoryEntry("WinNT://./Administrators");
                
foreach (var m in (IEnumerable)group.Invoke("Members"))
{
    var member = new DirectoryEntry(m);
    var str = JsonConvert.SerializeObject(member);
    Console.WriteLine(str);
    //if (str.Contains(args[0]))
    //{
    //    Console.WriteLine("Found");
    //    var returnVal = group.Invoke("Remove", new[] { member.Path });
    //    Console.WriteLine(returnVal.ToString());
    //    break;
    

So Is there a way to handle this?

enter image description here

Vivek Nuna
  • 25,472
  • 25
  • 109
  • 197

1 Answers1

1

The problem with using the AccountManagement namespace for this (GroupPrincipal/UserPrincipal) is that the act of creating the object loads all the properties. That means that it needs to connect to AD to create a UserPrincipal object of an AD user, but we don't need to to do that to remove a user from a group.

You can do this with DirectoryEntry, which GroupPrincipal and UserPrincipal use behind the scenes anyway. It would look like this:

var group = new DirectoryEntry("WinNT://./Administrators");

foreach (var m in (IEnumerable)group.Invoke("Members"))
{
    var member = new DirectoryEntry(m);
    if (member.Name == args[0]) {
        group.Invoke("Remove", new [] {member.Path});
        break;
    }
}

This uses the WinNT provider (as opposed to LDAP) to load the local group. The . means the local computer. If you wanted to load the Administrators group from a remote computer, you can put the computer name in the path, like WinNT://computer1/Administrators.

DirectoryEntry is a wrapper around the native Windows C++ COM ADSI objects. In this case, because we're loading a group, it's an IADsGroup object. We use DirectoryEntry.Invoke() to call a method from the underlying object. So group.Invoke("Members") calls IADsGroup::Members, which returns a list of the members. Likewise, group.Invoke("Remove", new [] {member.Path}) calls IADsGroup::Remove to remove the member.

I always prefer using DirectoryEntry, even when working purely with AD objects. It gives you far more control over performance. I talked about that in an article I wrote: Active Directory: Better performance

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • Thank you for your answer, I’ll try this ne update you. One quick question, will it also work on nonenglish systems as well? If they have different language in their systems? Also is there a way to check if a given user is domain user using DirectoryEntry? – Vivek Nuna Feb 28 '23 at 18:07
  • This code will work regardless of if the user is a domain user or local user. For a non-English system, I think the only thing that might be different is the name of the Administrators group. You can change that to the translated name of the group. – Gabriel Luci Feb 28 '23 at 18:19
  • Please refer this comment. Why I’m asking this question to check for a domain user because I already have a code in production which works as expected for a local user to remove from administrators group and I don’t want to impact that. So I can add a check if a user is a domain user then consider your code else the existing code, which is already tested – Vivek Nuna Feb 28 '23 at 18:30
  • But I am running this code on 10000 machines and so is there a way to know what would be the the translated name on each machine? I mean is there a way to know the locale and get the equivalent translation of administrators? – Vivek Nuna Feb 28 '23 at 18:32
  • There is a `GetLocalizedAdministratorGroupName` method in this answer: https://stackoverflow.com/a/70120998/1202807 – Gabriel Luci Feb 28 '23 at 18:54
  • I don't understand why you would want to use different code for local vs. domain. It's easier to use the same code for both rather than trying to figure out if it's a domain user and run different code. You're going to have to change your existing code no matter what you do. But if you really want to check, look at `member.Path`. A local user will have the computer name. A domain user will have the domain name. – Gabriel Luci Feb 28 '23 at 18:57
  • Thank you. I have tried your solution, but I am facing some issues with it. I have updated in the question. Please refer the **Edit** – Vivek Nuna Mar 01 '23 at 10:33
  • The forward slash is just how the domain and username are separated in a WinNT path (since a forward slash is what separates all parts, much like a URL). Why is that a problem? – Gabriel Luci Mar 01 '23 at 13:28
  • problem is that it's not going inside `if (member.Name == args[0]) {` – Vivek Nuna Mar 01 '23 at 14:03
  • I was assuming that `args[0]` was just the username, since `member.Name` is just the username (no domain name). If it includes the domain, then you'll have to do some string manipulation before you compare. – Gabriel Luci Mar 01 '23 at 14:04
  • understood, could you suggest a good way for manipulation? I mean if the username is passed like T1\spreda then with which property shall I compare with? – Vivek Nuna Mar 01 '23 at 14:11
  • 1
    Replace the ``\`` with `/` and look for it in `member.Path` – Gabriel Luci Mar 01 '23 at 14:27
  • Although its removing the user, but `Invoke` method is returning `null`, may I know why? what are different return value in case of success/failure? – Vivek Nuna Mar 01 '23 at 14:48
  • That's normal. If there was an error, it would throw an exception. The exceptions it might throw are in the [documentation for `DirectoryEntry.Invoke()`](https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.directoryentry.invoke) – Gabriel Luci Mar 01 '23 at 14:58