I have two domains, in a trusted relationship, that I'm trying to manage from a C# web application. To do that, I have to impersonate two different technical users, but that works good, so I will not emphasize that part of the code.
To build proper and easy to manage ACLs for the file system, I must
Create a group in domainA (OK!)
Find a user in domainB (OK!)
Add the user to the group (FAILS when committing changes, error message: There is no such object on the server. (Exception from HRESULT: 0x80072030))
If I'm adding a user from the same domain, the code works perfectly, so I believe I'm only missing a small partial info here. I used this document as a reference and saw this question as well (and a few more citing this error message) but neither of them helped.
Code (try-catch block removed to make it simpler)
// de is a DirectoryEntry object of the AD group, received by the method as a parameter
// first impersonation to search in domainB
// works all right
if (impersonator.impersonateUser("techUser1", "domainB", "pass")) {
DirectoryEntry dom = new DirectoryEntry("LDAP://domainB.company.com/OU=MyOU,DC=domainB,DC=company,DC=com", "techUser1", "pass");
de.Invoke("Add", new object[] { "LDAP://domainB.company.com/CN=theUserIWantToAdd,OU=MyOU,DC=domainB,DC=company,DC=com" });
// de.Invoke("Add", new object[] { "LDAP://domainA.company.com/CN=anotherUserFromDomainA,OU=AnotherOU,DC=domainB,DC=company,DC=com" });
impersonator.undoImpersonation();
}
// second impersonation because the group (de) is in domainA
// and techUser2 has account operator privileges there
if (impersonator.impersonateUser("techUser2", "domainA", "pass"))
{
de.CommitChanges();
impersonator.undoImpersonation();
return true;
}
else
{
// second impersonation was unsuccessful, so return an empty object
return false;
}
Line 6 works, if I debug it or force the properties to be written to HttpResponse, it is clearly there. So the LDAP queries seem to be OK.
Also, if I comment out line 6 and uncomment 7, so basically I add a user from the same domain, the whole thing works miraculously. With domainB, I'm stuck. Any good piece of advice?
Following your code, I see that you're getting de as a parameter, which is in Domain A. Then you're creating DirectoryEntry object dom, which is getting impersonated, but never getting used. However, you're trying to add an object from Domain B to de directly using LDAP. This line:
de.Invoke("Add", new object[{"LDAP://domainB.company.com/CN=theUserIWantToAdd,OU=MyOU,DC=domainB,DC=company,DC=com" });
is not getting impersonated.
Assuming your impersonation works correctly, use dom object which is already impersonated with DirectorySearcher to find the user in Domain B and then add the user object from Domain B to de.
...
using (DirectoryEntry dom = new DirectoryEntry("LDAP://domainB.company.com/OU=MyOU,DC=domainB,DC=company,DC=com", "techUser1", "pass"))
{
using (DirectorySearcher searcher = new DirectorySearcher(dom))
{
searcher.Filter = "(&(objectClass=user)(CN=theUserIWantToAdd))";
SearchResult result = searcher.FindOne();
de.Invoke("Add", new object[] { result.Path });
}
}
...
UDPATE
This example will show you how to get user SID from one domain, search group from another domain and add user to group using SID.
//GET THE USER FROM DOMAIN B
using (UserPrincipal userPrincipal = UserPrincipal.FindByIdentity(domainContext, UPN))
{
if (userPrincipal != null)
{
//FIND THE GROUP IN DOMAIN A
using (GroupPrincipal groupPrincipal = GroupPrincipal.FindByIdentity(domainContext, groupName))
{
if (groupPrincipal != null)
{
//CHECK TO MAKE SURE USER IS NOT IN THAT GROUP
if (!userPrincipal.IsMemberOf(groupPrincipal))
{
string userSid = string.Format("<SID={0}>", userPrincipal.SID.ToString());
DirectoryEntry groupDirectoryEntry = (DirectoryEntry)groupPrincipal.GetUnderlyingObject();
groupDirectoryEntry.Properties["member"].Add(userSid);
groupDirectoryEntry.CommitChanges();
}
}
}
}
}
Please note that I skipped all the impersonation in the above code.
What finally worked was using principals as Burzum suggested. The original code samples you can see in the MSDN article linked in the question did not work here. So the Principal-based approach is a must nut not enough. You need one more line before committing changes of the new group:
group.Properties["groupType"].Value = (-2147483644);
The default was 0x8000000 and I had to change it to 0x80000004 to enable it to accept FSPs from another domain.
So now the group exists, it has members, it is added to the ACL of the folder.
Related
We have a C# application that is using the Microsoft Graph API to display the contents of SharePoint folder to users in a Syncfusion FileManager control. We need to grant permissions to those folders for certain users in order that they can collaborate on files.
We can add specific users using the sharing invitation to add a permission (see https://learn.microsoft.com/en-us/graph/api/driveitem-invite?view=graph-rest-1.0&tabs=http). We also need to be able to remove a user from this permission without deleting the whole link (and therefore any other users using it). I cannot see a way of doing this!
I have also tried using CreateLink (see https://learn.microsoft.com/en-us/graph/api/driveitem-createlink?view=graph-rest-1.0&tabs=http) but get an ‘Invalid Request’ error when trying to ‘Grant’ permission to a user and therefore never get as far as trying to remove an individual user!. The code I am using to try and 'Grant' permission is as follows (the last line produces the error):
public object CreateSharingLink(string itemId, List<string> recipientList, List<string> roles)
{
if (itemId == null) return null;
var type = "edit";
var scope = "organization";
Permission p = GraphClient.Drives[documentLibrary.Id].Items[itemId].CreateLink(type, scope).Request().PostAsync().Result;
return GrantAccessToSharingLink(p.Link.WebUrl, recipientList, roles);
}
public object GrantAccessToSharingLink(string sharingUrl, List<string> recipientList, List<string> roles)
{
List<DriveRecipient> recipients = (recipientList.Select(r => new DriveRecipient { Email = r })).ToList();
string base64Value = System.Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sharingUrl));
string encodedUrl = "u!" + base64Value.TrimEnd('=').Replace('/', '_').Replace('+', '-');
return GraphClient.Shares[encodedUrl].Permission.Grant(roles, recipients).Request().PostAsync().Result;
}
Any assistance would be much appreciated.
A similar question was asked a while ago but without an answer. Remove GrantedTo user from Permission using Graph API
You can delete only not inherited permissions. Only sharing permissions that are not inherited can be deleted. Here's the related doc.
I'm trying to figure out how to manage a local group with C#.
What happens now, is that I have a local group called "TestGroup". Within this local group, I have 3 local user and 1 domain members as follows:
GV\member1
member2
member3
member4
Where GV\member is domain user and rest is local user.
I have the following basic code:
Code:
using (PrincipalContext lpc = new PrincipalContext(ContextType.Machine))
{
GroupPrincipal mainGroup = GroupPrincipal.FindByIdentity(lpc, "TestGroup");
PrincipalSearchResult<Principal> members = mainGroup.GetMembers();
if (members != null)
{
foreach (Principal member in members)
{
// Display name
}
}
}
While executing this code, I encounter an error message saying "network path was not found".
The funny part is, if my group only contains local users, it works perfectly.
Is there anything that I did wrong? I apologize if this question is silly in any way. It's probably something basic I guess.
We have an AD with users in "mydomain.com" and users in "child.mydomain.com". When We try to list them, we can only find the "mydomain.com"'s users and groups, but we also need those from the child domain. How can I achieve this using C# ? Please take a look to my sample code :
context = new PrincipalContext(ContextType.Domain);
//...
var filter = new GroupPrincipal(context);
filter.IsSecurityGroup = true;
using(var searcher = new PrincipalSearcher(filter)
using(var results = searcher.FindAll())
{
foreach(GroupPrincipal group in results)
{
string path = "LDAP://rootDSE";
DirectoryEntry searchRoot = new DirectoryEntry(path);
string configNC = searchRoot.Properties["configurationNamingContext"].Value.ToString();
DirectoryEntry configSearchRoot = new DirectoryEntry("LDAP://" + configNC);
DirectorySearcher configSearch = new DirectorySearcher(configSearchRoot);
configSearch.Filter("(NETBIOSName=*)");
configSearch.PropertiesToLoad.Add("dnsroot");
configSearch.PropertiesToLoad.Add("ncname");
configSearch.PropertiesToLoad.Add("NETBIOSName");
SearchResultCollection forestPartitionList = configSearch.FindAll();
List<Tuple<string,string>> netbiosNameList = new List<Tuple<string,string>>(forestPartitionList.Count);
foreach(SearchResult domainPartition in forestPartitionList)
{
string ncname = domainPartition.Properties["ncname"][0].ToString();
string netBIOSName = domainPartition.Properties["NETBIOSName"][0].ToString();
netbiosNameList.Add(Tuple.Create(ncname, netBIOSName));
}
//...
//Find group members
using (var principal = GroupPrincipal.FindByIdentity(context, IdentityType.DistinguishedName, group.DistinguishedName))
using (var members = principal.GetMembers(true))
using (var enumerator = members.GetEnumerator())
{
//...
}
}
}
The code is not exactly written this way, I just want to show you the main calls that are made to query the AD. We can list the parent domain groups and users but not the child domain ones. If I change the initialization of my "context" variable passing the child domain IP and user/password, I can list the groups and users in it. But we want to be able to do so while being in the parent domain.
I hope you can help me. Thanks a lot!
You can query the global catalog.
It contains a read-only, searchable, partial representation of every object in every domain in a multidomain Active Directory forest.
The GC operates on port 3268 ( standard ldap ) and 3269 ( SSL ldap ). Simply connect to any of your domain controllers on one of the above two ports and your search will be automatically directed to the GC server.
To perform any modifications, though, you will have to send such request to a domain controller for that particular domain the object belongs to.
I am using System.DirectoryServices.AccountManagement.dll to deal with Active Directory
to get all the users in the "Domain Users" group.
This is returning all the users in the domain but I need to get just the enabled ones.
Here is some sample code:
List<string> users = new List<string>();
PrincipalContext pcContext = GetPrincipalContext();
GroupPrincipal grp = GroupPrincipal.FindByIdentity(pcContext,
IdentityType.Name,
"Domain Users");
foreach (Principal user in grp.GetMembers(true).OfType<UserPrincipal>())
{
if (user.Enabled != false)
{
users.Add(user.Name);
}
}
Other groups work fine, but when the group is "Domain Users", the value of the Enabled property is false for all users. This makes it impossible to distinguish between enabled and disabled users without doing a further query for each user.
UserPrinciple objects have a bool Enabled property for this.
http://msdn.microsoft.com/en-us/library/system.directoryservices.accountmanagement.userprincipal_properties.aspx
// Add this to GetUserDetails
objUserDetails.EmployeeId = UserPrinical.EmployeeId;
// Then condition the add to only add enabled
if (objUserDetails.Enabled) {
objUserDetails.Add(GetUserDetails(p.Name));
}
A method around this problem could be to first search for Enabled Users using the PrincipalSearcher class and then use the Principal's method of IsMemberOf()
List<string> users = List<string>();
PrincipalContext pcContext = GetPrincipalContext();
GroupPrincipal grp = GroupPrincipal.FindByIdentity(pcContext, IdentityType.Name, "Domain Users");
UserPrincipal searchFilter = new UserPrincipal(pcContext){ Enabled = true }
PrincipalSearcher searcher = new PrincipalSearcher(searchFilter);
PrincipalSearchResult<Principal> results = searcher.FindAll();
foreach (Principal user in results)
if (user.IsMemberOf(grp))
users.Add(user.SamAccountName);
There's a remark on the MSDN page of the Enabled property saying :
If the principal has not been persisted in the store, this property returns null. After the principal is persisted, the default enabled setting depends on the store. The AD DS and AD LDS stores disable new principals when they are persisted, whereas SAM enables new principals when they are persisted. The application can only set this property to a value after it has been persisted in the store.
Perhaps it's related if the default is false ?
Also, there's a post on the MSDN forum about UserPrincipal.Enabled returns False for accounts that are in fact enabled? and that really sound similar to your issue. According to the post there's perhaps a solution here :
I think I misunderstood. Disregard what I posted before. I think I
know what's happening. The GetMembers method apparently isn't loading
the UserPrincipal data. I don't know if there is a better solution,
but the following works (at least on my AD):
foreach (UserPrincipal user in group.GetMembers(false))
{
UserPrincipal tempUser = UserPrincipal.FindByIdentity(context, user.SamAccountName);
// use tempUser.Enabled
// other code here
}
This is not so much a question as information for anyone experiencing the same problem.
The following error occurs:
System.DirectoryServices.AccountManagement.PrincipalOperationException: An error (87) occurred while enumerating the groups. The group's SID could not be resolved.
at System.DirectoryServices.AccountManagement.SidList.TranslateSids(String target, IntPtr[] pSids)
at System.DirectoryServices.AccountManagement.SidList.ctor(List`1 sidListByteFormat, String target, NetCred credentials)
at System.DirectoryServices.AccountManagement.ADDNLinkedAttrSet.TranslateForeignMembers()
When the following code is run and a group or child group contains a ForeignSecurityPrincipal:
private static void GetUsersFromGroup()
{
var groupDistinguishedName = "CN=IIS_IUSRS,CN=Builtin,DC=Domain,DC=com";
//NB: Exception thrown during iteration of members rather than call to GetMembers.
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "Domain", "Username", "Password"))
{
using (GroupPrincipal groupPrincipal = GroupPrincipal.FindByIdentity(ctx, IdentityType.DistinguishedName, groupDistinguishedName))
{
using (var searchResults = groupPrincipal.GetMembers(true))//Occurs when false also.
{
foreach (UserPrincipal item in searchResults.OfType())
{
Console.WriteLine("Found user: {0}", item.SamAccountName)
}
}
}
}
}
I raised a support call with Microsoft and they have confirmed it as an issue. A bug has been raised internally but it has not been confirmed whether this will be fixed.
Microsoft suggested the following workaround code but it performs poorly on groups with a large number of users because of the repeated calls to UserPrincipal.FindByIdentity.
class Program
{
//"CN=IIS_IUSRS,CN=Builtin,DC=dev-sp-sandbox,DC=local"; //TODO MODIFY THIS LINE ACCORDING TO YOUR DC CONFIGURATION
static void Main(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine("Usage: ListGroupMembers \"group's DistinguishedName\"");
Console.WriteLine("Example: ListGroupMembers \"CN=IIS_IUSRS,CN=Builtin,DC=MyDomain,DC=local\"");
return;
}
string groupDistinguishedName = args[0];
PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "dev-sp-dc", "Administrator", "Corp123!");
List<UserPrincipal> users = new List<UserPrincipal>();
listGroupMembers(groupDistinguishedName, ctx, users);
foreach (UserPrincipal u in users)
{
Console.WriteLine(u.DistinguishedName);
}
}
//Recursively list the group's members which are not Foreign Security Principals
private static void listGroupMembers(string groupDistinguishedName, PrincipalContext ctx, List<UserPrincipal> users)
{
DirectoryEntry group = new DirectoryEntry("LDAP://" + groupDistinguishedName);
foreach (string dn in group.Properties["member"])
{
DirectoryEntry gpMemberEntry = new DirectoryEntry("LDAP://" + dn);
System.DirectoryServices.PropertyCollection userProps = gpMemberEntry.Properties;
object[] objCls = (userProps["objectClass"].Value) as object[];
if (objCls.Contains("group"))
listGroupMembers(userProps["distinguishedName"].Value as string, ctx, users);
if (!objCls.Contains("foreignSecurityPrincipal"))
{
UserPrincipal u = UserPrincipal.FindByIdentity(ctx, IdentityType.DistinguishedName, dn);
if(u!=null) // u==null for any other types except users
users.Add(u);
}
}
}
}
The above code could be modified to find foreign security principals causing problems in groups.
Microsoft provided the following information about the foreign security principals:
This is a class of objects in AD which represents a security principal from an external source (so another forest/domain or one of the “special” accounts below).
The class is documented here: http://msdn.microsoft.com/en-us/library/cc221858(v=PROT.10).aspx
And the container is documented here : http://msdn.microsoft.com/en-us/library/cc200915(v=PROT.10).aspx
A FSP is not a real object in AD, but rather a placeholder (pointer) to an object which lives in a different, trusted domain/forest. It can also be one of the “special identities” which are a bunch of well-known accounts who are also classed as FSP’s because their SID’s are different to the domain SID.
For example the anonymous, Authenticated User, batch and several other accounts as documented here:
http://technet.microsoft.com/en-us/library/cc779144(v=WS.10).aspx
Sure this is an old thread, but might help someone. I used the below code block the solve the problem. the Principal class exposes a property called StructuralObjectClass which tells you what is the AD Class of that principal. I used this to decide whether the object is a user. The GetMembers(true) recursively searches all nested-members in the groupPrincipal in question.
Hope this helps someone.
List<UserPrincipal> members = new List<UserPrincipal>();
foreach (var principal in groupPrincipal.GetMembers(true))
{
var type = principal.StructuralObjectClass;
if (type.Contains("user"))
members.Add((UserPrincipal)principal);
}
Thanks,
R
The accountmanagement library has many saddening defects, this is just another of the many...
One thing you can do to make things slightly faster would be to adjust your LDAP query so that it checks both group membership and object type at the same time as part of the query instead of in the loop. Honestly I doubt it will make much difference though.
Most of the inspiration for the query came from How to write LDAP query to test if user is member of a group?.
Query: (&(!objectClass=foreignSecurityPrincipal)(memberof=CN=YourGroup,OU=Users,DC=YourDomain,DC=com))
Note: This is an untested query...
IF there was a way to run an LDAP query in AccountManagement (another gripe of mine) then this would be the end of your troubles as you could run the query and let AccountManagement take it from there, but this option does not exist...
Based on personal experience I don't see any other options if you stick with AccountManagement. What you could do is dump AccountManagement and use just DirectoryServices. Under the hood all AccountManagement does is wrap DirectoryEntry objects anyways, you could write a few helper classes to do similar things.
As an alternative, you can use this code to get the members:
var pth = "LDAP://ex.invalid/CN=grpName,OU=Groups,OU=whatever,DC=ex,DC=invalid";
var dirEntry = new DirectoryEntry(pth);
var members = dirEntry.Invoke("Members"); //COM object
foreach (var member in (IEnumerable)members) {
var userEntry = new DirectoryEntry(member); //member is COM object
var sid = new SecurityIdentifier((byte[]) userEntry.InvokeGet("objectSid"), 0);
var typ = typeof(System.Security.Principal.NTAccount);
var account = (NTAccount)sid.Translate(typ);
Console.WriteLine(account.Value);
}