Extending Python (directory permissions w/GetNamedSecurityInfo)

Existing document

\win32\help\security_directories.html

Section author: John Nielsen <jn@who.net>

Python has a good framework in place to extend it’s capabilities with C or C++. Often this is done for reasons of performance or to give it capabilities that aren’t present in it’s standard libraries. For example, standard python does not provide a way to acquire file and directory permissions on windows. However, Microsoft’s GetSecurityInfo/GetNamedSecurityInfo does and is accessible via C++. We’ll look at how one can build a small module that uses GetNamedSecurityInfo to return to python permission information for a file or directory.

Introduction

Extending python, though not nearly as simple as pure python, is reasonably straightforward. The most convenient method of doing this is to separate the extension into it’s own module which would act like any other python module one uses. It is straightforward enough that one can limit the need to use C++ to narrow well-defined extensions and have python manage the rest. Extending python to use Microsoft’s Security API is an excellent candidate for this. Of the four security functions that allow one to deal with security descriptors, we’re going to look at GetNamedSecurityInfo. Specifically, python is going to be extended so it can get permissions from a filesystem.

Extending Python

There are several ways one can extend python. You can take a raw wrapping approach, SWIG http://www.swig.org/ or BPL (Boost Python Libraries) http://www.boost.org/libs/python/doc/index.html. This extension is simple enough that we’re going to wrap the C++ w/out the help of SWIG of BPL. This won’t be an extensive discussion about extending python, just enough to serve as a starting point to deciphering and making your own win32 extensions. The approach to wrap has a few standard todos. You need a to:

define whatever functions you want to expose manipulate C and python data types create a structure that has references to the functions initialize the module. Python has helper objects and functions to make this as painless as possible. You need to include Python.h to get access to these. To define the the function, you need to follow a specific format: static PyObject * module_function PyObject is the base type of Python Object. The name of the function needs to be the module followed by an ‘_’ followed by the function. To manipulate data types, there are numerous “Py” functions. The ones used in this extension are:

PyList_New(),PyList_Append – make and add to a python list PyDict_New(),PyDict_SetItem – make a dictionary and add to a python dictionary PyArg_ParseTuple – process arguments sent to module PyBytes_FromString/PyLong_FromUnsignedLong – convert C datatypes to Python PyArg_ParseTuple – accept data from python and convert to C

You also need to create a structure of all the functions that you want the Python interpreter to see. To do this you make an array of arrays of type static PyMethodDef. The array needs to end w/a NULL entry to mark it’s end.

Finally, you need to initialize the module (which is what happens when the module is imported) This uses another “Py” function called Py_InitModule.

These steps should become clearer once you see all the details of an actual extension.

GetNamedSecurityInfo

GetNamedSecurityInfo retrieves a security descriptor from a string that specifies the object. This works well for strings that contain filenames. A Security Descriptor contains the security information about an object. Of that information, we’re primarily concerned with the DACL (discretionary access control list), which contains a list of things that are allowed to interact with the object(in our case the file or directory). Specifically, we will process the ACEs in the DACL, each of which contains the removal/addition of permissions and what SIDs they are assigned to. To get and process the DACL, you need to follow these steps: GetNamedSecurityInfo – Get the DACL GetAclInformation – get the list of the ACL’s in the DACL to go through GetAce – get an ACE (which contains the access mask and the SID) from the list LookupAccountSid– gives you the domain and name for the SID The following code goes through those 4 steps in greater detail. Refer to Microsoft MSDN references http://msdn.microsoft.com/ for info about the various win32 calls made.

Extending Python for Directory Permissions

To setup Visual Studio correctly for building an extension, it is easiest to use a program called compile.py http://starship.python.net/crew/da/compile which builds the project for you. The Extension creates a dictionary of user or group plus the access mask. To Python it will look like: import fileperm all_perms=fileperm.get_perms(r’\Simonsharedba.txt’)

And if you print the perms you’ll get something like:

print all_perms {’\Everyone’: 2032127L, ‘Domain\fred’: 1179817L, ‘BUILTIN\Users’: 1179817L}

C code

  1#include
  2
  3//win32 security
  4#include
  5#include
  6#include
  7
  8
  9struct file_perms {
 10char user_domain[2050];
 11unsigned long user_mask;
 12};
 13
 14
 15//This function determines the username and domain
 16void lookup_sid ( ACCESS_ALLOWED_ACE* pACE, char user_domain[] ) {
 17   char username[1024]="";
 18   char domain[1024]="";
 19
 20   ULONG len_username = sizeof(username);
 21   ULONG len_domain = sizeof(domain);
 22   PSID pSID =(PSID)(&(pACE->SidStart));
 23   SID_NAME_USE sid_name_use;
 24
 25   if (!LookupAccountSid(NULL, pSID,
 26      username, &len_username, domain, &len_domain, &sid_name_use)){
 27      strcpy(user_domain, "unknown");
 28   } else {
 29      strcat(user_domain,domain);
 30      strcat(user_domain,"\\");
 31      strcat(user_domain,username);
 32   }
 33
 34
 35}
 36
 37//Store the mask and username in the file_perms structure.
 38//call lookup_sid to get the username
 39void acl_info( PACL pACL, ULONG AceCount, file_perms fp[]){
 40   for (ULONG acl_index = 0;acl_index < AceCount;acl_index++){
 41      ACCESS_ALLOWED_ACE* pACE;
 42
 43      if (GetAce(pACL, acl_index, (PVOID*)&pACE))
 44      {
 45         char user_domain[2050]="";
 46         lookup_sid(pACE,user_domain);
 47         strcpy(fp[acl_index].user_domain,user_domain);
 48         fp[acl_index].user_mask=(ULONG)pACE->Mask;
 49      }
 50   }
 51}
 52
 53static PyObject *get_perms(PyObject *self, PyObject *args)
 54{
 55
 56   PyObject *py_perms = PyDict_New();
 57   //get file or directory name
 58   char *file;
 59
 60   if (!PyArg_ParseTuple(args, "s", &file))
 61      return NULL;
 62
 63   //setup security code
 64   PSECURITY_DESCRIPTOR pSD;
 65   PACL pDACL;
 66   //GetNamedSecurityInfo() will give you the DACL when you ask for
 67   //DACL_SECURITY_INFORMATION. At this point, you have SIDs in the ACEs contained in the DACL.
 68   ULONG result = GetNamedSecurityInfo(file,SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL,
 69   &pDACL, NULL, &pSD);
 70
 71   if (result != ERROR_SUCCESS){ return NULL;}
 72   if (result == ERROR_SUCCESS){
 73      ACL_SIZE_INFORMATION aclSize = {0};
 74      if(pDACL != NULL){
 75         if(!GetAclInformation(pDACL, &aclSize, sizeof(aclSize),
 76            AclSizeInformation)){
 77            return NULL;
 78         }
 79      }
 80
 81      file_perms *fp = new file_perms[aclSize.AceCount];
 82      acl_info(pDACL, aclSize.AceCount, fp );
 83
 84      //Dict
 85      for (ULONG i=0;i
 86
 87
 88
 89
 90//Boilerplate functions
 91
 92//3 parts
 93//name of python function
 94//C++ function
 95//flags METH_VARARGS means function takes variable number of args
 96static PyMethodDef fileperm_methods[] = {
 97   { "get_perms", get_perms, METH_VARARGS },
 98   { NULL }
 99};
100
101
102
103void initfileperm()
104{
105
106Py_InitModule("fileperm",fileperm_methods);
107
108}

Python code

One thing the extension doesn’t do is process the access mask into human readable names. Python can easily do that as shown in the program below.

This program looks down a directory tree, takes the access mask and the login/group information, processes the access mask to produce human readable names and prints out the permission structure for the tree.

  1import os
  2import sys
  3import win32net
  4import string
  5import time
  6import copy
  7import getopt
  8
  9#the extension module
 10import fileperm
 11
 12All_perms={
 13   1:"ACCESS_READ",            #0x00000001
 14   2:"ACCESS_WRITE",           #0x00000002
 15   4:"ACCESS_CREATE",          #0x00000004
 16   8:"ACCESS_EXEC",            #0x00000008
 17   16:"ACCESS_DELETE",         #0x00000010
 18   32:"ACCESS_ATRIB [sic]",    #0x00000020
 19   64:"ACCESS_PERM",           #0x00000040
 20   32768:"ACCESS_GROUP",       #0x00008000
 21   65536:"DELETE",             #0x00010000
 22   131072:"READ_CONTROL",      #0x00020000
 23   262144:"WRITE_DAC",         #0x00040000
 24   524288:"WRITE_OWNER",       #0x00080000
 25   1048576:"SYNCHRONIZE",      #0x00100000
 26   16777216:"ACCESS_SYSTEM_SECURITY",#0x01000000
 27   33554432:"MAXIMUM_ALLOWED", #0x02000000
 28   268435456:"GENERIC_ALL",    #0x10000000
 29   536870912:"GENERIC_EXECUTE",#0x20000000
 30   1073741824:"GENERIC_WRITE", #0x40000000
 31   65535:"SPECIFIC_RIGHTS_ALL",#0x0000ffff
 32   983040:"STANDARD_RIGHTS_REQUIRED",#0x000f0000
 33   2031616:"STANDARD_RIGHTS_ALL",#0x001f0000
 34   }
 35
 36Typical_perms={
 37   2032127L:"Full Control(All)",
 38   1179817L:"Read(RX)",
 39   1180086L:"Add",
 40   1180095L:"Add&Read",
 41   1245631L:"Change"
 42}
 43
 44
 45def get_mask(mask):
 46   a=2147483648L
 47   if Typical_perms.has_key(mask):
 48      return Typical_perms[mask]
 49   else:
 50      result=''
 51      while a>>1:
 52            a=a>>1
 53            masked=mask&a
 54            if masked:
 55               if All_perms.has_key(masked):
 56                  result=All_perms[masked]+':'+result
 57   return result
 58
 59
 60def is_group(sys_id):
 61   #get the server for the domain -- it has to be a primary dc
 62   group=0
 63   resume=0
 64   sys_id=string.strip(sys_id)
 65   if D_group.has_key(sys_id):
 66      group=1
 67   elif D_except.has_key(sys_id):
 68      group=0
 69   else:
 70      try:
 71            #info returns a dictionary of information
 72            info = win32net.NetGroupGetInfo(Server, sys_id, 0)
 73            group=1
 74      except:
 75            try:
 76               win32net.NetLocalGroupGetMembers(Server, sys_id, 0,resume,4096)
 77               group=1
 78            except:
 79               pass
 80   return group
 81
 82
 83def get_perm_base(file):
 84   all_perms=fileperm.get_perms(file)
 85   for (domain_id,mask) in all_perms.items():
 86      (domain,sys_id)=string.split(domain_id,'\\',1)
 87      mask_name=get_mask(mask)
 88      Results.append(file+','+sys_id+','+mask_name)
 89
 90def get_perm(file):
 91   perm_list=[]
 92   perm_list.append(file)
 93   all_perms=fileperm.get_perms(file)
 94   for (domain_id,mask) in all_perms.items():
 95      (domain,sys_id)=string.split(domain_id,'\\',1)
 96      print domain,sys_id
 97      sys_id=str(sys_id)
 98      mask_name=get_mask(mask)
 99      if len(sys_id)<7:
100            perm_list.append(sys_id+'\t\t\t'+mask_name)
101      elif len(sys_id)>14:
102            perm_list.append(sys_id+'\t'+mask_name)
103      else:
104            perm_list.append(sys_id+'\t\t'+mask_name)
105   return perm_list
106def get_perms(arg, d, files):
107   a=2147483648L #1L<<31L
108   print 'Now at ',d
109   for i in files:
110      file=d+'\\'+i
111      if opts['-d']:
112            if not os.path.isdir(file): # skip non-directories
113               continue
114      all_perms=fileperm.get_perms(file)
115      for (domain_id,mask) in all_perms.items():
116            if string.find(domain_id,'\\')!=-1:
117               (domain,sys_id)=string.split(domain_id,'\\',1)
118            else:
119               sys_id=domain_id
120            mask_name=get_mask(mask)
121            Results.append(file+','+sys_id+','+mask_name)
122   Results.sort()
123   return Results
124
125##############################################################################
126
127#h - help
128#r - recursive
129#o - output file
130#d - directories only
131
132domain='bedrock'
133
134Server=str(win32net.NetGetDCName("",domain))
135print '************************ Using domain ',domain
136
137only_dir=0
138D_group={}
139D_except={}
140if len(sys.argv)==1:
141   print sys.argv[0]," file or directory"
142   print "-r for recursive mode \n-o for output file (default screen) \n-d for directories only"
143   print 'Example:',sys.argv[0],'-o a.txt -r c:\\junk  \n ----goes down dir tree in c:\\junk and saves in a.txt'
144   sys.exit(0)
145else:
146   try:
147      optlist, args = getopt.getopt(sys.argv[1:], 'dho:r')
148   except getopt.error:
149      print "invalid option.  available options are: -d -h -r -o "
150      print "-r for recursive mode \n-o for output file (default screen) \n-d for directories only"
151
152      sys.exit(0)
153
154   opts = {'-d':0,'-h':0,'-o':0,'-r':0}
155   for key, value in optlist:
156      opts[key]=1
157      if key == '-o':
158            opts[key]=value
159   init=time.clock()
160
161
162   Results=[]
163   if opts['-r']:
164      if os.path.isdir(args[0]):
165            print 'walking thru',args[0]
166            get_perm_base(args[0])
167            os.path.walk(args[0],get_perms,opts['-d'])
168      else:
169            print 'Directory',args[0],'does not exist'
170            sys.exit(0)
171   else:
172      if os.path.exists(args[0]):
173            Results=get_perm(args[0])
174      else:
175            print 'Directory or file',args[0],'does not exist'
176            sys.exit(0)
177
178   #now print out the results
179   if opts['-o']:
180      #send to a file
181      print 'Storing results in',opts['-o']
182      f=open(opts['-o'],'w')
183      for i in Results:
184            f.write(i)
185            f.write('\n')
186   else:
187      for i in Results:
188            print i
189      end = time.clock()-init

In Conclusion

Extending python isn’t as simple as writing python, but it greatly expands python’s capabilities. There are many details not covered here like reference counting, threading, and error handeling. The python website has documentation about Extending Python http://www.python.org/doc/current/ext/ext.html .

Have a great time with programming with python!

Further Info

Microsoft MSDN references http://msdn.microsoft.com/ Extending Python http://www.python.org/doc/current/ext/ext.html compile.py http://starship.python.net/crew/da/compile SWIG http://www.swig.org/ BPL (Boost Python Libraries) http://www.boost.org/libs/python/doc/index.html