var client = ActiveDirectoryClientFactory.GetActiveDirectoryClient(_connector);
client.Context.MergeOption = MergeOption.AppendOnly;
_withOwnedDevices = await client.Users.Expand(d => d.OwnedDevices).ExecuteAsync();
_withMemberOf = await client.Users.Expand(d => d.MemberOf).ExecuteAsync();
When running the above code I do get OwnedDevices but MemberOf comes back empty all the time.
var client = ActiveDirectoryClientFactory.GetActiveDirectoryClient(_connector);
_withOwners = await client.Devices.Expand(d => d.RegisteredOwners).ExecuteAsync();
_withUsers = await client.Devices.Expand(d => d.RegisteredUsers).ExecuteAsync();
_withMemberOf = await client.Devices.Expand(d => d.MemberOf).ExecuteAsync();
Similiarly running the above nets RegisteredOwners and RegisteredUsers but nothing on the MemberOf collection.
var client = ActiveDirectoryClientFactory.GetActiveDirectoryClient(_connector);
var matches = await client.Users.Where(u => u.Mail == email).Expand(u => u.MemberOf).ExecuteAsync();
This nets a list of matches, one, and the MemberOf collection has results. Querying the same Active Directory in all cases.
The problem seems to be related to the need for the special MergeOption. Without AppendOnly or NoTracking I get an exception when making the second of two calls with different Expand options. I do a similar thing with devices and do not have to set the MergeOption at all and the context merges the objects no problem so both expanded collections are shown on the entity. In the case of users if you try the same thing the ActiveDirectoryClient throws a "The context is already tracking a different entity with the same resource Uri." In testing with Devices that problem only creeps in when trying to do an expand on MemberOf, can do multiple calls to expand other collections but once I try with MemberOf you get the tracking exception.
I tested below code:
ActiveDirectoryClient client = AuthenticationHelper.GetActiveDirectoryClient();
client.Context.MergeOption = MergeOption.AppendOnly;
var _withMemberOf = await client.Users.Expand(d => d.MemberOf).ExecuteAsync();
Above code will return page Collection of the users. Max number of users in current page is 100.Each user has a MemberOf property which shows the groups and roles memberships of the user .Blow is a screenshot when debug in vs:
Please check whether the user you successfully get MemberOf collection via :
var client = ActiveDirectoryClientFactory.GetActiveDirectoryClient(_connector);
var matches = await client.Users.Where(u => u.Mail == email).Expand(u => u.MemberOf).ExecuteAsync();
is included in the CurrentPage collections , you could use below code to get the next page collection of users to check that :
client.Context.MergeOption = MergeOption.AppendOnly;
IPagedCollection<IUser> pagedCollection = await client.Users.Expand(d => d.MemberOf).ExecuteAsync();
if (pagedCollection != null)
{
do
{
//loop the users and get the memberof information
pagedCollection = await pagedCollection.GetNextPageAsync();
} while (pagedCollection != null);
}
Related
I just migrated my application from ASP.Net Core 2.2 to 3.1, and I have a bug I don't understand. It's a fairly straightforward MVC app with a mySQL DB, and I'm using EF Core - Identity to manage what they call "policy-based" authorization, aka 'claims-based' auth.
The main Class/Model is a Family Profile. A given User can have 2 levels of claims to different Families - they can be either a Parent or a Caregiver. The User dashboard View shows a few lists: A list of children in any Parent-Family, a list of Caregivers authorized for Parent-Families, and a list of Caregiver-Families.
This is the Controller code that sends all that info to the view. The problem is that the foreach loop is generating an error in the browser:
"InvalidOperationException: Cannot set MySqlCommand.CommandText when there is an open DataReader for this command; it must be closed first."
It only happens when the User has a Caregiver relationship with a Family, since the conditional prevents you from entering the foreach, but part of my confusion is why this is an issue inside this loop when it seems like I am making very similar requests to the DB in other places.
I looked at this question and this one, and it seems like maybe the 2nd one is more relevant, so I tried adding the await piece and making the query method Async inside the foreach loop (it had been just 1 line in there), but it didn't fix the problem. Maybe I put it in the wrong place? The first question helped me understand what's going on under the hood, but I don't know where in my code I would actually implement any of it.
Thanks in advance for any help!
public async Task<ActionResult> Index()
{
var userId = this.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var currentUser = await _userManager.FindByIdAsync(userId);
var userFamilies = (_db.Families.Where(entry => (entry.ParentId == currentUser.Id))).ToList();
if (_db.Caregivers.Select(entry => (entry.CaregiverId == currentUser.Id)) != null)
{
var userCareFamilies = new List<Family>();
var careFamilies = _db.CaregiverFamilies
.Where(c => c.CaregiverId == currentUser.Id);
if (careFamilies.Count() != 0)
{
foreach (CaregiverFamily cf in careFamilies)
{
var thisFamily = await _db.Families.FirstOrDefaultAsync(f => f.FamilyId == cf.FamilyId);
userCareFamilies.Add(thisFamily);
}
}
ViewBag.CareFamilies = userCareFamilies;
}
The problem is with this line:
var careFamilies = _db.CaregiverFamilies
.Where(c => c.CaregiverId == currentUser.Id);
Because you are not materializing the query (via ToList etc), it keeps the connection open while you're iterating through the results.
Try using
var careFamilies = _db.CaregiverFamilies
.Where(c => c.CaregiverId == currentUser.Id).ToList();
On a side note, you're mixing Async (FirstOrDefaultAsync) and Sync (ToList) methods. I'd suggest sticking to one or the other.
I have read the relevant Stack Overflow questions and tried out the following code:
WindowsIdentity identity = WindowsIdentity.GetCurrent();
if (null != identity)
{
WindowsPrincipal principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
return false;
It does not return true even though I have manually confirmed that the current user is a member of the local built-in Administrators group.
What am I missing ?
Thanks.
Just found other way to check if user is admin, not running application as admin:
private static bool IsAdmin()
{
WindowsIdentity identity = WindowsIdentity.GetCurrent();
if (identity != null)
{
WindowsPrincipal principal = new WindowsPrincipal(identity);
List<Claim> list = new List<Claim>(principal.UserClaims);
Claim c = list.Find(p => p.Value.Contains("S-1-5-32-544"));
if (c != null)
return true;
}
return false;
}
Credit to this answer, but code is corrected a bit.
The code you have above seemed to only work if running as an administrator, however you can query to see if the user belongs to the local administrators group (without running as an administrator) by doing something like the code below. Note, however, that the group name is hard-coded, so I guess you would have some localization work to do if you want to run it on operating systems of different languages.
using (var pc = new PrincipalContext(ContextType.Domain, Environment.UserDomainName))
{
using (var up = UserPrincipal.FindByIdentity(pc, WindowsIdentity.GetCurrent().Name))
{
return up.GetAuthorizationGroups().Any(group => group.Name == "Administrators");
}
}
Note that you can also get a list of ALL the groups the user is a member of by doing this inside the second using block:
var allGroups = up.GetAuthorizationGroups();
But this will be much slower depending on how many groups they're a member of. For example, I'm in 638 groups and it takes 15 seconds when I run it.
I have 2 domains, A and B. The Domain A has the group GroupA which contains users from Domain B.
My code:
using (var context = new PrincipalContext(ContextType.Domain, DomainName, User, Password))
{
using (var groupPrincipal = GroupPrincipal.FindByIdentity(context, IdentityType.SamAccountName,
groupName))
{
if (groupPrincipal == null) return null;
using (var principalSearchResult = groupPrincipal.GetMembers(true))
{
var changedUsersFromGroup =
principalSearchResult
.Where(member => member is UserPrincipal)
.Where(member => IsModifiedUser(member, usnChanged))
.Cast<UserPrincipal>()
.Select(adsUser => new AdsUser(adsUser)).Cast<IAdsUser>()
.ToArray();
return changedUsersFromGroup;
}
}
}
System.DirectoryServices.AccountManagement.PrincipalOperationException:
While trying to resolve a cross-store reference, the target principal
could not be found in the domain indicated by the principal's SID.
But if I add user from here
new PrincipalContext(ContextType.Domain, DomainName, User, Password)
to domain B, it works correctly.
How can I fix it?
At least in .NET 4.7, you can work around it by manually managing the enumerator. This has been tested and has been able to successfully get past the errors.
static System.Guid[] GetGroupMemberGuids(System.DirectoryServices.AccountManagement.GroupPrincipal group)
{
System.Collections.Generic.List<System.Guid> result = new List<Guid>();
if (group == null) return null;
System.DirectoryServices.AccountManagement.PrincipalCollection px = group.Members;
System.Collections.IEnumerator en = px.GetEnumerator();
bool hasMore = true;
int consecFaults = 0;
while (hasMore && consecFaults < 10)
{
System.DirectoryServices.AccountManagement.Principal csr = null;
try
{
hasMore = en.MoveNext();
if (!hasMore) break;
csr = (System.DirectoryServices.AccountManagement.Principal)en.Current;
consecFaults = 0;
}
catch (System.DirectoryServices.AccountManagement.PrincipalOperationException e)
{
Console.Error.WriteLine(" Unable to enumerate a member due to the following error: {0}", e.Message);
consecFaults++;
csr = null;
}
if (csr is System.DirectoryServices.AccountManagement.UserPrincipal)
result.Add(csr.Guid.Value);
}
if (consecFaults >= 10) throw new InvalidOperationException("Too many consecutive errors on retrieval.");
return result.ToArray();
}
Check if it behaves differently when not casting to UserPrincipal, e.g.
var changedUsersFromGroup = principalSearchResult.ToArray();
As per other threads it might be some issue there. Also as per MSDN, using GetMembers(true) returned principal collection that does not contain group objects, only leaf nodes are returned, and so maybe you don't need that casting at all. Next, is to check how many results such search would return. If your AD has many users/nested groups, it might be better to try not to use GetMembers(true) to ensure it works on small groups of users.
It seems you are defining a PrincipalContext of domain A (so you can get the group), but because the users inside the group are defined in domain B the context cannot access them (as it's a domain A context).
You might need to define a second 'PricipalContext` for domain B and run the query against it and filter the objects using maybe the list of SIDs of the users located in the domain A group (you'll need to get the list of SIDs without causing the underlying code to try and resolve them).
Hope it helps!
Unfortunately I currently cannot test it, but maybe you can try this
var contextB = new PrincipalContext(ContextType.Domain, DomainName_B, User_B, Password_B)
[..]
var changedUsersFromGroup =
principalSearchResult
.Where(member => member is UserPrincipal)
.Where(member => IsModifiedUser(member, usnChanged))
.Select(principal => Principal.FindByIdentity(contextB, principal.SamAccountName))
.Cast<UserPrincipal>()
.Select(adsUser => new AdsUser(adsUser)).Cast<IAdsUser>()
.ToArray();
This could work, but only, if all members are in Domain B of course. If they are mixed in different domains you may have to filter it before that and iterate through your domains.
Alternatively you could run your application with a domain account? Then don't pass user/pass and give the required access rights to this account to avoid the error.
Explanation: The domain context will switch for your retrieved principals (you can view this in debugging mode if you comment out the new AdsUser / IAdsUser Cast part). This is the one, that seems to cause the exception. Though exactly this is the part I cannot test, I think the creation of AdsUser creates a new ldap Bind to the target Domain. This fails with the "original" Credentials. Is AdsUser Part of ActiveDS or 3rd Party? I did not find any hint if passing credentials uses basic authentication, but I think it should. Using application credentials uses negotiate and "handling this over" to the new AdsUser(..) should fix the issue as well.
problem found and reported to MS as bug. currently impossible to do it with .net :( but it works via native c++ api via queries
I'm trying to create a new user and adding it to an already existing ParseRole, but i'm getting the ParseException: object not found for update after performing await parseRole.SaveAsync();.
I checked in the Parse website and both the Role and User are saved (the ids returned by c# code are correct), but the Role hasn't that user.
[TestMethod]
public async Task CanCreateCustomer()
{
var customer = ParseObject.Create<ApplicationUser>();
customer.FirstName = GetRandom.FirstName();
customer.LastName = GetRandom.LastName();
customer.Password = "123";
customer.Username = customer.GetUserName();
Assert.IsNull(customer.ObjectId);
await customer.SignUpAsync();
Assert.IsNotNull(customer.ObjectId);
var parseRole = await ParseRole.Query.Where(x => x.Name == Roles.CustomerRole).FirstAsync();
parseRole.Users.Add(customer);
await parseRole.SaveAsync();
}
The problem was I didn't set the ACL for my roles to allow write access to the logged user (or public, in case of my unit test)
I am trying to get a list of all users in our instance of Desire2Learn using a looping structure through the bookmarks however for some reason it continuously loops and doesn't return. When I debug it it is showing massive amounts of users (far more than we have in the system as shown by the User Management Tool. A portion of my code is here:
public async Task<List<UserData>> GetAllUsers(int pages = 0)
{
//List<UserData> users = new List<UserData>();
HashSet<UserData> users = new HashSet<UserData>();
int pageCount = 0;
bool getMorePages = true;
var response = await Get<PagedResultSet<UserData>>("/d2l/api/lp/1.4/users/");
var qParams = new Dictionary<string, string>();
do
{
qParams["bookmark"] = response.PagingInfo.Bookmark;
//users = users.Concat(response.Items).ToList<UserData>();
users.UnionWith(response.Items);
response = await Get<PagedResultSet<UserData>>("/d2l/api/lp/1.4/users/", qParams);
if (pages != 0)
{
pageCount++;
if (pageCount >= pages)
{
getMorePages = false;
}
}
}
while (response.PagingInfo.HasMoreItems && getMorePages);
return users.ToList();
}
I originally was using the List container that is commented out but just switched to the HashSet to see if I could notice if duplicates where being added.
It's fairly simple, but for whatever reason it's not working. The Get<PagedResultSet<UserData>>() method simply wraps the HTTP request logic. We set the bookmark each time and send it on.
The User Management Tool indicates there are 39,695 users in the system. After running for just a couple of minutes and breaking on the UnionWith in the loop I'm showing that my set has 211,800 users.
What am I missing?
It appears that you’ve encountered a defect in this API. The next course of action is for you to have your institution’s Approved Support Contact open an Incident through the Desire2Learn Helpdesk. Please make mention in the Incident report that Sarah-Beth Bianchi is aware of the issue, and I will work with our Support team to direct this issue appropriately.