~yuriy-kozlov/guidance/userconfig-kde4

« back to all changes in this revision

Viewing changes to unixauthdb.py

  • Committer: Yuriy Kozlov
  • Date: 2008-07-27 20:20:36 UTC
  • Revision ID: yuriy.kozlov@gmail.com-20080727202036-g6lmrlgyd9uwte5n
Initial import.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
###########################################################################
 
3
#    Copyright (C) 2004-2006 by Simon Edwards                                      
 
4
#    <simon@simonzone.com>                                                             
 
5
#
 
6
# Copyright: See COPYING file that comes with this distribution
 
7
#
 
8
###########################################################################
 
9
# An API for querying and modifying the authorisation database on Unix systems.
 
10
#
 
11
# The first function that you need to use is getContext(). It returns a 
 
12
# Context object that contains all relevant information concerning 
 
13
# the current authorisation database on this machine.
 
14
 
 
15
import crypt
 
16
import random
 
17
import fcntl
 
18
import time
 
19
import os
 
20
import os.path
 
21
import stat
 
22
import shutil
 
23
import codecs
 
24
import locale
 
25
import tempfile
 
26
 
 
27
ldaperror = ""
 
28
try:
 
29
    import ldap 
 
30
except ImportError:
 
31
    ldaperror = "The LDAP Python Module is not installed, but needed to use LDAP. Install it."
 
32
 
 
33
def createTempFile(origfile):
 
34
    origstat = os.stat(origfile)
 
35
    tmp_prefix = os.path.basename(origfile) + "."
 
36
    tmp_dir = os.path.dirname(origfile)
 
37
    try:
 
38
        ret = tempfile.mkstemp(prefix=tmp_prefix, dir=tmp_dir)
 
39
    except:
 
40
        raise IOError, "Unable to create a new temporary file for " + origfile
 
41
    (fd, tmpfile) = ret
 
42
    shutil.copymode(origfile, tmpfile)
 
43
    os.chown(tmpfile, origstat.st_uid, origstat.st_gid)
 
44
 
 
45
    return ret
 
46
 
 
47
def getContext(editmode=False):
 
48
    """Get a Context object describing the system's authorisation database.
 
49
 
 
50
    Parameters:
 
51
 
 
52
    editmode - Set to true if you also wish change the information in this
 
53
    context. Root access is required. Defaults to false.
 
54
 
 
55
    Returns a Context object.
 
56
 
 
57
    If the environmental variable "USERCONFIG_USES_LDAP" is set to "true",
 
58
    userconfig will use LDAP as the backend. This feature is in development
 
59
    and using it is not recommended, it won't work.
 
60
    """
 
61
 
 
62
    # Detect what kind of auth system we are running on and create
 
63
    # and initialise the corresponding Context object type.
 
64
 
 
65
    # Check for Mandrake
 
66
 
 
67
    # Check libuser.conf
 
68
    try:
 
69
        if os.environ["USERCONFIG_USES_LDAP"].lower() == "true":
 
70
            use_ldap = True 
 
71
    except KeyError,e:
 
72
        use_ldap = False
 
73
    if not use_ldap:
 
74
        return PwdContext(editmode)
 
75
    else:
 
76
        print "==================================================================="
 
77
        print "Warning:"
 
78
        print "\tYou are using LDAP as backend. This feature is under development"
 
79
        print "\tand it is currently not recommended to use it."
 
80
        print "\tIf you do not want to use LDAP as backend, set the environmental"
 
81
        print "\tvariabale 'USERCONFIG_USES_LDAP' to 'False'."
 
82
        print "==================================================================="
 
83
        return LdapContext(editmode)
 
84
 
 
85
###########################################################################
 
86
# Base classes.
 
87
#
 
88
class Context(object):
 
89
    """Contains all of the information about the current authorisation
 
90
    database, plus some useful methods for modify this information.
 
91
 
 
92
    """
 
93
    def __init__(self):
 
94
        self._users = []
 
95
        self._groups = []
 
96
        self._shells = None
 
97
        self._setDefaultValues()
 
98
 
 
99
    def newUser(self,defaults=False,systemuser=False):
 
100
        """Create a new UnixUser object.
 
101
 
 
102
        Creates a new blank UnixUser object. The object is not part of the
 
103
        current Context. You need to add it yourself using addUser().
 
104
 
 
105
        Newly allocated UIDs are unique with respect to the list of UnixUser
 
106
        objects in the Context. 
 
107
 
 
108
        Keyword arguments:
 
109
        defaults -- Set to true if the new object should be filled in with
 
110
                    reasonable default values for the UID and username.
 
111
                    (default False)
 
112
        systemuser -- Should the new user be allocated a UID from the system
 
113
                      range of UIDs. (default is False)
 
114
 
 
115
        Returns a new UnixUser object.
 
116
        """
 
117
        newuserobj = self._createUser()
 
118
        if defaults:
 
119
            if systemuser:
 
120
                r = xrange(0,self.last_system_uid)
 
121
            else:
 
122
                r = xrange(self.first_uid,self.last_uid)
 
123
            for candiate in r:
 
124
                for u in self._users:
 
125
                    if u.getUID()==candiate:
 
126
                        break
 
127
                else:
 
128
                    newuserobj.setUID(candiate)
 
129
                    break
 
130
 
 
131
            if self.lookupUsername(u'new_user') is None:
 
132
                newuserobj.setUsername(u'new_user')
 
133
            else:
 
134
                i = 1
 
135
                while 1:
 
136
                    if self.lookupUsername(u'new_user_'+str(i)) is None:
 
137
                        newuserobj.setUsername(u'new_user_'+str(i))
 
138
                        break
 
139
                    i += 1
 
140
        return newuserobj
 
141
 
 
142
    def getUsers(self):
 
143
        """Get a list of all existing users.
 
144
 
 
145
        Returns an array of UnixUser objects.
 
146
        """
 
147
        #print "USERS:", self._users
 
148
        return self._users[:]  
 
149
 
 
150
    def getGroups(self):
 
151
        """Get a list of all existing groups.
 
152
 
 
153
        Returns an array of UnixGroup objects.
 
154
        """
 
155
        try:
 
156
            self._groups.remove("new_user")
 
157
        except ValueError:
 
158
            print "no user removed"
 
159
            pass
 
160
        return self._groups[:]
 
161
 
 
162
    def newGroup(self,defaults=False,systemgroup=False):
 
163
        """Create a new UnixGroup object.
 
164
 
 
165
        Creates a new blank UnixGroup object. The object is not part of the
 
166
        current Context. You need to add it yourself using addGroup().
 
167
 
 
168
        Newly allocated GIDs are unique with respect to the list of UnixGroup
 
169
        objects in the Context. 
 
170
 
 
171
        Keyword arguments:
 
172
        defaults -- Set to true if the new object should be filled in with
 
173
                    reasonable default values for the GID and groupname.
 
174
                    (default False)
 
175
        systemgroup  -- Set to True if the newly allocated GID should come
 
176
                        from the pool of system group IDs. (default False)
 
177
 
 
178
        Returns a new UnixGroup object.
 
179
        """
 
180
        newgroupobj = self._createGroup()
 
181
        if defaults:
 
182
            if systemgroup:
 
183
                r = xrange(0,self.last_system_gid)
 
184
            else:
 
185
                r = xrange(self.first_gid,self.last_gid)
 
186
            for candiate in r:
 
187
                for u in self._groups:
 
188
                    if u.getGID()==candiate:
 
189
                        break
 
190
                else:
 
191
                    newgroupobj.setGID(candiate)
 
192
                    break
 
193
            if self.lookupGroupname(u'new_group') is None:
 
194
                newgroupobj.setGroupname(u'new_group')
 
195
            else:
 
196
                i = 1
 
197
                while 1:
 
198
                    if self.lookupGroupname(u'new_user_'+str(i)) is None:
 
199
                        newgroupobj.setGroupname(u'new_user_'+str(i))
 
200
                        break
 
201
                    i += 1
 
202
        return newgroupobj
 
203
 
 
204
    def _createGroup(self):
 
205
        raise NotImplementedError, "Context.newGroup()"
 
206
 
 
207
    def addUser(self,userobj):
 
208
        """Adds the given user to the authorisation database.
 
209
 
 
210
        This change only takes effect after calling context.save().
 
211
 
 
212
        Keyword arguments:
 
213
        userobj -- The UnixUser object to add.
 
214
        """
 
215
        self._users.append(userobj)
 
216
 
 
217
    def addGroup(self,groupobj):
 
218
        """Adds the given group to the authorisation database.
 
219
 
 
220
        This change only takes effect after calling context.save().
 
221
 
 
222
        Keyword arguments:
 
223
        groupobj -- The UnixGroup object to add.
 
224
        """
 
225
        if groupobj not in self._groups:
 
226
            self._groups.append(groupobj)
 
227
 
 
228
    def removeUser(self,userobj):
 
229
        """Removes the given user object from the authorisation database.
 
230
 
 
231
        The user is also removed from all groups.
 
232
 
 
233
        This change only takes effect after calling context.save().
 
234
        """
 
235
        for g in userobj.getGroups():
 
236
            userobj.removeFromGroup(g)
 
237
 
 
238
        self._users.remove(userobj)
 
239
 
 
240
    def removeGroup(self,groupobj):
 
241
        """Removes the given group object from the authorisation database.
 
242
 
 
243
        All users are removed from the group.
 
244
 
 
245
        This change only takes effect after calling context.save().
 
246
        """
 
247
        for u in groupobj.getUsers():
 
248
            u.removeFromGroup(groupobj)
 
249
 
 
250
        self._groups.remove(groupobj)
 
251
 
 
252
    def lookupUID(self,uid):
 
253
        """Lookup a UnixUser object by its numeric user ID.
 
254
 
 
255
        Keyword arguments:
 
256
        uid -- User ID to lookup, integer.
 
257
 
 
258
        Returns the matching UnixUser object or None if it was not found.
 
259
        """
 
260
        for user in self._users:
 
261
            if user.getUID()==uid:
 
262
                return user
 
263
        return None
 
264
 
 
265
    def lookupUsername(self,username):
 
266
        """Lookup a UnixUser object by username.
 
267
 
 
268
        Keyword arguments:
 
269
        username -- Username to lookup, string.
 
270
 
 
271
        Returns the matching UnixUser object or None if it was not found.
 
272
        """
 
273
        for user in self._users:
 
274
            if user.getUsername()==username:
 
275
                return user
 
276
        return None
 
277
 
 
278
    def lookupGID(self,gid):
 
279
        """Lookup a UnixGroup object by its numeric group ID.
 
280
 
 
281
        Keyword arguments:
 
282
        gid -- Group ID to lookup, integer.
 
283
 
 
284
        Returns the matching UnixGroup object or None if it was not found.
 
285
        """
 
286
        for group in self._groups:
 
287
            if group.getGID()==gid:
 
288
                return group
 
289
        return None
 
290
 
 
291
    def lookupGroupname(self,groupname):
 
292
        """Lookup a UnixGroup object by groupname.
 
293
 
 
294
        Returns the matching UnixGroup object or None if it was not found.
 
295
        """
 
296
        for group in self._groups:
 
297
            if group.getGroupname()==groupname:
 
298
                return group
 
299
        return None
 
300
 
 
301
    def getUserShells(self):
 
302
        """Get the list of available login shells.
 
303
 
 
304
        Returns an array of strings.
 
305
        """
 
306
        if self._shells is None:
 
307
            self._shells = []
 
308
            fhandle = codecs.open('/etc/shells','r',locale.getpreferredencoding())
 
309
            for l in fhandle.readlines():
 
310
                # TODO: strangely this lets some comented lines slip through
 
311
                if len(l.strip()) > 1 and l.strip()[0] is not "#":
 
312
                    # Only show existing shells
 
313
                    if os.path.isfile(l.strip()): 
 
314
                        self._shells.append(l.strip())
 
315
            fhandle.close()
 
316
        return self._shells[:]
 
317
 
 
318
    def save(self):
 
319
        """Synchronises the Context with the underlying operating system.
 
320
 
 
321
        After a successful save, any changes to the Context will be reflected
 
322
        system wide.
 
323
        """
 
324
        raise NotImplementedError, "Context.save()"
 
325
 
 
326
    def createHomeDirectory(self,userobj):
 
327
        if os.path.exists(userobj.getHomeDirectory()):
 
328
            raise IOError, u"Home directory %s already exists." % userobj.getHomeDirectory()
 
329
 
 
330
        # Copy the skeleton directory over
 
331
        shutil.copytree(self._getSkeletonDirectory(),userobj.getHomeDirectory(),True)
 
332
 
 
333
        # Fix the file ownership stuff
 
334
        uid = userobj.getUID()
 
335
        gid = userobj.getPrimaryGroup().getGID()
 
336
        os.chmod(userobj.getHomeDirectory(),self.dir_mode)
 
337
        #os.system("chmod "+self.dir_mode+" "+userobj.getHomeDirectory())
 
338
        #print "Setting permissions:", userobj.getHomeDirectory(),self.dir_mode
 
339
        os.lchown(userobj.getHomeDirectory(),uid,gid)
 
340
        for root,dirs,files in os.walk(userobj.getHomeDirectory()):
 
341
            for d in dirs:
 
342
                os.lchown(os.path.join(root,d),uid,gid)
 
343
            for f in files:
 
344
                os.lchown(os.path.join(root,f),uid,gid)
 
345
 
 
346
    def removeHomeDirectory(self,userobj):
 
347
        if os.path.exists(userobj.getHomeDirectory()):
 
348
            shutil.rmtree(userobj.getHomeDirectory())
 
349
 
 
350
    def _createUser(self):
 
351
        raise NotImplementedError, "Context._createUser()"
 
352
 
 
353
    def _sanityCheck(self):
 
354
        userids = []
 
355
        for u in self._users:
 
356
            if isinstance(u,UnixUser)==False:
 
357
                raise TypeError,"Found an object in the list of users that is not a UnixUser object."
 
358
            uid = u.getUID()
 
359
            if uid in userids:
 
360
                raise ValueError, "User ID %i appears more than once." % uid
 
361
            userids.append(uid)
 
362
            u._sanityCheck()
 
363
 
 
364
        groupids = []
 
365
        for g in self._groups:
 
366
            if isinstance(g,UnixGroup)==False:
 
367
                raise TypeError,"Found an object in the list of groups that is not a UnixGroup object."
 
368
            gid = g.getGID()
 
369
            if gid in groupids:
 
370
                raise ValueError, "Group ID %i appears more than once." % gid
 
371
            groupids.append(gid)    
 
372
            g._sanityCheck()
 
373
 
 
374
    def _getSkeletonDirectory(self):
 
375
        return self.skel
 
376
 
 
377
    def _readAdduserConf(self):
 
378
        """ Fill a dictionary with the values from /etc/adduser.conf
 
379
            which then can be used as default values, if the file exists
 
380
            at least. 
 
381
            Attention: We're not validating!"""
 
382
        self.defaults = {}
 
383
        self.adduserconf = '/etc/adduser.conf'
 
384
        if not os.path.isfile(self.adduserconf):
 
385
            return
 
386
        fhandle = codecs.open(self.adduserconf,'r',locale.getpreferredencoding())
 
387
        for line in fhandle.readlines():
 
388
            line = line.strip()
 
389
            parts = line.split("=")
 
390
            if len(parts) == 2:
 
391
                self.defaults[str(parts[0].strip())] = parts[1].strip()
 
392
 
 
393
    def _setDefaultValues(self):
 
394
        """ Set a lot of default values for UIDs and GIDs, try to use the values
 
395
            from /etc/adduser.conf."""
 
396
        self._readAdduserConf()
 
397
 
 
398
        try:
 
399
            self.skel = self.defaults["SKEL"]
 
400
        except KeyError:
 
401
            self.skel = '/etc/skel'
 
402
 
 
403
        # IDs for new users and groups.
 
404
        try:
 
405
            self.first_uid = int(self.defaults['FIRST_UID'])
 
406
        except (KeyError,ValueError):
 
407
            self.first_uid = 1000
 
408
 
 
409
        try:
 
410
            self.last_uid = int(self.defaults["LAST_UID"])
 
411
        except (KeyError,ValueError):
 
412
            self.last_uid = 29999
 
413
 
 
414
        try:
 
415
            self.first_gid = int(self.defaults["FIRST_GID"])
 
416
        except (KeyError,ValueError):
 
417
            self.first_gid = 1000
 
418
 
 
419
        try:
 
420
            self.last_gid = int(self.defaults["LAST_GID"])
 
421
        except (KeyError,ValueError):
 
422
            self.last_gid = 65534
 
423
 
 
424
        # Which IDs are system user and system groups?
 
425
        try:
 
426
            self.first_system_uid = int(self.defaults["FIRST_SYSTEM_UID"])
 
427
        except (KeyError,ValueError):
 
428
            self.first_system_uid = 500
 
429
 
 
430
        try:
 
431
            self.last_system_uid = int(self.defaults["LAST_SYSTEM_UID"])
 
432
        except (KeyError,ValueError):
 
433
            self.last_system_uid = 65534
 
434
 
 
435
        try:
 
436
            self.first_system_gid = int(self.defaults["FIRST_SYSTEM_GID"])
 
437
        except (KeyError,ValueError):
 
438
            self.first_system_gid = 500
 
439
 
 
440
        try:
 
441
            self.last_system_gid = int(self.defaults["LAST_SYSTEM_GID"])
 
442
        except (KeyError,ValueError):
 
443
            self.last_system_gid = 65534
 
444
 
 
445
        # More defaults which might make sense.
 
446
        try:
 
447
            self.dir_mode = int(self.defaults["DIR_MODE"],8)
 
448
        except (KeyError,ValueError):
 
449
            self.dir_mode = int("0755",8)
 
450
            print "Didn't read default DIR_MODE"
 
451
 
 
452
        try:
 
453
            self.dhome = self.defaults["DHOME"]
 
454
        except KeyError:
 
455
            self.dhome = "/home"
 
456
 
 
457
        try:
 
458
            self.dshell = self.defaults["DSHELL"]
 
459
        except KeyError:
 
460
            # Will be set in showNewUser()
 
461
            self.dshell = None
 
462
 
 
463
###########################################################################
 
464
class UnixUser(object):
 
465
    def __init__(self,context):
 
466
        self._context = context
 
467
        self._uid = None
 
468
        self._username = None
 
469
 
 
470
        # UnixGroup object.
 
471
        self._primarygroup = None
 
472
 
 
473
        # List of UnixGroup objects.
 
474
        self._groups = []
 
475
 
 
476
        self._gecos = None
 
477
        self._homedirectory = None
 
478
        self._loginshell = None
 
479
 
 
480
        self._islocked = False
 
481
 
 
482
        self._encpass = ""
 
483
 
 
484
        # FIXME : This should actually be days since epoch or something like this
 
485
        self._passlastchange = 0 
 
486
        self._passminimumagebeforechange = 0
 
487
        self._passmaximumage = None
 
488
        self._passexpirewarn = 7
 
489
        self._passexpiredisabledays = None
 
490
        self._disableddays = None
 
491
 
 
492
    def polish(self):
 
493
        primary_group = self._context.lookupGID(self._gid)
 
494
        if primary_group is None:
 
495
            # The GID didn't match an existing group. Quickly make a new group.
 
496
            new_group = self._context.newGroup()
 
497
            new_group.setGID(self._gid)
 
498
 
 
499
            new_group_name = u"group%i" % self._gid
 
500
            i = 0
 
501
            while self._context.lookupGroupname(new_group_name) is not None:
 
502
                i += 1
 
503
                new_group_name = u"group%i_%i" % (self._gid,i)
 
504
            new_group.setGroupname(new_group_name)
 
505
 
 
506
            self._context.addGroup(new_group)
 
507
            primary_group = new_group
 
508
 
 
509
        self.setPrimaryGroup(primary_group)
 
510
        for group in self._context._groups:
 
511
            if group.contains(self):
 
512
                self._groups.append(group)
 
513
 
 
514
    def getUID(self):
 
515
        """Get the unix user ID.
 
516
 
 
517
        Returns the integer.
 
518
        """
 
519
        return self._uid
 
520
 
 
521
    def setUID(self,uid):
 
522
        """Set the unix user ID.
 
523
 
 
524
        Keyword arguments:
 
525
        uid -- Integer user id.
 
526
        """
 
527
        uid = int(uid)
 
528
        if uid<0:
 
529
            raise ValueError, "User ID (%i) is a negative number." % uid
 
530
        self._uid = uid
 
531
 
 
532
    def isSystemUser(self):
 
533
        """See if this user is a system user.
 
534
 
 
535
        Returns True or False.
 
536
        """
 
537
        return not (self._context.first_uid <= self._uid < self._context.last_uid)
 
538
 
 
539
    def getUsername(self): return self._username
 
540
 
 
541
    def setUsername(self,username): self._username = username
 
542
 
 
543
    def getPrimaryGroup(self):
 
544
        """Get the primary group for this user.
 
545
 
 
546
        Returns a UnixGroup object.
 
547
        """
 
548
        return self._primarygroup
 
549
 
 
550
    def setPrimaryGroup(self,groupobj):
 
551
        """Set the primary group for this user.
 
552
 
 
553
        If the given group is not part of this user's list of groups, then
 
554
        it will be added.
 
555
 
 
556
        Keyword arguments:
 
557
        groupobj -- The group to set as the primary group.
 
558
        """
 
559
        self.addToGroup(groupobj)
 
560
        self._primarygroup = groupobj
 
561
 
 
562
    def getGroups(self):
 
563
        """Get the list of groups that this user belongs to.
 
564
 
 
565
        The user's primary group is also included in the returned list.
 
566
 
 
567
        Returns a list of UnixGroup objects. Modify the list does not affect
 
568
        this UnixUser object.
 
569
        """
 
570
        return self._groups[:]
 
571
 
 
572
    def addToGroup(self,groupobj):
 
573
        """Add this user to the given group.
 
574
 
 
575
        Keyword arguments:
 
576
        groupobj -- UnixGroup object.
 
577
        """
 
578
        groupobj._addUser(self)
 
579
        if groupobj not in self._groups:
 
580
            self._groups.append(groupobj)
 
581
 
 
582
    def removeFromGroup(self,groupobj):
 
583
        """Remove this user from the given group.
 
584
 
 
585
        If group is current this user's primary group, then
 
586
 
 
587
        Keyword arguments:
 
588
        groupobj -- UnixGroup object.
 
589
        """
 
590
        groupobj._removeUser(self)
 
591
        try:
 
592
            self._groups.remove(groupobj)
 
593
        except ValueError:
 
594
            pass
 
595
        if self._primarygroup is groupobj:
 
596
            if len(self._groups)==0:
 
597
                self._primarygroup = None
 
598
            else:
 
599
                self._primarygroup = self._groups[0]
 
600
 
 
601
    def setPassword(self,password):
 
602
        # Make some salt.
 
603
        space = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQSRTUVWXYZ0123456789./'
 
604
        salt = ""
 
605
        for x in range(8):
 
606
            salt += space[random.randint(0,len(space)-1)]
 
607
        self._encpass = crypt.crypt(password,'$1$'+salt+'$')
 
608
 
 
609
    def isLocked(self): return self._islocked
 
610
    def setLocked(self,locked): self._islocked = locked
 
611
 
 
612
    def getRealName(self): 
 
613
        if not self._gecos:
 
614
            return ""
 
615
        try:
 
616
            return self._gecos.split(",")[0]
 
617
        except AttributeError:
 
618
            return self._gecos
 
619
 
 
620
    def setRealName(self,realname): self._gecos = realname
 
621
    def getHomeDirectory(self): return self._homedirectory
 
622
    def setHomeDirectory(self,homedirectory): self._homedirectory = homedirectory
 
623
    def getLoginShell(self): return self._loginshell
 
624
    def setLoginShell(self,loginshell): self._loginshell = loginshell
 
625
 
 
626
    # 'None' means that there is no maximum password age.
 
627
    def getMaximumPasswordAge(self): return self._passmaximumage
 
628
    def setMaximumPasswordAge(self,days): self._passmaximumage = days
 
629
 
 
630
    def getMinimumPasswordAgeBeforeChange(self): return self._passminimumagebeforechange
 
631
    def setMinimumPasswordAgeBeforeChange(self,days): self._passminimumagebeforechange = days
 
632
    def getPasswordDisableAfterExpire(self): return self._passexpiredisabledays
 
633
    def setPasswordDisableAfterExpire(self,days): self._passexpiredisabledays = days
 
634
    def getPasswordExpireWarning(self): return self._passexpirewarn
 
635
    def setPasswordExpireWarning(self,days): self._passexpirewarn = days
 
636
    def getLastPasswordChange(self): return self._passlastchange
 
637
    def getExpirationDate(self): return self._disableddays
 
638
    def setExpirationDate(self,unixdate): self._disableddays = unixdate
 
639
 
 
640
    def __str__(self):
 
641
        return "%s(%i)" % (self._username,self._uid)
 
642
 
 
643
    def _sanityCheck(self):
 
644
        if self._primarygroup is None:
 
645
            raise ValueError,"Userobj has no primary group!"
 
646
        if self._uid is None:
 
647
            raise ValueError,"Userobj has no UID!"
 
648
 
 
649
###########################################################################
 
650
class UnixGroup(object):
 
651
    def __init__(self,context):
 
652
        self._context = context
 
653
 
 
654
        # List of UnixUser objects.
 
655
        self._members = []
 
656
 
 
657
        self._gid = None
 
658
        self._groupname = None
 
659
 
 
660
    def contains(self,userobj):
 
661
        """Check if a the given user is a member of this group.
 
662
 
 
663
        Returns True or False.
 
664
        """
 
665
        return userobj in self._members
 
666
 
 
667
    def polish(self): pass
 
668
    def isSystemGroup(self):
 
669
        """Check if this group is a system group.
 
670
 
 
671
        Returns True or False.
 
672
        """
 
673
        return not (self._context.first_gid <= self._gid < self._context.last_gid)
 
674
        #return not (500 <= self._gid < 65534)
 
675
 
 
676
    def getGID(self):
 
677
        """Get the unix group ID.
 
678
 
 
679
        Returns the integer group id.
 
680
        """
 
681
        return self._gid
 
682
 
 
683
    def setGID(self,gid):
 
684
        """Set the unix group ID.
 
685
 
 
686
        Keyword arguments:
 
687
        gid -- new group id, integer.
 
688
        """
 
689
        self._gid = gid
 
690
 
 
691
    def getGroupname(self): return self._groupname
 
692
    def setGroupname(self,groupname): self._groupname = groupname
 
693
    def getUsers(self): return self._members[:]
 
694
    def _addUser(self,userobj):
 
695
        if not self.contains(userobj):
 
696
            self._members.append(userobj)
 
697
 
 
698
    def _removeUser(self,userobj):
 
699
        try:
 
700
            self._members.remove(userobj)
 
701
        except ValueError:
 
702
            pass
 
703
 
 
704
    def __str__(self):
 
705
        # FIXME encoding
 
706
        return str(self._groupname) + " (" + str(self._gid) + ") " + str([str(u) for u in self._members])
 
707
 
 
708
    def _sanityCheck(self):
 
709
        pass
 
710
 
 
711
###########################################################################
 
712
class PwdContext(Context):
 
713
    #def __init__(self,editmode,passwordfile="etc-passwd",groupfile='etc-group',shadowfile="etc-shadow"):
 
714
    def __init__(self,editmode,passwordfile="/etc/passwd",groupfile='/etc/group',shadowfile="/etc/shadow"):
 
715
        Context.__init__(self)
 
716
        self.__editmode = editmode
 
717
        self.__passwordfile = passwordfile
 
718
        self.__groupfile = groupfile
 
719
        self.__shadowfile = shadowfile
 
720
        self._setDefaultValues()
 
721
 
 
722
        # Read in the password file
 
723
        fhandle = codecs.open(passwordfile,'r',locale.getpreferredencoding())
 
724
        if LockFDRead(fhandle.fileno())==False:
 
725
            raise IOError,"Unable to lock the "+passwordfile+" file."
 
726
        try:
 
727
            for line in fhandle.readlines():
 
728
                if line.strip()!="":
 
729
                    newuserobj = self.newUser(False)
 
730
                    newuserobj._initString(line)
 
731
                    self._users.append(newuserobj)
 
732
        finally:
 
733
            UnlockFD(fhandle.fileno())
 
734
            fhandle.close()
 
735
 
 
736
        # Read the group file
 
737
        fhandle = codecs.open(groupfile,'r',locale.getpreferredencoding())
 
738
        if LockFDRead(fhandle.fileno())==False:
 
739
            raise IOError,"Unable to lock the "+groupfile+" file."
 
740
        try:
 
741
            for line in fhandle.readlines():
 
742
                if line.strip()!="":
 
743
                    newgroupobj = self.newGroup(False)
 
744
                    newgroupobj._initString(line)
 
745
                    self._groups.append(newgroupobj)
 
746
        finally:
 
747
            UnlockFD(fhandle.fileno())
 
748
            fhandle.close()
 
749
 
 
750
        if self.__editmode:
 
751
            # Load up the info from the shadow file too.
 
752
            fhandle = codecs.open(shadowfile,'r',locale.getpreferredencoding())
 
753
            if LockFDRead(fhandle.fileno())==False:
 
754
                raise IOError,"Unable to lock the "+shadowfile+" file."
 
755
            try:
 
756
                for line in fhandle.readlines():
 
757
                    if line.strip()!="":
 
758
                        try:
 
759
                            (username,encpass,passlastchange,passminimumagebeforechange,passmaximumage, \
 
760
                                passexpirewarn,passexpiredisabledays,disableddays,reserve) = \
 
761
                                tuple(line.strip().split(":"))
 
762
                            userobj = self.lookupUsername(username)
 
763
                            if userobj is not None:
 
764
                                if encpass=="":
 
765
                                    encpass = u"*"
 
766
                                userobj._encpass = encpass
 
767
                                if userobj._encpass[0]=='!':
 
768
                                    userobj._islocked = True
 
769
                                    userobj._encpass = userobj._encpass[1:]
 
770
                                else:
 
771
                                    userobj._islocked = False
 
772
                                # FIXME : set time
 
773
                                if passlastchange and passlastchange!=u"None":
 
774
                                    userobj._passlastchange = int(passlastchange)
 
775
                                else:
 
776
                                    passlastchange = 0
 
777
 
 
778
                                if passminimumagebeforechange=="":
 
779
                                    passminimumagebeforechange = None
 
780
                                else:
 
781
                                    passminimumagebeforechange = int(passminimumagebeforechange)
 
782
                                    if passminimumagebeforechange>=99999:
 
783
                                        passminimumagebeforechange = None
 
784
                                userobj._passminimumagebeforechange = passminimumagebeforechange
 
785
 
 
786
                                if passmaximumage=="":
 
787
                                    passmaximumage = None
 
788
                                else:
 
789
                                    passmaximumage = int(passmaximumage)
 
790
                                    if passmaximumage>=99999:
 
791
                                        passmaximumage = None
 
792
                                userobj._passmaximumage = passmaximumage
 
793
 
 
794
                                if passexpirewarn=="":
 
795
                                    passexpirewarn = None
 
796
                                else:
 
797
                                    passexpirewarn = int(passexpirewarn)
 
798
                                    if passexpirewarn>=99999:
 
799
                                        passexpirewarn = None
 
800
                                userobj._passexpirewarn = passexpirewarn
 
801
 
 
802
                                if passexpiredisabledays=="":
 
803
                                    userobj._passexpiredisabledays = None
 
804
                                else:
 
805
                                    userobj._passexpiredisabledays = int(passexpiredisabledays)
 
806
 
 
807
                                if disableddays=="" or disableddays==u"99999":
 
808
                                    userobj._disableddays = None
 
809
                                else:
 
810
                                    userobj._disableddays = int(disableddays)
 
811
 
 
812
                                userobj._reserve = reserve
 
813
                            else:
 
814
                                print "Couldn't find",username
 
815
                        except ValueError:
 
816
                            pass
 
817
            finally:
 
818
                UnlockFD(fhandle.fileno())
 
819
                fhandle.close()
 
820
 
 
821
        for group in self._groups:
 
822
            group.polish()
 
823
        for user in self._users:
 
824
            user.polish()
 
825
 
 
826
    def _createUser(self):
 
827
        return PwdUser(self)
 
828
 
 
829
    def _createGroup(self):
 
830
        return PwdGroup(self)
 
831
 
 
832
    def save(self):
 
833
        if self.__editmode==False:
 
834
            raise IOError, "Can't save, the context was created Read only."
 
835
 
 
836
        self._sanityCheck()
 
837
 
 
838
        # Write out the new password file.        
 
839
        (fd, tmpname) = createTempFile(self.__passwordfile)
 
840
        for u in self._users:
 
841
            os.write(fd, u._getPasswdEntry().encode(locale.getpreferredencoding(),'replace'))
 
842
            #print u._getPasswdEntry()
 
843
        os.close(fd)
 
844
 
 
845
        # Update the passwd file
 
846
        passwordlock = os.open(self.__passwordfile, os.O_WRONLY) # FIXME encoding
 
847
        if LockFDWrite(passwordlock)==False:
 
848
            raise IOError,"Couldn't get a write lock on "+self.__passwordfile
 
849
        try:
 
850
            os.rename(tmpname, self.__passwordfile)
 
851
        finally:
 
852
            UnlockFD(passwordlock)
 
853
            os.close(passwordlock)
 
854
 
 
855
        # Write out the new group file
 
856
        (fd, tmpname) = createTempFile(self.__groupfile)
 
857
        origstat = os.stat(self.__groupfile)
 
858
        for g in self._groups:
 
859
            os.write(fd,g._getGroupFileEntry().encode(locale.getpreferredencoding()))
 
860
            #print g._getGroupFileEntry()[:-1]
 
861
        os.close(fd)
 
862
        os.chown(tmpname, origstat.st_uid, origstat.st_gid)
 
863
 
 
864
        # Update the group file.
 
865
        grouplock = os.open(self.__groupfile, os.O_WRONLY)
 
866
        if LockFDWrite(grouplock)==False:
 
867
            raise IOError,"Couldn't get write lock on "+self.__groupfile
 
868
        try:
 
869
            os.rename(tmpname, self.__groupfile)
 
870
        finally:
 
871
            UnlockFD(grouplock)
 
872
            os.close(grouplock)
 
873
 
 
874
        # Write out the new shadow file
 
875
        origstat = os.stat(self.__shadowfile)
 
876
        (fd, tmpname) = createTempFile(self.__shadowfile)
 
877
        for u in self._users:
 
878
            os.write(fd,u._getShadowEntry().encode(locale.getpreferredencoding()))
 
879
            #print u._getShadowEntry()[:-1]
 
880
        os.close(fd)
 
881
 
 
882
        # Update the shadow file.
 
883
 
 
884
        # Make sure that it is writable.
 
885
        if (origstat.st_mode & stat.S_IWUSR)==0:
 
886
            os.chmod(self.__shadowfile,origstat.st_mode|stat.S_IWUSR)
 
887
 
 
888
        shadowlock = os.open(self.__shadowfile, os.O_WRONLY)
 
889
        if LockFDWrite(shadowlock)==False:
 
890
            raise IOError,"Couldn't get write lock on "+self.__shadowfile
 
891
        try:
 
892
            os.rename(tmpname, self.__shadowfile)
 
893
        finally:
 
894
            UnlockFD(shadowlock)
 
895
            os.close(shadowlock)
 
896
 
 
897
        # set the permissions back to thier default.
 
898
        if (origstat.st_mode & stat.S_IWUSR)==0:
 
899
            os.chmod(self.__shadowfile,origstat.st_mode)
 
900
 
 
901
###########################################################################
 
902
class LdapContext(Context):
 
903
 
 
904
    def __init__(self,editmode,server="localhost",admin_dn="",admin_pass=""):
 
905
        """ Connect to the LDAP server and invoke further actions. 
 
906
        """
 
907
        Context.__init__(self)
 
908
        # admin_dn is DistinguishedName? (or dn, for short)
 
909
        self.server = server
 
910
        self.baseDN = "dc=vizZzion,dc=net"
 
911
 
 
912
        self.url = "ldap://"+self.server
 
913
 
 
914
        self.ldapserver = ldap.initialize(self.url)
 
915
        self.ldapserver.protocol_version = ldap.VERSION3
 
916
 
 
917
        self.editmode = editmode
 
918
        if not self.editmode:
 
919
            self.ldapserver.simple_bind("admin",admin_pass)
 
920
        print "Connected to ", self.url
 
921
 
 
922
        self._users = self._getUsers()
 
923
 
 
924
    def _getUsers(self):
 
925
        """ Retrieve a list of users from the LDAP server.
 
926
        """
 
927
        _users = []
 
928
        print "LdapContext._getUsers"
 
929
        searchScope = ldap.SCOPE_SUBTREE
 
930
        retrieveAttributes = None 
 
931
        searchFilter = "cn=*"
 
932
        try:
 
933
            ldap_result_id = self.ldapserver.search(self.baseDN, searchScope, searchFilter, retrieveAttributes)
 
934
            result_set = []
 
935
            while 1:
 
936
                result_type, result_data = self.ldapserver.result(ldap_result_id, 0)
 
937
                if (result_data == []):
 
938
                    break
 
939
                else:
 
940
                    if result_type == ldap.RES_SEARCH_ENTRY:
 
941
                        #print result_data[0][1]
 
942
                        #print " --------------------- "
 
943
                        result_set.append(result_data[0][1])
 
944
            #print result_set
 
945
        except ldap.LDAPError, e:
 
946
            print "ERROR: ",e
 
947
 
 
948
        if len(result_set) == 0:
 
949
            print "No Results."
 
950
            return 
 
951
        count = 0
 
952
        """
 
953
        for entry in result_set:
 
954
            for d in entry.keys():
 
955
                print d, "::", entry[d]
 
956
            print "======== Next User =============="
 
957
        """
 
958
        # Walk through result_set and create users.
 
959
        for entry in result_set:
 
960
            try:
 
961
                name = entry['cn'][0]
 
962
                login = entry['uid'][0]
 
963
                loginshell = entry['loginShell'][0]
 
964
                homedirectory = entry['homeDirectory'][0]
 
965
                uid = entry['uidNumber'][0]
 
966
                gid = entry['gidNumber'][0]
 
967
                count = count + 1
 
968
                #print "\n%d. User: %s\n\tName: %s\n\tShell: %s\n\tHomeDir: %s\n\tUID: %s\n\tGID: %s\n" %\
 
969
                #       (count, login, name, loginshell, homedirectory, uid, gid)
 
970
                # Create a new userobject
 
971
                new_user = self._createUser()
 
972
                new_user.setHomeDirectory(homedirectory)
 
973
                new_user.setUID(uid)
 
974
                new_user.setRealName(name)
 
975
                new_user.setLoginShell(loginshell)
 
976
                new_user.setUsername(login)
 
977
                _users.append(new_user)
 
978
                print "Number of Users:", len(self._users)
 
979
 
 
980
            except KeyError, e:
 
981
                # Debugging output...
 
982
                print "ERR:: ",e
 
983
                print 'err:: ',entry
 
984
        return _users
 
985
 
 
986
    def _createUser(self):
 
987
        return LdapUser(self)
 
988
 
 
989
    def _createGroup(self):
 
990
        return LdapGroup(self)
 
991
 
 
992
    def save(self):
 
993
        print "LdapContext.save() does nothing yet."
 
994
 
 
995
###########################################################################
 
996
class LdapUser(UnixUser):
 
997
 
 
998
    def __str__(self):
 
999
        return "LdapUser: %s(%i)" % (self._username,self._uid)
 
1000
 
 
1001
 
 
1002
###########################################################################
 
1003
class LdapGroup(UnixGroup):
 
1004
 
 
1005
    def __str__(self):
 
1006
        return "LdapGroup: %s(%i)" % (self._username,self._uid)
 
1007
 
 
1008
 
 
1009
###########################################################################
 
1010
class PwdUser(UnixUser):
 
1011
    def __init__(self,context):
 
1012
        UnixUser.__init__(self,context)
 
1013
        self._reserve = u""
 
1014
 
 
1015
    def _initString(self,line):
 
1016
        (self._username,x,self._uid,self._gid,self._gecos,self._homedirectory, \
 
1017
            self._loginshell) =  tuple(line.strip().split(":"))
 
1018
        self._uid = int(self._uid)
 
1019
        self._gid = int(self._gid)
 
1020
 
 
1021
    def _getPasswdEntry(self):
 
1022
        return u":".join( [self._username,
 
1023
            u"x",
 
1024
            unicode(self._uid),
 
1025
            unicode(self._primarygroup.getGID()),
 
1026
            self._gecos,
 
1027
            self._homedirectory,
 
1028
            self._loginshell ] ) + u"\n"
 
1029
 
 
1030
    def _getShadowEntry(self):
 
1031
        if self._islocked:
 
1032
            encpass = u'!' + self._encpass
 
1033
        else:
 
1034
            encpass = self._encpass
 
1035
 
 
1036
        if self._passminimumagebeforechange==None:
 
1037
            passminimumagebeforechange = ""
 
1038
        else:
 
1039
            passminimumagebeforechange = str(self._passminimumagebeforechange)
 
1040
 
 
1041
        if self._passmaximumage==None:
 
1042
            passmaximumage = u"99999"
 
1043
        else:
 
1044
            passmaximumage = unicode(self._passmaximumage)
 
1045
 
 
1046
        if self._disableddays==None:
 
1047
            disableddays = u""
 
1048
        else:
 
1049
            disableddays = unicode(self._disableddays)
 
1050
 
 
1051
        if self._passexpiredisabledays==None:
 
1052
            passexpiredisabledays = u""
 
1053
        else:
 
1054
            passexpiredisabledays = unicode(self._passexpiredisabledays)
 
1055
 
 
1056
        if self._passexpirewarn==None:
 
1057
            passexpirewarn = u""
 
1058
        else:
 
1059
            passexpirewarn = unicode(self._passexpirewarn)
 
1060
 
 
1061
        return u":".join( [self._username,
 
1062
            encpass,
 
1063
            unicode(self._passlastchange),
 
1064
            passminimumagebeforechange,
 
1065
            passmaximumage,
 
1066
            passexpirewarn,
 
1067
            passexpiredisabledays,
 
1068
            disableddays,
 
1069
            self._reserve ])+ u"\n"
 
1070
 
 
1071
###########################################################################
 
1072
class PwdGroup(UnixGroup):
 
1073
    def __init__(self,context):
 
1074
        UnixGroup.__init__(self,context)
 
1075
        self._memberids = u""
 
1076
        self._encpass = u""
 
1077
 
 
1078
    def _initString(self,line):
 
1079
        (self._groupname,self._encpass,self._gid,self._memberids) = tuple(line.strip().split(":"))
 
1080
        self._gid = int(self._gid)
 
1081
 
 
1082
    def polish(self):
 
1083
        membernames = self._memberids.split(",")
 
1084
        for username in membernames:
 
1085
            userobj = self._context.lookupUsername(username)
 
1086
            if userobj!=None:
 
1087
                self._members.append(userobj)
 
1088
 
 
1089
    def _getGroupFileEntry(self):
 
1090
        return u":".join( [ self._groupname,
 
1091
            self._encpass,
 
1092
            unicode(self._gid),
 
1093
            u",".join([u.getUsername() for u in self._members if u.getPrimaryGroup() is not self])]) + u"\n"
 
1094
 
 
1095
###########################################################################
 
1096
def LockFDRead(fd):
 
1097
    retries = 6
 
1098
    while retries!=0:
 
1099
        try:
 
1100
            fcntl.lockf(fd,fcntl.LOCK_SH | fcntl.LOCK_NB)
 
1101
            return True
 
1102
        except IOError:
 
1103
            # Wait a moment
 
1104
            time.sleep(1)
 
1105
    return False
 
1106
 
 
1107
def LockFDWrite(fd):
 
1108
    retries = 6
 
1109
    while retries!=0:
 
1110
        try:
 
1111
            fcntl.lockf(fd,fcntl.LOCK_EX | fcntl.LOCK_NB)
 
1112
            return True
 
1113
        except IOError:
 
1114
            # Wait a moment
 
1115
            time.sleep(1)
 
1116
    return False
 
1117
 
 
1118
def UnlockFD(fd):
 
1119
    fcntl.lockf(fd,fcntl.LOCK_UN)
 
1120
 
 
1121
###########################################################################
 
1122
 
 
1123
if __name__=='__main__':
 
1124
    print "Testing"
 
1125
    context = getContext(True)
 
1126
 
 
1127
    print "Stopping here..."
 
1128
    #import sys
 
1129
    #sys.exit(0) ## Remove.
 
1130
    #print "Users:"
 
1131
    #for user in context.getUsers():
 
1132
    for user in context._users:
 
1133
        print "--------------------------------------------------"
 
1134
        print "UID:",user.getUID()
 
1135
        print "Is system user:",user.isSystemUser()
 
1136
        print "Username:",user.getUsername()
 
1137
        print "Primary Group:",str(user.getPrimaryGroup())
 
1138
        print "Groups:",[str(u) for u in user.getGroups()]
 
1139
        print "Is locked:",user.isLocked()
 
1140
        print "Real name:",user.getRealName()
 
1141
        print "Home Dir:",user.getHomeDirectory()
 
1142
        print "Maximum password age:",user.getMaximumPasswordAge()
 
1143
        print "Minimum password age before change:",user.getMinimumPasswordAgeBeforeChange()
 
1144
        print "Expire warning:",user.getPasswordExpireWarning()
 
1145
        print "Disable after Expire:",user.getPasswordDisableAfterExpire()
 
1146
        #print user._getPasswdEntry()
 
1147
 
 
1148
    print "Groups"
 
1149
    for group in context.getGroups():
 
1150
        print str(group)
 
1151
        #print group._getGroupFileEntry()
 
1152
 
 
1153
    print "Saving"    
 
1154
    context.save()