Error adding/removing Outlook Distribution List users through .NET - c#

I am a co-owner of several Outlook Distribution Lists (DL's). I can edit them in Outlook, adding and removing members directly in there. However, I cannot edit them through a simple .NET program:
using System;
using System.DirectoryServices.AccountManagement;
namespace DL_Remove_User
{
class Program
{
static void Main(string[] args)
{
try
{
RemoveUser("My Distribution List", "jimtut");
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.ToString());
}
}
private static void RemoveUser(string dl, string username)
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, "CORP"))
{
GroupPrincipal group = GroupPrincipal.FindByIdentity(pc, dl);
bool result = group.Members.Remove(pc, IdentityType.SamAccountName, username);
Console.WriteLine(result.ToString());
group.Save();
}
}
}
}
This same code works on many other DL's, but for a couple, I get the message "Access is Denied". Full stack trace:
at System.DirectoryServices.Interop.UnsafeNativeMethods.IAds.SetInfo()
at System.DirectoryServices.DirectoryEntry.CommitChanges()
at System.DirectoryServices.AccountManagement.ADStoreCtx.UpdateGroupMembership(Principal group, DirectoryEntry de, NetCred credentials, AuthenticationTypes authTypes)
at System.DirectoryServices.AccountManagement.SDSUtils.ApplyChangesToDirectory(Principal p, StoreCtx storeCtx, GroupMembershipUpdater updateGroupMembership, NetCred credentials, AuthenticationTypes authTypes)
at System.DirectoryServices.AccountManagement.ADStoreCtx.Update(Principal p)
at System.DirectoryServices.AccountManagement.Principal.Save()
at Department_Distribution_Lists.Program.RemoveUser(String dl, String username) in Program.cs:line 483
Of course, "Access is denied" does indicate a permission problem, but I can edit these DL's directly in Outlook. I can even query the DL "owners" in AD/LDAP, and I'm in the collection "msExchCoManagedByLink".
Any thoughts on why I can edit in Outlook but not through .NET?

I finally figured this out. I was confused by this permissions problem since I could edit the DL in Outlook, but not thru .NET.
I started looking for differences between the DL's that I could edit thru .NET and those that I could not, and found the difference was represented in the AD property shown in this GUI as "Manager can update membership list":
Even though I was the "manager" (list owner), if the DL didn't have that property set, I could ONLY edit in Outlook.
I didn't want to have to visually check all the DL's, so I wrote the following code to detect the "real" owners/editors of a DL:
static List<string> GetGroupOwners(GroupPrincipal group)
{
List<string> owners = new List<string>();
DirectoryEntry deGroup = group.GetUnderlyingObject() as DirectoryEntry;
ActiveDirectorySecurity ads = deGroup.ObjectSecurity;
AuthorizationRuleCollection rules = ads.GetAccessRules(true, true, typeof(SecurityIdentifier));
Guid exRight_Member = new Guid("{bf9679c0-0de6-11d0-a285-00aa003049e2}");
foreach (ActiveDirectoryAccessRule ar in rules)
{
if (ar.ActiveDirectoryRights.HasFlag(ActiveDirectoryRights.GenericWrite) || (ar.ObjectType.Equals(exRight_Member) && ar.ActiveDirectoryRights.HasFlag(ActiveDirectoryRights.WriteProperty)))
{
string friendlyName = "";
try
{
friendlyName = ar.IdentityReference.Translate(typeof(NTAccount)).Value;
}
catch
{
}
owners.Add(friendlyName);
}
}
return owners;
}
If you want to know who has Outlook-based edit access, that's different:
static List<string> GetGroupOwnersOutlook(GroupPrincipal group)
{
List<string> owners = new List<string>();
DirectoryEntry deGroup = group.GetUnderlyingObject() as DirectoryEntry;
System.DirectoryServices.PropertyCollection r = deGroup.Properties;
foreach (string a in r["managedBy"])
{
owners.Add(a);
}
foreach (string a in r["msExchCoManagedByLink"])
{
owners.Add(a);
}
return owners;
}

Related

How to check OU accessability in C # , DirectoryEntry?

I'm using DirectoryEntry class
Trying to read all users from specific OU and sub OU's.
Following code is part of task
using(DirectoryEntry dEntry = new DirectoryEntry(dn))
using(DirectorySearcher dSearcher = new DirectorySearcher(dEntry))
{
dSearcher.SearchScope = SearchScope.Subtree;
dSearcher.Filter = "(&(objectClass=user) (objectCategory=person))";
foreach(SearchResult in dSearcher.FindAll())
{
//Do something...
}
}
some of sub OU's are protected from reading for current user.
And i got task exception "one or more error accured"
I'm looking for way to check if OU is not accessible and skip it. And to write that OU to log.
I tried following :
public void GetOu(List<MyUser> list, string path)
{
using (DirectoryEntry dEntry = new DirectoryEntry(path))
using(DirectorySearcher dSearcher = new DirectorySearcher(dEntry))
{
dSearcher.SearcherScope = SearchScope.Subtree;
dSearcher.Filter = "(objectClass=organizationalUnit)";
foreach(SearchResult result in dSearcher.FindAll())
{
GetUsersFromOU(list,result.GetDirectoryEntry());
}
}
}
public void GetUsersFromOU(List<MyUser> list,DirectoryEntry ou)
{
using (DirectorySearcher dSearcher = new DirectorySearcher(ou);
dSearcher.SearchScope = SearchScope.OneLevel;
dSearcher.Filter = "(&(objectClass=user)(objectCategory=person))";
foreach (Search result in dSearcher.FindAll())
{
//Do something.... update list...
}
}
Now get no exceptions and skips not accessible OUs.
1.But still can't find what are the "bad ou"s
2.run time is catastrophic...
You will have to make sure that the user running the process has the appropriate permissions to perform the lookup. This will be your Windows account in a desktop application and the account running the application pool in an ASP.NET application.

Windows Authentication issue in IIS using ASP.NET app in a multi-domain forest

I am trying to implement a web-based intranet project that uses Windows Authentication. The web server (Windows 2012 Server) is in Domain A, but I need to be able to access the site from computers in any domain within the forest. I am just testing this using computers in domains A and B.
In IIS 7.0, I have enabled Windows Authentication and disabled all others, including Anonymous Authentication. In web.config I have:
<authentication mode="Windows"></authentication>
<identity impersonate="true" />
<authorization>
<deny users="?" />
</authorization>
Authenticated users need to be in AD group "TestGroup"; I removed the <allow groups="TestGroup" /> in web.config for testing purposes; I also added a few labels on home page to display my user's ID, groups I belongs to, all members of "TestGroup" and whether I am a member of "TestGroup" or not, jut for debugging purposes.
I believe I have done everything correct so far. With web.config as is:
When I access from a PC in domain A, I am not prompted to log in (whihc is correct since I am already logged in to domain A), and all lebles show correct data.
When I access from a PC in domain B, I am asked to log in (correctly), user ID label shows my ID correctly but shows no groups for my user ID and no group members in "TestGroup".
If I remove the identity section in web.config:
When access from a PC in either domain A or domain B, User ID label shows "NT AUTHORITY/NETWORK SERVICE", nothing listed as groups I belong to (since I am now apparently "NT Authority"), but group members for "TestGroup" are listed correctly. Accessing from PC in domain B pops up login dialog box, correctly.
If I remove the authorization section and leave identity section in web.config:
I am not asked to login from PCs in either domain;
Accessing from PC in domain A shows everything correctly
Accessing from PC in domain B, shows user ID corrctly but no group membership and no users listed for "TestGroup" group.
It seems that in order to be able to show correct user ID, I need to have impersonate set to true; in order to require users to log in form a PC outside domain A I need to have authorization part, but both together don't seem to work from PCs outside domain A.
This is what I am using to get user ID, user group membership and group members:
WindowsIdentity wiUser = WindowsIdentity.GetCurrent();
string sID = wiUser.Name.ToUpper().Repl("DomainA\\", string.Empty);
string sGroupName = #"TestGroup";
List<string> lsGroups = Utils.GetUserADGroups(sID);
bool bTC = lsGroups.Contains(sGroupName);
StringCollection scGroupMembers = Utils.GetGroupMembers(Utils.DomainType., sGroupName);
static string adDomain = "USA.ABC.DEF.COM";
static string adContainer = "DC=USA,DC=abc,DC=def,DC=com";
static string adADPath = "LDAP://USA.abc.def.com";
public static List<string> GetUserADGroups(string UserName)
{
List<string> lsGroups = new List<string>();
try
{
PrincipalContext pc = new PrincipalContext(ContextType.Domain, adDomain, adContainer);
UserPrincipal up = UserPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, UserName);
PrincipalSearchResult<Principal> psr = up.GetGroups(pc);
foreach (Principal p in psr)
{
lsGroups.Add(p.Name);
}
}
catch (Exception)
{
}
return lsGroups;
}
public static StringCollection GetGroupMembers(DomainType eDomainType, string strGroup)
{
DirectoryEntry de = new DirectoryEntry(adDSADPath);
System.Collections.Specialized.StringCollection GroupMembers = new System.Collections.Specialized.StringCollection();
try
{
//DirectoryEntry DirectoryRoot = new DirectoryEntry(sADPath);
DirectorySearcher DirectorySearch = new DirectorySearcher(de, ("(CN=" + (strGroup + ")")));
SearchResultCollection DirectorySearchCollection = DirectorySearch.FindAll();
foreach (SearchResult DirectorySearchResult in DirectorySearchCollection)
{
ResultPropertyCollection ResultPropertyCollection = DirectorySearchResult.Properties;
foreach (string GroupMemberDN in ResultPropertyCollection["member"])
{
DirectoryEntry DirectoryMember = new DirectoryEntry(("LDAP://" + GroupMemberDN));
System.DirectoryServices.PropertyCollection DirectoryMemberProperties = DirectoryMember.Properties;
object DirectoryItem = DirectoryMemberProperties["sAMAccountName"].Value;
if (null != DirectoryItem)
{
GroupMembers.Add(DirectoryItem.ToString());
}
}
}
}
catch (Exception ex)
{
}
return GroupMembers;
}
I also tried to use this to see if user is a member of the group but it throws error if I access the site from PC in domain B:
public static bool IsMember(string UserName, string GroupName)
{
try
{
PrincipalContext pc = new PrincipalContext(ContextType.Domain, adDomain, adContainer);
UserPrincipal up = UserPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, UserName);
PrincipalSearchResult<Principal> psr = up.GetGroups(pc);
foreach (Principal result in psr)
{
if (string.Compare(result.Name, GroupName, true) == 0)
return true;
}
return false;
}
catch (Exception e)
{
throw e;
}
}
What ended up solving my problem was wrapping the functionality in the methods in:
using (System.Web.Hosting.HostingEnvironment.Impersonate())
{
}
For example, I change the method in my original post from:
public static StringCollection GetGroupMembers(DomainType eDomainType, string strGroup)
{
DirectoryEntry de = new DirectoryEntry(adDSADPath);
System.Collections.Specialized.StringCollection GroupMembers = new System.Collections.Specialized.StringCollection();
...
}
to:
public static StringCollection GetGroupMembers(DomainType eDomainType, string strGroup)
{
using (System.Web.Hosting.HostingEnvironment.Impersonate())
{
DirectoryEntry de = new DirectoryEntry(adDSADPath);
System.Collections.Specialized.StringCollection GroupMembers = new System.Collections.Specialized.StringCollection();
...
}
}
Hope this saves someone else some grief as it caused me a lot!

Connect to active directory with PrincipalContext

Im writtem app that looks for a user in active directory based on there name
When I try to create new PrincipalContext with ContextType.Domain and the domain name as a string, I get the exception "The server could not be contacted."
So to get the the AD that I did this ...
Open System by clicking the Start button Picture of the Start button, clicking Control Panel, clicking System and Maintenance, and then clicking System.
Which I got from http://windows.microsoft.com/en-gb/windows-vista/find-the-domain-your-computer-is-on
Which gave me
Something.Something (not this, but two strings with a . between them)
So when I run the following code and enter Something.Something as the domain, I get the exception "The server could not be contacted." on the new PrincipalContext(ContextType.Domain, domain);
I have tried changing the case of the string and the but dont seem to have any luck.
So what should I be using for the domain?
public static void Main(string[] args)
{
try
{
Console.WriteLine("Enter Domain, then press enter.");
string domain = Console.ReadLine();
Console.WriteLine("Enter First Name, then press enter.");
string userName = Console.ReadLine();
//This is the line that always crashes throws error
var principalContext = new PrincipalContext(ContextType.Domain, domain);
var user = UserPrincipal.FindByIdentity(principalContext, userName);
if (user == null)
{
Console.WriteLine("User Not Found");
}
var groups = user.GetGroups(principalContext);
var result = new List<string>();
groups.ToList().ForEach(sr => result.Add(sr.SamAccountName));
foreach (var item in result)
{
Console.WriteLine(item);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Ashley is correct... assuming you are running the application on a machine that is joined to the domain.
using System;
using System.DirectoryServices.AccountManagement;
using System.Linq;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Enter First Name, then press enter.");
var userName = Console.ReadLine();
// Will search the domain the application is running on
var principalContext = new PrincipalContext(ContextType.Domain);
var user = UserPrincipal.FindByIdentity(principalContext, userName);
if (user == null)
{
Console.WriteLine("User Not Found");
}
else
{
// Gets a list of the user's groups
var groups = user.GetGroups().ToList();
// Loops the groups and prints the SamAccountName
groups.ForEach(g => Console.WriteLine(g.SamAccountName));
}
Console.ReadKey();
}
}
}
If you've got several seconds to spare waiting for your data form a large AD, then go ahead and use PrincipalContext but if you want your response in milliseconds, use DirectoryEntry, DirectorySearcher and .PropertiesToLoad.
Here's an example for getting the groups for a user:
https://stackoverflow.com/a/65986796/5248400

Get Active Directory user information

Is there a way to retrieve the users active directory information like ad groups, SID and so on?
Currently I could find only examples with active directory azure.
At the moment I catch user information the following way:
IReadOnlyList<User> users = await User.FindAllAsync(UserType.LocalUser);
foreach (User user in users)
{
string displayName = (string)await user.GetPropertyAsync(KnownUserProperties.DisplayName);
}
Also the Win10 SDK doesn't provide any examples regarding active directory.
[Edit] There is a WinRT Api request on https://wpdev.uservoice.com/forums/110705-universal-windows-platform/suggestions/12556779-get-active-directory-user-information
[Edit] Regarding Issue on corefx https://github.com/dotnet/corefx/issues/7325
From command line you can use whoami /groups.
From C# you can check membership in a group using the SID using something like this:
using System;
using System.Security.Principal;
namespace ADMembership
{
class Program
{
static void Main()
{
WindowsPrincipal myPrincipal = new
WindowsPrincipal(WindowsIdentity.GetCurrent());
String groupName = "A Group";
SecurityIdentifier sid = new SecurityIdentifier(
"S-1-1-22-999999999-999999999-999999999-4444");
if (myPrincipal.IsInRole(groupName))
{
Console.WriteLine("{0} is in {1}.",
myPrincipal.Identity.Name.ToString(), groupName);
}
else
{
Console.WriteLine("{0} is not in {1}.",
myPrincipal.Identity.Name.ToString(), groupName);
}
if (myPrincipal.IsInRole(sid))
{
Console.WriteLine("{0} has SID: {1}.",
myPrincipal.Identity.Name.ToString(), sid);
}
else
{
Console.WriteLine("{0} does not have SID {1}.",
myPrincipal.Identity.Name.ToString(), sid);
}
}
}
}

How to check in C# if user account is active

How can I check from C# if a local user account (namely the local Administrator account) is active?
What I actually want is a C# replacement for the "Account Active" = "Yes" (or "No") output from the "net user Administrator" command.
I'm afraid this question looks like a duplicate to this one, but I don't know what to pass in for the parameter for the root DirectoryEntry object. Tried different things like "ldap://" + Environment.MachineName, "ldap://127.0.0.1", "WinNT://" + Environment.MachineName, but none of them worked. I get an exception thrown by the searcher.FindAll() call in all three cases.
class Program
{
static void Main(string[] args)
{
// Create the context for the principal object.
PrincipalContext ctx = new PrincipalContext(ContextType.Machine);
UserPrincipal u = UserPrincipal.FindByIdentity(ctx, IdentityType.SamAccountName, "Administrator");
Console.WriteLine(String.Format("Administrator is enable: {0}", u.Enabled));
}
}
You can query WMI's Win32_UserAccount
This is boilerplate what MS's wmi code creator spits out as a reference;
using System;
using System.Management;
using System.Windows.Forms;
namespace WMISample
{
public class MyWMIQuery
{
public static void Main()
{
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT Disabled FROM Win32_UserAccount WHERE name = 'alexk'");
foreach (ManagementObject queryObj in searcher.Get())
{
Console.WriteLine("-----------------------------------");
Console.WriteLine("Win32_UserAccount instance");
Console.WriteLine("-----------------------------------");
Console.WriteLine("Disabled: {0}", queryObj["Disabled"]);
Console.ReadKey();
}
}
catch (ManagementException e)
{
MessageBox.Show("An error occurred while querying for WMI data: " + e.Message);
}
}
}
}
(I'd link the tool but as usual the msdn links are dead)
Try this.
var server = "YOURMACHINENAME";
var username = "Guest";
var de = new DirectoryEntry {Path = "WinNT://" + server + ",computer"};
var result = de.Children
.Cast<DirectoryEntry>()
.First<DirectoryEntry>(d => d.SchemaClassName == "User" && d.Properties["Name"].Value.ToString() == username);
var flags = (int)result.Properties["UserFlags"].Value;
var disabled = (flags & 2) == 2;
This isn't quite the same but they use DirectoryEntry directoryEntry = new DirectoryEntry(string.Format("WinNT://{0}/{1}", computerName, username)); Would that help?
Considering it's a local user, you need to call the win32 api funcion NetGetUserInfo to get what you need.
The example in pinvoke.net is almost what you need, however you need to change the level parameter to 2 to get the neccesary info

Categories