1

I found this lovely code in this question that helps me get the list of users.

#import <OpenDirectory/OpenDirectory.h>
ODSession *s = [ODSession defaultSession];
ODNode *root = [ODNode nodeWithSession:s name:@"/Local/Default" error:nil];
ODQuery *q = [ODQuery queryWithNode:root forRecordTypes:kODRecordTypeUsers attribute:nil matchType:0 queryValues:nil returnAttributes:nil maximumResults:0 error:nil];

NSArray *results = [q resultsAllowingPartial:NO error:nil];
for (ODRecord *r in results) {
    NSLog(@"%@", [r recordName]);
}

Now in Terminal/Shell, I could simply:

id -G <username or userID>

to immediately get the list of groups that user is member of, e.g.

>id -G 501 
20 12 61 79 80 81 98 702 701 33 100 204 250 395 398 399 400

However, is looks silly to go on and spawn an NSTask running "id", then collect the output and parse, just because I don't know how to use the OpenDirectory APIs...

So I dug a little deeper, and saw I could specify one or more "attributes" in the returnAttributes: parameter of the ODQuery... and then I went through the long list of attributes and found only one that made sense in CFOpenDirectoryConstants.h:

/*!
    @const      kODAttributeTypeGroup
    @abstract   List of groups.
    @discussion List of groups.
*/
CF_EXPORT
const ODAttributeType kODAttributeTypeGroup;

However, when I add it to the "standard" or "nativeOnly" or "All" sets of returnedAttributes, or even specify it alone - the result ODRecords I receive do NOT contain that attribute.

I try to obtain it using [r valueForAttribute: kODAttributeTypeGroup]; and also when I print [r description] where I can see all the attributes returned - alas it's not there.

I tried other attributes too, which were "close to the subject" like

kODAttributeTypeGroupMembers, kODAttributeTypeGroupMembers, kODAttributeTypeNetGroups, kODAttributeTypeGroupMembership and others - but both they are more expected on "Group" records of OpenDirectory, and anyway they're not returned.

So my question: how do I persuade OD to retrieve the groups for each of the users? Any hint (or reference to helpful documentation) will be greatly appreciated. Thanks!

Motti Shneor
  • 2,095
  • 1
  • 18
  • 24

2 Answers2

1

If you change your loop as follows

        for (ODRecord *r in results) {
            NSLog(@"%@", [r recordName]);
            
            // Query groups
            ODQuery * gq = [ODQuery queryWithNode:root
                           forRecordTypes:kODRecordTypeGroups
                            attribute:kODAttributeTypeGroupMembership
                            matchType:kODMatchContains
                          queryValues:r.recordName
                         returnAttributes:nil
                           maximumResults:0
                            error:NULL];
            NSArray * gr = [gq resultsAllowingPartial:NO
                                error:NULL];
            
            for ( ODRecord * g in gr ) {
                
                NSLog(@"\t%@",g.recordName);
                
            }
        }

Update

If you want something similar to id you need to query each group individually. Here is an example, just forced into your structure.

        for (ODRecord *r in results) {
            NSLog(@"%@", [r recordName]);
            
            // Query groups
            ODQuery * gq = [ODQuery queryWithNode:root
                           forRecordTypes:kODRecordTypeGroups
                            attribute:nil
                            matchType:0
                          queryValues:nil
                         returnAttributes:nil
                           maximumResults:0
                            error:NULL];
            NSArray * gr = [gq resultsAllowingPartial:NO
                                error:NULL];
            
            for ( ODRecord * g in gr ) {
                
                if ( [g isMemberRecord:r
                         error:NULL] ) {

                    NSLog(@"\t%@",g.recordName);

                }
            }
        }

This illustrates how to do it. What is wrong with it is that the groups are retrieved every time. You could e.g. easily cache that.

It is important to take note of both queries. In the first I query for group membership and in the second I query for groups and use each group to test for membership. It seems the first is more direct and the second stronger in the sense that it is more complete / accurate. I think this is more accurate because of nesting in groups.

skaak
  • 2,988
  • 1
  • 8
  • 16
  • I already figured out how to retrieve groups, and their "membership" attribute (which contains UUID's of the user ODRecords), and I can do it upside down like you suggest - which is a little slower and uglier - but possible. However, Groups can contain not just users, but other groups, too - and so this code will need to become even nastier and slower by becoming recursive. Are ou sure there's no straight way to retrieve a User with their groups? where I that kODAttributeTypeGroup, and why can't I get it? – Motti Shneor Dec 31 '20 at 06:36
  • Like you I was hoping for a clean solution but based on your more extensive research and a few of my own tests and a bit of googling it seems this is not as easy as one would assume. I tried a bit using GUIDs and different queries but that also gets dirty quickly. I realise this is not ideal, but at least it does what it says on the box. Although the first alternative here seems cleaner, I'd go with the second and let ```-isMemberRecord:``` deal with the inner workings of calculating group membership. I wish it was cleaner and would be interested in such a solution, but this works at least. – skaak Dec 31 '20 at 06:46
  • I also think as mentioned that the second approach takes care of the nested groups so you need not worry too much about recursion. I am as perplexed as you as to why a user record does not contain group info, but my guess is that it is not as straightforward as we assume because of this nesting and either we do it this way or it will be done this way in some other boxed solution. – skaak Dec 31 '20 at 06:49
  • Thanks a lot for digging into this. I'm now trying a different approach in my code, and again I stump on some strange behavior of these queries... I don't know if I should update my question (for a little different task) or create a new one... – Motti Shneor Jan 04 '21 at 05:19
  • Actually, my issue relates to your answers. the "membership" attribute of groups seems to also contain users that are not "prime" members of the group (e.g. user 0 - 'root' appears as a member of group - 'staff') - unlike what appears in 'id -Gn' any idea why? – Motti Shneor Jan 04 '21 at 05:48
  • I now tested with your code, and it, too, finds user 0 'root' a member of 'staff' which is not what I was expecting. Maybe I only need the "primary group ID" of a user. I wish I understood the meaning of those definitions... – Motti Shneor Jan 04 '21 at 06:58
  • Hi - this problem needs good docs to solve but, sans that, lots of testing. How about using the ```-isMemberRecord``` message? I hope that solves a lot of what you are trying to solve now. (PS ```sudo id -Gn``` also shows staff) – skaak Jan 04 '21 at 08:14
  • It seems really difficult to figure it all out with UID and GID and membership. It seems brittle and complex but it also seems as if ```isMemberRecord``` sort of clean it up without having to go into the detail youself. – skaak Jan 04 '21 at 08:15
  • Well... isMemberRecord: is too "thorough" - it goes recursivey, and also includes "non-primary" group memberships - which is good for SOME things, but not for others. The question I'm trying to answer now is: Given a set of group names (e.g. "staff", "admin") get the set of 'uid's who log in using these groups (yes, I know the definition is vague) with that specific example that user root is indeed part of 'staff' but it is NOT its primary group, and so I don't want it in my user-list. :( I'm now writing queries based on the "PrimaryGroupID" attribute, and hope for good. – Motti Shneor Jan 04 '21 at 08:48
  • I'm getting the impression the problem you try to solve is either not a good fit based on group membership or you will have to develop your own logic based on your specific requirements. Sounds like typical programming problem - lots of promise for easy solutions but the trouble lies in the detail and at some stage you need to develop some custom solution. Why not just use 'normal' users. The moment you request the set of uid's that log in it means `normal` users to me so I think that could be easier to do? – skaak Jan 04 '21 at 08:54
1

Although I fully accept skaak's answer, I thought I'd provide both a refined version of his code that's a little cleaner and avoids re-fetching all the groups for every user, AND I'll provide another version the answers a more specific variant of my question (the one I need for my implementation) in the hope this code snippet will help someone.

First, a working version of skaaks answer:

-(BOOL)groupsOfAllUsers {
    NSError *error = nil;
    ODSession *s = [ODSession defaultSession];
    ODNode *root = [ODNode nodeWithSession:s name:@"/Local/Default" error:nil];
    ODQuery *q = nil;

    // fetch all user records.
    NSArray *attributesToFetch = @[kODAttributeTypeUniqueID, kODAttributeTypeRecordName];
    q = [ODQuery queryWithNode:root forRecordTypes:kODRecordTypeUsers attribute:kODAttributeTypeRecordName matchType:0 queryValues:nil returnAttributes:attributesToFetch maximumResults:0 error:&error];   //kODAttributeTypeStandardOnly
    if (error!= nil)
        return NO;
    
    NSArray *userRecords = [q resultsAllowingPartial:NO error:&error];
    if (userRecords==nil || error!= nil)
        return NO;
    
    // fetch all groups
    attributesToFetch = @[kODAttributeTypeUniqueID, kODAttributeTypePrimaryGroupID];
    q = [ODQuery queryWithNode:root forRecordTypes:kODRecordTypeGroups
                     attribute:nil matchType:nil queryValues:nil
              returnAttributes:attributesToFetch maximumResults:0 error:&error];
    if (q == nil || error!= nil)
        return NO;

    NSArray *groupRecords = [q resultsAllowingPartial:NO error:&error];
    if (groupRecords == nil || error!= nil)
        return NO;
    
    NSMutableDictionary *userGroupIDs = [[NSMutableDictionary alloc] initWithCapacity:userRecords.count];
    NSMutableDictionary *userGroupNames = [[NSMutableDictionary alloc] initWithCapacity:userRecords.count];
    
    for (ODRecord *userRecord in userRecords) {
        NSMutableSet *gids = [[NSMutableSet alloc] initWithCapacity:groupRecords.count];
        NSMutableSet *groupNames = [[NSMutableSet alloc] initWithCapacity:groupRecords.count];
        for (ODRecord *groupRecord in groupRecords) {
            BOOL isMember = [groupRecord isMemberRecord:userRecord error:&error];
            if (error != nil)
                return NO;
            if (isMember) {
                [groupNames addObject:[groupRecord recordName]];
                id gid = [[groupRecord valuesForAttribute:kODAttributeTypePrimaryGroupID error:&error] firstObject];
                if (nil == gid || error!= nil)
                    return NO;
                [gids addObject:@([gid intValue])];
            }
        }
        id uid = [[userRecord valuesForAttribute:kODAttributeTypeUniqueID error:&error] firstObject];
        if (nil == uid || error!= nil)
            return NO;
        [userGroupIDs setObject:[gids copy] forKey:@([uid intValue])];
        [userGroupNames setObject:[groupNames copy] forKey:[userRecord recordName]];
    }
    return YES;
}

But, as pointed out in skaaks answer (and my many comments) the isMemberRecord: method takes into account recursive groups (groups containing groups) and also non-primary group memberships, and it didn't fit my needs. What I wanted is to find which users participate in a predefined set of groups (say {"staff","admin"} which, for example, excludes "root" user (uid 0) whose primary group is 'root', although it also is member of 'staff'. So - my other variation (receives a set of group names) looks like this, and runs much faster:

-(BOOL)usersInGroupNames:(NSArray *)inGroupNames { // e.g.  @[@"staff", @"admin"]
    
    // stuff needed for all our OD work...
    NSError *error = nil;
    ODSession *s = [ODSession defaultSession];
    ODNode *root = [ODNode nodeWithSession:s name:@"/Local/Default" error:nil];
    ODQuery *q = nil; NSArray *results = nil;
    
    // First fetch desired groups, their names and group-ids
    NSArray *attributesToFetch = @[kODAttributeTypePrimaryGroupID, kODAttributeTypeRecordName];
    q = [ODQuery queryWithNode:root forRecordTypes:kODRecordTypeGroups
                     attribute:kODAttributeTypeRecordName matchType:kODMatchEqualTo queryValues: inGroupNames
              returnAttributes:attributesToFetch maximumResults:0 error:&error];
    if (q == nil || error!= nil)
        return NO;
    
    if (nil == (results = [q resultsAllowingPartial:NO error:&error]) || error!= nil)
        return NO;

    NSArray *groupIDs = [results valueForKeyPath: [NSString stringWithFormat:@"@unionOfArrays.%@", kODAttributeTypePrimaryGroupID]];
    
    // now fetch users whose primary group IDs are in the list
    attributesToFetch = @[kODAttributeTypeUniqueID, kODAttributeTypeRecordName, kODAttributeTypePrimaryGroupID];
    q = [ODQuery queryWithNode:root forRecordTypes:kODRecordTypeUsers
                     attribute:kODAttributeTypePrimaryGroupID matchType:kODMatchEqualTo queryValues:groupIDs
              returnAttributes:attributesToFetch maximumResults:0 error:&error];
    if (q == nil || error!= nil)
        return NO;
    
    if (nil == (results = [q resultsAllowingPartial:NO error:&error]) || error!= nil)
        return NO;
    
    NSArray *userIDs = [results valueForKeyPath: [NSString stringWithFormat:@"@unionOfArrays.%@.intValue", kODAttributeTypeUniqueID]];
    NSLog (@"userIDs:%@", userIDs);
    return YES;
}

hope this helps anyone...

Motti Shneor
  • 2,095
  • 1
  • 18
  • 24
  • Nice - you are kind, accepting my answer while you craft (and publish) your own. I think it is best if you can do it - as you do - with queries but are also surprised you got it right given how much of a struggle that was. Thanks Motti - hope we meet again. – skaak Jan 04 '21 at 14:56
  • You can always accept my answers (If I ever get anything right ;) I just thought your's is more concise and less cluttered, but if someone hits this question, they can still copy-paste fuller and more functional code from my answer. Just giving back to the community what I just received - help. – Motti Shneor Jan 07 '21 at 08:16