A script to list service ACLs on Mac OS X 10.5

I personally don't think it's a good thing to blog in english when you're french, unless you are very fluent and your target audience reads english. Today, my audience is the worldwide crowd of Mac OS X Server sysadmin. So, while I'm not fluent, I'm going to write my first post in english.


There is something quite messy in the Service Access Control Lists (SACLs) on Mac OS X 10.5: you just can't display the full users & groups list of a SACL in command line.
Basically, you can do this:

$ dscl . -read /Groups/com.apple.access_ssh
AppleMetaNodeLocation: /Local/Default
GeneratedUID: A7E16606-3C52-42B9-852E-D197C7598EA8
NestedGroups: 955F946A-7C9D-4D3E-B286-E16003380282 ABCDEFAB-CDEF-ABCD-EFAB-CD...
PrimaryGroupID: 101
 Remote Login Group
RecordName: com.apple.access_ssh
RecordType: dsRecTypeStandard:Groups

As you can see, this SACL group com.apple.access_ssh has no direct members, only nested groups (NestedGroups key). So, in order to list users, you have to read the content of each nested group. But groups are only available by their name. So the first step is to find out group's names.
At this stage, you have no way to know if the target group is local or if it sits on a remote open directory server, so you must use the /Search path:

$ dscl /Search -search /Groups GeneratedUID 955F946A-7C9D-4D3E-B286-E16003380282
myadmins		GeneratedUID = (

The second step is to list users of the group:

$ dscl /Search -read /Groups/myadmins GroupMembership
GroupMembership: admin01 admin02 user01 user02 ldapuser01

But guess what: this group might have more than just users, may be its NestedGroups key is not empty! So at this point, you must also check the NestedGroups value, and recursively follow each group GUID, until you find only users.
Think "huge groups", think "handfulls of nested groups", and watch your fingers as you're going thru dscl torments. You've figured it out: Mac OS X lacks a good command line tool for following a SACL tree of users and groups.

Here come's getsacls.sh

I won't promise you a killer command line tool with foolproof error and recursion handling, but I still believe I've designed a usable piece of shell script. Even if it looks like it's the worst code I've ever wrote (wich is not true, I've made things way uglier).
The source code is too long and messy to be just copy-pasted here, just follow this link to download the getsacls.sh script.

How to get getsacls.sh:
Just download the latest version from here.

How to install getsacls.sh:
Simply copy to your Mac OS X 10.5 server (or managed client). Somewhere in your $PATH should be fine. Then chmod +x the script, so that it can be executed.

How to configure getsacls.sh:
Defaults values should be ok, but if you really want to change something, open the script in your favorite editor, and find the "FEW USER TUNABLE MISCS" section. Edit at your own risks.

How to use getsacls.sh:
It's simple, you just have to launch it. It will then proceed with the parsing of every SACL on your local system.
DO NOT use the sh command to launch this script. getsacls.sh uses special escape sequences and command options that sh will not recognize. Just run:

$ getsacls.sh

If you want to parse only some SACLs, you can provide each SACL name at the command line:

$ getsacls.sh com.apple.access_ssh com.apple.access_loginwindow

Still, you should only use SACL names that exist on your local system.

The default output is "fancy", it uses bold, indentation, and a beach-ball cursor. If you want the "no fancy" mode, you can either edit the corresponding "tunable misc variable" or define FANCY=NO at launch time:

$ FANCY=NO getsacls.sh com.apple.access_ssh

This "no fancy" mode allows for later parsing.

The script will not handle circular references. If your SACL uses nested groups in a circular way (group 1 -> group 2 -> group 1), the script will not stop.
When finding two or more similar users or groups (for example the local admin group and the open directory admin group), it will use only one of them, and that should be the local one.
The script uses SQLite3 as a backend, because bash is not good with arrays, and because I'm not good with PERL/Python/Ruby.

Sample "fancy" output:

   myadmins	/LDAPv3/	955F946A-7C9D-4D3E-B286-...
     admin01	/Local/Default	9A7917D1-D8E7-49D6-8211-...
     admin02	/Local/Default	40D516A2-4D02-4C92-9505-...
     ldapuser01	/LDAPv3/ldap.example.com	ldapuser01_OUT_OF_OD
     ldapuser02	/LDAPv3/ldap.example.com	ldapuser02_OUT_OF_OD
     ldapuser03	/LDAPv3/ldap.example.com	ldapuser03_OUT_OF_OD
     user01	/LDAPv3/	49EF9C64-D98B-11D8-BCFA-...
   admin	/Local/Default	ABCDEFAB-CDEF-ABCD-EFAB-...
     root	/Local/Default	FFFFEEEE-DDDD-CCCC-BBBB-...
     admin01	/Local/Default	9A7917D1-D8E7-49D6-8211-...
     admin02	/Local/Default	40D516A2-4D02-4C92-9505-...
     user01	/LDAPv3/	49EF9C64-D98B-11D8-BCFA-...

Sample "no fancy" output:

g 1 myadmins /LDAPv3/ 955F946A-7C9D-4D3E-B286-...
u 2 admin01 /Local/Default 9A7917D1-D8E7-49D6-8211-...
u 2 admin02 /Local/Default 40D516A2-4D02-4C92-9505-...
u 2 ldapuser01 /LDAPv3/ldap.example.com ldapuser01_OUT_OF_OD
u 2 ldapuser02 /LDAPv3/ldap.example.com ldapuser02_OUT_OF_OD
u 2 ldapuser03 /LDAPv3/ldap.example.com ldapuser03_OUT_OF_OD
u 2 user01 /LDAPv3/ 49EF9C64-D98B-11D8-BCFA-...
g 1 admin /Local/Default ABCDEFAB-CDEF-ABCD-EFAB-...
u 2 root /Local/Default FFFFEEEE-DDDD-CCCC-BBBB-...
u 2 admin01 /Local/Default 9A7917D1-D8E7-49D6-8211-...
u 2 admin02 /Local/Default 40D516A2-4D02-4C92-9505-...
u 2 user01 /LDAPv3/ 49EF9C64-D98B-11D8-BCFA-...

Current version:
As of now, current version of getsacls.sh is 407 ($Id: getsacls.sh 407 2009-07-09 09:36:26Z patpro $). Next revisions will be listed here.

Update: $Id: getsacls.sh 409 2009-07-09 14:30:01Z patpro $
I've added some error handling for a rare case: when a user account lives on a LDAP server distinct from the Open Directory server, the GroupMembership field is not updated on the OD if the user account is destroyed on the LDAP. So according to the GroupMembership the user is still here, but according to the LDAP the user is nowhere to be found.

Update: $Id: getsacls.sh 412 2009-07-23 20:24:54Z patpro $
I'm forcing LC_NUMERIC in the beachball function, so that sleep 0.05 runs as expected even for people not using the dot as a decimal separator. Some cleanup.

Update: $Id: getsacls.sh 414 2009-08-03 10:33:30Z patpro $
Some cleanup and english corrections. Added some delay to the beatchball rotation so it's more enjoyable.

Feel free to comment, and to correct my english ;)


  1. well, thanks for that.

    how difficult to work with the command line.

    As against the GUI:
    click on it and it will list it.

    Time to realise, command line IS A DINOSAUR!

  2. This is a great tool. Is it possible to do the opposite? List all users that have the sacl disabled.

  3. Well, listing users that have a sasl disabled is not easy to do. Basically you would need to list users who belong to a particular sasl (what my script does), and then you must remove this list from the list of all your users. If like me you manage a +40K users LDAP or OpenDirectory, you just end up with a useless list that takes a very long time to retrieve :)

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.