diff options
author | Arthur de Jong <arthur@arthurdejong.org> | 2010-12-29 22:50:31 +0000 |
---|---|---|
committer | Arthur de Jong <arthur@arthurdejong.org> | 2010-12-29 22:50:31 +0000 |
commit | e985efa83458e1cc9c2bcb12e3cc10b6526c3399 (patch) | |
tree | 8311cb525c9d452d62d88280e6cca854496f9c42 /pynslcd | |
parent | 4e9224817ee303404b804a1a51f2f9c9a49164e4 (diff) | |
parent | ed6bc27721075adf0215ad8b856fcdcf7b98b9b7 (diff) |
merge changes from trunk
git-svn-id: http://arthurdejong.org/svn/nss-pam-ldapd/nss-pam-ldapd-solaris@1349 ef36b2f9-881f-0410-afb5-c4e39611909c
Diffstat (limited to 'pynslcd')
-rw-r--r-- | pynslcd/Makefile.am | 41 | ||||
-rw-r--r-- | pynslcd/alias.py | 74 | ||||
-rw-r--r-- | pynslcd/cfg.py | 58 | ||||
-rw-r--r-- | pynslcd/common.py | 125 | ||||
-rw-r--r-- | pynslcd/config.py.in | 61 | ||||
-rw-r--r-- | pynslcd/debugio.py | 65 | ||||
-rw-r--r-- | pynslcd/ether.py | 100 | ||||
-rw-r--r-- | pynslcd/group.py | 174 | ||||
-rw-r--r-- | pynslcd/mypidfile.py | 70 | ||||
-rw-r--r-- | pynslcd/pam.py | 129 | ||||
-rw-r--r-- | pynslcd/passwd.py | 163 | ||||
-rwxr-xr-x | pynslcd/pynslcd.py | 276 | ||||
-rw-r--r-- | pynslcd/shadow.py | 116 | ||||
-rw-r--r-- | pynslcd/tio.py | 98 |
14 files changed, 1550 insertions, 0 deletions
diff --git a/pynslcd/Makefile.am b/pynslcd/Makefile.am new file mode 100644 index 0000000..098cdfb --- /dev/null +++ b/pynslcd/Makefile.am @@ -0,0 +1,41 @@ +# Makefile.am - use automake to generate Makefile.in +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +pynslcddir = $(datadir)/pynslcd + +pynslcd_PYTHON = pynslcd.py cfg.py common.py tio.py \ + ether.py group.py passwd.py +nodist_pynslcd_PYTHON = constants.py config.py +CLEANFILES = $(nodist_pynslcd_PYTHON) + +all-local: $(nodist_pynslcd_PYTHON) + +# create a symbolic link for the pynslcd daemon and fix permissions +install-data-hook: + chmod a+rx $(DESTDIR)$(pynslcddir)/pynslcd.py + $(MKDIR_P) $(DESTDIR)$(sbindir) + [ -L $(DESTDIR)$(sbindir)/pynslcd ] || $(LN_S) $(pynslcddir)/pynslcd.py $(DESTDIR)$(sbindir)/pynslcd + +# generate constants module +constants.py: $(top_srcdir)/nslcd.h Makefile + ( echo "# This file is automatically generated from nslcd.h." ; \ + echo "# See that file for details." ; \ + echo "" ; \ + sed -n 's| */\*.*\*/ *||;s/^.define *\(NSLCD_[A-Z_]*\) */\1 = /p' \ + $< ) > $@ diff --git a/pynslcd/alias.py b/pynslcd/alias.py new file mode 100644 index 0000000..5126466 --- /dev/null +++ b/pynslcd/alias.py @@ -0,0 +1,74 @@ + +# alias.py - lookup functions for aliasnet addresses +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import constants +import common + +import ldap +import ldap.filter + + +class AliasRequest(common.Request): + + filter = '(objectClass=nisMailAlias)' + + attmap_cn = 'cn' + attmap_rfc822MailMember = 'rfc822MailMember' + + attributes = ( 'cn', 'rfc822MailMember' ) + + def write(self, entry): + dn, attributes = entry + # get name and check against requested name + names = attributes.get(self.attmap_cn, []) + if not names: + logging.error('Error: entry %s does not contain %s value', dn, self.attmap_cn) + return + if self.name: + if self.name.lower() not in (x.lower() for x in names): + return + names = ( self.name, ) + # get the members of the alias + members = attributes.get(self.attmap_rfc822MailMember, []) + if not members: + logging.error('Error: entry %s does not contain %s value', dn, self.attmap_rfc822MailMember) + return + # write results + for name in names: + self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) + self.fp.write_string(name) + self.fp.write_stringlist(members) + + +class AliasByNameRequest(AliasRequest): + + action = constants.NSLCD_ACTION_ALIAS_BYNAME + + def read_parameters(self): + self.name = self.fp.read_string() + + def mk_filter(self): + return '(&%s(%s=%s))' % ( self.filter, + self.attmap_cn, ldap.filter.escape_filter_chars(self.name) ) + + +class AliasAllRequest(AliasRequest): + + action = constants.NSLCD_ACTION_ALIAS_ALL diff --git a/pynslcd/cfg.py b/pynslcd/cfg.py new file mode 100644 index 0000000..000e601 --- /dev/null +++ b/pynslcd/cfg.py @@ -0,0 +1,58 @@ + +# cfg.py - module for accessing configuration information +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import ldap + +# these values are defined here + +# the name of the program +program_name = 'pynslcd' +# the debugging level +debug = 0 +# whether the --check option was passed +check = False +# the number of threads to start +threads = 5 + +# the user id nslcd should be run as +uid = None +# the group id nslcd should be run as +gid = None + +# the LDAP server to use +# FIXME: support multiple servers and have a fail-over mechanism +ldap_uri = 'ldapi:///' + +# default search scope for searches +scope = ldap.SCOPE_SUBTREE + +# LDAP search bases to search +bases = ( 'dc=test, dc=tld', ) + +# the users for which no initgroups() searches should be done +nss_initgroups_ignoreusers = [] + +# the DN to use to perform password modifications as root +rootpwmoddn = 'cn=admin, dc=test, dc=tld' +rootpwmodpw = 'test' + +# FIXME: implement reading configuration from file +def read(cfgfile): + pass diff --git a/pynslcd/common.py b/pynslcd/common.py new file mode 100644 index 0000000..fd3196a --- /dev/null +++ b/pynslcd/common.py @@ -0,0 +1,125 @@ + +# common.py - functions that are used by different modules +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import cfg +import constants + +import re +import ldap +import ldap.dn + +_validname_re = re.compile(r'^[A-Za-z0-9._@$][A-Za-z0-9._@$ \\~-]{0,98}[A-Za-z0-9._@$~-]$') + +def isvalidname(name): + """Checks to see if the specified name seems to be a valid user or group + name. + + This test is based on the definition from POSIX (IEEE Std 1003.1, 2004, + 3.426 User Name, 3.189 Group Name and 3.276 Portable Filename Character Set): + http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_426 + http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_189 + http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_276 + + The standard defines user names valid if they contain characters from + the set [A-Za-z0-9._-] where the hyphen should not be used as first + character. As an extension this test allows some more characters.""" + return bool(_validname_re.match(name)) + +def validate_name(name): + """Checks to see if the specified name seems to be a valid user or group + name. See isvalidname().""" + if not _validname_re.match(name): + raise ValueError('%r: invalid user name' % name) + + +class Request(object): + """ + Request handler class. Subclasses are expected to handle actual requests + and should implement the following members: + + action: the NSLCD_ACTION_* action that should trigger this handler + read_parameters: a function that reads the request parameters of the + request stream + filter: LDAP search filter + mk_filter (optional): function that returns the LDAP search filter + write: function that writes a single LDAP entry to the result stream + """ + + bases = cfg.bases + scope = cfg.scope + + def __init__(self, fp, conn, calleruid): + self.fp = fp + self.conn = conn + self.calleruid = calleruid + # have default empty values for these + self.name = None + self.uid = None + self.gid = None + self.address = None + + def read_parameters(self): + """This method should read the parameters from ths stream and + store them in self.""" + pass + + def mk_filter(self): + """Return the active search filter (based on the read parameters).""" + return self.filter + + def handle_request(self): + """This method handles the request based on the parameters read + with read_parameters().""" + # get search results + for base in self.bases: + # do the LDAP search + try: + res = self.conn.search_s(base, self.scope, self.mk_filter(), self.attributes) + for entry in res: + if entry[0]: + self.write(entry) + except ldap.NO_SUCH_OBJECT: + # FIXME: log message + pass + # write the final result code + self.fp.write_int32(constants.NSLCD_RESULT_END) + + def __call__(self): + self.read_parameters() + # TODO: log call with parameters + self.fp.write_int32(constants.NSLCD_VERSION) + self.fp.write_int32(self.action) + self.handle_request() + + +def get_handlers(module): + """Return a dictionary mapping actions to Request classes.""" + import inspect + res = {} + if isinstance(module, basestring): + module = __import__(module, globals()) + for name, cls in inspect.getmembers(module, inspect.isclass): + if issubclass(cls, Request) and hasattr(cls, 'action'): + res[cls.action] = cls + return res + +def get_rdn_value(entry, attribute): + dn, attributes = entry + return dict((x, y) for x, y, z in ldap.dn.str2dn(dn)[0])[attribute] diff --git a/pynslcd/config.py.in b/pynslcd/config.py.in new file mode 100644 index 0000000..bab172e --- /dev/null +++ b/pynslcd/config.py.in @@ -0,0 +1,61 @@ + +# config.py.in - configured information, this file is processed by the +# configure script to produce config.py +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + + +# Name of package +PACKAGE = '''@PACKAGE@''' + +# Define to the address where bug reports for this package should be sent. +PACKAGE_BUGREPORT = '''@PACKAGE_BUGREPORT@''' + +# Define to the full name of this package. +PACKAGE_NAME = '''@PACKAGE_NAME@''' + +# Define to the full name and version of this package. +PACKAGE_STRING = '''@PACKAGE_STRING@''' + +# Define to the one symbol short name of this package. +PACKAGE_TARNAME = '''@PACKAGE_TARNAME@''' + +# Define to the home page for this package. +PACKAGE_URL = '''@PACKAGE_URL@''' + +# Define to the version of this package. +PACKAGE_VERSION = '''@PACKAGE_VERSION@''' + +# Version number of package +VERSION = '''@VERSION@''' + +# Whether to check configfile options. +ENABLE_CONFIGFILE_CHECKING = '''@ENABLE_CONFIGFILE_CHECKING@''' + +# Path to bindpw value. +NSLCD_BINDPW_PATH = '''@NSLCD_BINDPW_PATH@''' + +# Path to nslcd configuration file. +NSLCD_CONF_PATH = '''@NSLCD_CONF_PATH@''' + +# The location of the pidfile used for checking availability of the nslcd. +NSLCD_PIDFILE = '''@NSLCD_PIDFILE@''' + +# The location of the socket used for communicating. +NSLCD_SOCKET = '''@NSLCD_SOCKET@''' + +# The SONAME of the NSS library module. +NSS_LDAP_SONAME = '''@NSS_LDAP_SONAME@''' diff --git a/pynslcd/debugio.py b/pynslcd/debugio.py new file mode 100644 index 0000000..dbb5b02 --- /dev/null +++ b/pynslcd/debugio.py @@ -0,0 +1,65 @@ + +# debugio.py - module for debugging an I/O stream +# +# Copyright (C) 2008, 2009 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +class DebugIO(): + """This class is a file-like object that writes from one file and + writes to another. It is mainly used for debugging the serial protocol + without a serial connection.""" + + def __init__(self, name): + import os + if not os.path.exists(name+'.in'): os.mkfifo(name+'.in') + if not os.path.exists(name+'.out'): os.mkfifo(name+'.out') + r = open(name+'.in', 'r', 0) + w = open(name+'.out', 'w', 0) + self._r = r + self._w = w + self.write = w.write + self.portstr = 'debuging to %s.in and %s.out' % ( name, name ) + self._timeout = None + + def close(self): + self._r.close() + self._w.close() + + def inWaiting(self): + # we are never out of data and 100 should be enough for everybody + return 100 + + def setTimeout(self, seconds): + self._timeout = seconds + + def getTimeout(self): + return self._timeout + + def read(self, size): + import select + read = '' + if size > 0: + while len(read) < size: + ready, _, _ = select.select([self._r.fileno()], [], [], self._timeout) + if not ready: + break #timeout + buf = self._r.read(size-len(read)) + read = read + buf + if self._timeout >= 0 and not buf: + break #early abort on timeout + return read + diff --git a/pynslcd/ether.py b/pynslcd/ether.py new file mode 100644 index 0000000..0995377 --- /dev/null +++ b/pynslcd/ether.py @@ -0,0 +1,100 @@ + +# ether.py - lookup functions for ethernet addresses +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import constants +import common + +import struct +import ldap.filter + + +def ether_aton(ether): + return struct.pack('BBBBBB', *(int(x, 16) for x in ether.split(':'))) + +def ether_ntoa(ether): + return ':'.join('%x' % x for x in struct.unpack('6B', ether)) + + +class EtherRequest(common.Request): + + filter = '(objectClass=ieee802Device)' + + attmap_cn = 'cn' + attmap_macAddress = 'macAddress' + + attributes = ( 'cn', 'macAddress' ) + + def __init__(self, *args): + super(EtherRequest, self).__init__(*args) + self.ether = None + + def write(self, entry): + dn, attributes = entry + # get name and check against requested user name + names = attributes.get(self.attmap_cn, []) + if not names: + print 'Error: entry %s does not contain %s value' % ( dn, self.attmap_cn) + if self.name: + if self.name.lower() not in (x.lower() for x in names): + return # skip entry + names = ( self.name, ) + # get addresses and convert to binary form + addresses = [ether_aton(x) for x in attributes.get(self.attmap_macAddress, [])] + if not addresses: + print 'Error: entry %s does not contain %s value' % ( dn, self.attmap_macAddress) + if self.ether: + if self.ether not in addresses: + return + addresses = ( self.ether, ) + # write results + for name in names: + for ether in addresses: + self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) + self.fp.write_string(name) + self.fp.write(ether) + + +class EtherByNameRequest(EtherRequest): + + action = constants.NSLCD_ACTION_ETHER_BYNAME + + def read_parameters(self): + self.name = self.fp.read_string() + + def mk_filter(self): + return '(&%s(%s=%s))' % ( self.filter, + self.attmap_cn, ldap.filter.escape_filter_chars(self.name) ) + + +class EtherByEtherRequest(EtherRequest): + + action = constants.NSLCD_ACTION_ETHER_BYETHER + + def read_parameters(self): + self.ether = self.fp.read(6) + + def mk_filter(self): + return '(&%s(%s=%s))' % ( self.filter, + self.attmap_macAddress, ether_ntoa(self.ether) ) + + +class EtherAllRequest(EtherRequest): + + action = constants.NSLCD_ACTION_ETHER_ALL diff --git a/pynslcd/group.py b/pynslcd/group.py new file mode 100644 index 0000000..4f85441 --- /dev/null +++ b/pynslcd/group.py @@ -0,0 +1,174 @@ + +# group.py - group entry lookup routines +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import constants +import common +import cfg + +import logging +import ldap +import ldap.filter + + +def clean(lst): + for i in lst: + yield i.replace('\0', '') + +class GroupRequest(common.Request): + + filter = '(|(objectClass=posixGroup)(objectClass=groupOfUniqueNames))' + + attmap_group_cn = 'cn' + attmap_group_userPassword = 'userPassword' + attmap_group_gidNumber = 'gidNumber' + attmap_group_memberUid = 'memberUid' + attmap_group_uniqueMember = 'uniqueMember' + + attributes = ( 'cn', 'userPassword', 'gidNumber', 'memberUid', + 'uniqueMember' ) + + wantmembers = True + + def write(self, entry): + dn, attributes = entry + # get uid attribute and check against requested user name + names = attributes.get('uid', []) + if self.name: + if self.name not in names: + return + names = ( self.name, ) + # get user password entry + passwd = '*' + # get numeric user and group ids + uids = ( self.uid, ) if self.uid else attributes.get(self.attmap_group_uidNumber, []) + uids = [ int(x) for x in uids ] + ( gid, ) = attributes[self.attmap_group_gidNumber] + gid = int(gid) + # FIXME: use expression here + gecos = attributes.get(self.attmap_group_gecos, [None])[0] or attributes.get(self.attmap_group_cn, [''])[0] + ( home, ) = attributes.get(self.attmap_group_homeDirectory, ['']) + ( shell, ) = attributes.get(self.attmap_group_loginShell, ['']) + for name in names: + if not common.isvalidname(name): + print 'Warning: group entry %s contains invalid user name: "%s"' % ( dn, name ) + else: + for uid in uids: + self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) + self.fp.write_string(name) + self.fp.write_string(passwd) + self.fp.write_uid_t(uid) + self.fp.write_gid_t(gid) + self.fp.write_string(gecos) + self.fp.write_string(home) + self.fp.write_string(shell) + + def write(self, entry): + dn, attributes = entry + # get group names and check against requested group name + names = attributes.get(self.attmap_group_cn, []) + if self.name: + if self.name not in names: + return + names = ( self.name, ) + # get group group password + ( passwd, ) = attributes.get(self.attmap_group_userPassword, ['*']) + # get group id(s) + gids = ( self.gid, ) if self.gid else attributes.get(self.attmap_group_gidNumber, []) + gids = [ int(x) for x in gids ] + # build member list + members = set() + if self.wantmembers: + # add the memberUid values + for member in clean(attributes.get(self.attmap_group_memberUid, [])): + #print 'found member %r' % member + if common.isvalidname(member): + members.add(member) + # translate and add the uniqueMember values + from passwd import dn2uid + for memberdn in clean(attributes.get(self.attmap_group_uniqueMember, [])): + member = dn2uid(self.conn, memberdn) + #print 'found memberdn %r, member=%r' % ( memberdn, member) + if member: + members.add(member) + # actually return the results + for name in names: + if not common.isvalidname(name): + print 'Warning: group entry %s contains invalid group name: "%s"' % ( dn, name ) + else: + for gid in gids: + self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) + self.fp.write_string(name) + self.fp.write_string(passwd) + self.fp.write_gid_t(gid) + self.fp.write_stringlist(members) + + +class GroupByNameRequest(GroupRequest): + + action = constants.NSLCD_ACTION_GROUP_BYNAME + + def read_parameters(self): + self.name = self.fp.read_string() + common.validate_name(self.name) + + def mk_filter(self): + return '(&%s(%s=%s))' % ( self.filter, + self.attmap_group_cn, ldap.filter.escape_filter_chars(self.name) ) + + +class GroupByGidRequest(GroupRequest): + + action = constants.NSLCD_ACTION_GROUP_BYGID + + def read_parameters(self): + self.gid = self.fp.read_gid_t() + + def mk_filter(self): + return '(&%s(%s=%d))' % ( self.filter, + self.attmap_group_gidNumber, self.gid ) + + +class GroupByMemberRequest(GroupRequest): + + action = constants.NSLCD_ACTION_GROUP_BYMEMBER + wantmembers = False + attributes = ( 'cn', 'userPassword', 'gidNumber' ) + + def read_parameters(self): + self.memberuid = self.fp.read_string() + common.validate_name(self.memberuid) + + def mk_filter(self): + # try to translate uid to DN + # TODO: only do this if memberuid attribute is mapped + import passwd + dn = passwd.uid2dn(self.conn, self.memberuid) + if dn: + return '(&%s(|(%s=%s)(%s=%s)))' % ( self.filter, + self.attmap_group_memberUid, ldap.filter.escape_filter_chars(self.memberuid), + self.attmap_group_uniqueMember, ldap.filter.escape_filter_chars(dn) ) + else: + return '(&%s(%s=%s))' % ( self.filter, + self.attmap_group_memberUid, ldap.filter.escape_filter_chars(self.memberuid) ) + + +class GroupAllRequest(GroupRequest): + + action = constants.NSLCD_ACTION_GROUP_ALL diff --git a/pynslcd/mypidfile.py b/pynslcd/mypidfile.py new file mode 100644 index 0000000..4781089 --- /dev/null +++ b/pynslcd/mypidfile.py @@ -0,0 +1,70 @@ + +# mypidfile.py - functions for properly locking a PIDFile +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import fcntl +import errno +import os + + +class MyPIDLockFile(object): + """Implementation of a PIDFile fit for use with the daemon module + that locks the PIDFile with fcntl.lockf().""" + + def __init__(self, path): + self.path = path + + def __enter__(self): + """Lock the PID file and write the process ID to the file.""" + fd = os.open(self.path, os.O_RDWR | os.O_CREAT, 0644) + try: + fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + pidfile = os.fdopen(fd, 'w') + except: + os.close(fd) + raise + pidfile.write('%d\n' % os.getpid()) + pidfile.flush() + self.pidfile = pidfile + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Release the lock (close the lockfile).""" + fcntl.lockf(self.pidfile.fileno(), fcntl.LOCK_UN) + self.pidfile.close() + del self.pidfile + + def is_locked(self): + """Check whether the file is already present and locked.""" + try: + fd = os.open(self.path, os.O_RDWR, 0644) + # Python doesn't seem to have F_TEST so we'll just try to lock + fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + # if we're here we must have aquired the lock + fcntl.lockf(fd, fcntl.LOCK_UN) + return False + except (IOError, OSError), e: + if e.errno == errno.ENOENT: + return False + if e.errno in (errno.EACCES, errno.EAGAIN): + return True + raise + finally: + if 'fd' in locals(): + os.close(fd) diff --git a/pynslcd/pam.py b/pynslcd/pam.py new file mode 100644 index 0000000..852830c --- /dev/null +++ b/pynslcd/pam.py @@ -0,0 +1,129 @@ + +# pam.py - functions authentication, authorisation and session handling +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import constants +import common +import cfg + +import logging +import ldap + +import passwd + +def try_bind(userdn, password): + # open a new connection + conn = ldap.initialize(cfg.ldap_uri) + # bind using the specified credentials + conn.simple_bind_s(userdn, password) + # perform search for own object (just to do any kind of search) + res = conn.search_s(userdn, ldap.SCOPE_BASE, '(objectClass=*)', [ 'dn', ]) + for entry in res: + if entry[0] == userdn: + return + raise ldap.NO_SUCH_OBJECT() + + +class PAMRequest(common.Request): + + def validate_request(self): + """This method checks the provided username for validity and fills + in the DN if needed.""" + from passwd import PasswdRequest + # check username for validity + common.validate_name(self.username) + # look up user DN if not known + if not self.userdn: + entry = passwd.uid2entry(self.conn, self.username) + if not entry: + raise ValueError('%r: user not found' % self.username) + # save the DN + self.userdn = entry[0] + # get the "real" username + value = common.get_rdn_value(entry, PasswdRequest.attmap_passwd_uid) + if not value: + # get the username from the uid attribute + values = myldap_get_values(entry, PasswdRequest.attmap_passwd_uid) + if not values or not values[0]: + logging.warn('%s: is missing a %s attribute', entry.dn, PasswdRequest.attmap_passwd_uid) + value = values[0] + # check the username + if value and not common.isvalidname(value): + raise ValueError('%s: has invalid %s attribute', entry.dn, PasswdRequest.attmap_passwd_uid) + # check if the username is different and update it if needed + if value != self.username: + logging.info('username changed from %r to %r', self.username, value) + self.username = value + + +class PAMAuthenticationRequest(PAMRequest): + + action = constants.NSLCD_ACTION_PAM_AUTHC + + def read_parameters(self): + self.username = self.fp.read_string() + self.userdn = self.fp.read_string() + self.servicename = self.fp.read_string() + self.password = self.fp.read_string() + #self.validate_request() + # TODO: log call with parameters + + def write(self, code=constants.NSLCD_PAM_SUCCESS, msg=''): + self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) + self.fp.write_string(self.username) + self.fp.write_string(self.userdn) + self.fp.write_int32(code) # authc + self.fp.write_int32(constants.NSLCD_PAM_SUCCESS) # authz + self.fp.write_string(msg) # authzmsg + self.fp.write_int32(constants.NSLCD_RESULT_END) + + def handle_request(self): + # if the username is blank and rootpwmoddn is configured, try to + # authenticate as administrator, otherwise validate request as usual + if not self.username and cfg.ldc_rootpwmoddn: + # authenticate as rootpwmoddn + self.userdn = cfg.ldc_rootpwmoddn + # if the caller is root we will allow the use of rootpwmodpw + if not self.password and self.calleruid == 0 and cfg.rootpwmodpw: + self.password = cfg.rootpwmodpw + else: + self.validate_request() + # try authentication + try: + try_bind(self.userdn, self.password) + logging.debug('bind successful') + self.write() + except ldap.INVALID_CREDENTIALS, e: + try: + msg = e[0]['desc'] + except: + msg = str(e) + logging.debug('bind failed: %s', msg) + self.write(constants.NSLCD_PAM_AUTH_ERR, msg) + +#class PAMAuthorisationRequest(PAMRequest): + +# action = constants.NSLCD_ACTION_PAM_AUTHZ + +# def handle_request(self): + + +#NSLCD_ACTION_PAM_SESS_O +#NSLCD_ACTION_PAM_SESS_C +#NSLCD_ACTION_PAM_PWMOD diff --git a/pynslcd/passwd.py b/pynslcd/passwd.py new file mode 100644 index 0000000..80b2a4a --- /dev/null +++ b/pynslcd/passwd.py @@ -0,0 +1,163 @@ + +# passwd.py - lookup functions for user account information +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import constants +import common +import cfg + +import logging +import ldap +import ldap.filter + + +class PasswdRequest(common.Request): + + attmap = { 'uid': 'uid', 'userPassword': 'userPassword', + 'uidNumber': 'uidNumber', 'gidNumber': 'gidNumber', + 'gecos': '"${gecos:-$cn}"', 'cn': 'cn', + 'homeDirectory': 'homeDirectory', + 'loginShell': 'loginShell', + 'objectClass': 'objectClass' } + filter = '(objectClass=posixAccount)' + + attmap_passwd_uid = 'uid' + attmap_passwd_userPassword = 'userPassword' + attmap_passwd_uidNumber = 'uidNumber' + attmap_passwd_gidNumber = 'gidNumber' + attmap_passwd_gecos = '"${gecos:-$cn}"' + attmap_passwd_homeDirectory = 'homeDirectory' + attmap_passwd_loginShell = 'loginShell' + + # these should be removed + attmap_passwd_cn = 'cn' + + attributes = ( 'uid', 'userPassword', 'uidNumber', 'gidNumber', + 'gecos', 'cn', 'homeDirectory', 'loginShell', + 'objectClass' ) + + bases = ( 'ou=people,dc=test,dc=tld', ) + + def write(self, entry): + dn, attributes = entry + # get uid attribute and check against requested user name + names = attributes.get('uid', []) + if self.name: + if self.name not in names: + return + names = ( self.name, ) + # get user password entry + if 'shadowAccount' in attributes.get('objectClass', []): + passwd = 'x' + else: + passwd = '*'; + # get numeric user and group ids + uids = ( self.uid, ) if self.uid else attributes.get(self.attmap_passwd_uidNumber, []) + uids = [ int(x) for x in uids ] + ( gid, ) = attributes[self.attmap_passwd_gidNumber] + gid = int(gid) + # FIXME: use expression here + gecos = attributes.get(self.attmap_passwd_gecos, [None])[0] or attributes.get(self.attmap_passwd_cn, [''])[0] + ( home, ) = attributes.get(self.attmap_passwd_homeDirectory, ['']) + ( shell, ) = attributes.get(self.attmap_passwd_loginShell, ['']) + for name in names: + if not common.isvalidname(name): + print 'Warning: passwd entry %s contains invalid user name: "%s"' % ( dn, name ) + else: + for uid in uids: + #print '%s:%s:%d:%d:%s:%s:%s' % ( name, passwd, uid, gid, gecos, home, shell ) + self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) + self.fp.write_string(name) + self.fp.write_string(passwd) + self.fp.write_uid_t(uid) + self.fp.write_gid_t(gid) + self.fp.write_string(gecos) + self.fp.write_string(home) + self.fp.write_string(shell) + + +class PasswdByNameRequest(PasswdRequest): + + action = constants.NSLCD_ACTION_PASSWD_BYNAME + + def read_parameters(self): + self.name = self.fp.read_string() + common.validate_name(self.name) + + def mk_filter(self): + return '(&%s(%s=%s))' % ( self.filter, + self.attmap_passwd_uid, ldap.filter.escape_filter_chars(self.name) ) + + +class PasswdByUidRequest(PasswdRequest): + + action = constants.NSLCD_ACTION_PASSWD_BYUID + + def read_parameters(self): + self.uid = self.fp.read_uid_t() + + def mk_filter(self): + return '(&%s(%s=%d))' % ( self.filter, + self.attmap_passwd_uidNumber, self.uid ) + + +class PasswdAllRequest(PasswdRequest): + + action = constants.NSLCD_ACTION_PASSWD_ALL + + +def do_search(conn, filter=None, base=None): + mybases = ( base, ) if base else PasswdRequest.bases + filter = filter or PasswdRequest.filter + # perform a search for each search base + for base in mybases: + # do the LDAP search + try: + res = conn.search_s(base, PasswdRequest.scope, filter, [PasswdRequest.attmap_passwd_uid]) + for entry in res: + if entry[0]: + yield entry + except ldap.NO_SUCH_OBJECT: + # FIXME: log message + pass + +def uid2entry(conn, uid): + """Look up the user by uid and return the LDAP entry or None if the user + was not found.""" + myfilter = '(&%s(%s=%s))' % ( PasswdRequest.filter, + PasswdRequest.attmap_passwd_uid, ldap.filter.escape_filter_chars(uid) ) + for dn, attributes in do_search(conn, myfilter): + if uid in attributes[PasswdRequest.attmap_passwd_uid]: + return dn, attributes + +def uid2dn(conn, uid): + """Look up the user by uid and return the DN or None if the user was + not found.""" + x = uid2entry(conn, uid) + if x is not None: + return x[0] + +def dn2uid(conn, dn): + """Look up the user by dn and return a uid or None if the user was + not found.""" + try: + for dn, attributes in do_search(conn, base=dn): + return attributes[PasswdRequest.attmap_passwd_uid][0] + except ldap.NO_SUCH_OBJECT: + return None diff --git a/pynslcd/pynslcd.py b/pynslcd/pynslcd.py new file mode 100755 index 0000000..4644a35 --- /dev/null +++ b/pynslcd/pynslcd.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python + +# pynslcd.py - main daemon module +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import os +import sys +import daemon +import mypidfile +import threading +import logging +import logging.handlers +import signal +import ldap + +import constants # from nslcd.h +import config # from configure +import cfg # from nslcd.conf +import common + +from tio import TIOStream + + +# configure logging +class MyFormatter(logging.Formatter): + def format(self, record): + msg = logging.Formatter.format(self, record) + if record.levelno == logging.DEBUG: + msg = 'DEBUG: %s' % msg + return msg +#logging.basicConfig(level=logging.INFO) +# , format='%(message)s' +formatter = MyFormatter('%(message)s') +stderrhandler = logging.StreamHandler(sys.stderr) +stderrhandler.setFormatter(formatter) +##sysloghandler = logging.handlers.SysLogHandler(address='/dev/log') +##sysloghandler.setFormatter(formatter) +#logging.getLogger().setFormatter(MyFormatter()) +logging.getLogger().addHandler(stderrhandler) + +#logger = logging.getLogger() +#logger.setLevel(logging.INFO) +#syslog = logging.handlers.SysLogHandler(address='/dev/log') +#formatter = logging.Formatter('%(name)s: %(levelname)s %(message)s') +#syslog.setFormatter(formatter) +#logger.addHandler(syslog) + +def display_version(fp): + fp.write('%(PACKAGE_STRING)s\n' + 'Written by Arthur de Jong.\n' + '\n' + 'Copyright (C) 2010 Arthur de Jong\n' + 'This is free software; see the source for copying conditions. There is NO\n' + 'warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n' + % { 'PACKAGE_STRING': config.PACKAGE_STRING, } ); + +def display_usage(fp): + fp.write("Usage: %(program_name)s [OPTION]...\n" + "Name Service LDAP connection daemon.\n" + " -c, --check check if the daemon already is running\n" + " -d, --debug don't fork and print debugging to stderr\n" + " --help display this help and exit\n" + " --version output version information and exit\n" + "\n" + "Report bugs to <%(PACKAGE_BUGREPORT)s>.\n" + % { 'program_name': cfg.program_name, + 'PACKAGE_BUGREPORT': config.PACKAGE_BUGREPORT, } ) + +def parse_cmdline(): + """Parse command-line arguments.""" + import getopt + cfg.program_name = sys.argv[0] or 'pynslcd' + try: + optlist, args = getopt.gnu_getopt(sys.argv[1:], + 'cdhV', ('check', 'debug', 'help', 'version', )) + for flag, arg in optlist: + if flag in ('-c', '--check'): + cfg.check = True + elif flag in ('-d', '--debug'): + cfg.debug += 1 + elif flag in ('-h', '--help'): + display_usage(sys.stdout) + sys.exit(0) + elif flag in ('-V', '--version'): + display_version(sys.stdout) + sys.exit(0) + if len(args): + raise getopt.GetoptError('unrecognized option \'%s\'' % args[0], args[0]) + except getopt.GetoptError, reason: + sys.stderr.write("%(program_name)s: %(reason)s\n" + "Try '%(program_name)s --help' for more information.\n" + % { 'program_name': cfg.program_name, + 'reason': reason, }) + sys.exit(1) + +def create_socket(): + """Returns a socket ready to answer requests from the client.""" + import socket + import fcntl + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + # remove existing named socket + try: + os.unlink(config.NSLCD_SOCKET) + except OSError: + pass # ignore any problems + # bind to named socket + sock.bind((config.NSLCD_SOCKET)) + # close the file descriptor on exit + fcntl.fcntl(sock, fcntl.F_SETFD, fcntl.FD_CLOEXEC) + # set permissions of socket so anybody can do requests + os.chmod(config.NSLCD_SOCKET, 0666) + # start listening for connections + sock.listen(socket.SOMAXCONN) + return sock + +def log_newsession(): + pass + # FIXME: implement + +def getpeercred(fd): + return (None, None, None) + # FIXME: implement and return uid, gid, pid + +handlers = {} +handlers.update(common.get_handlers('alias')) +handlers.update(common.get_handlers('ether')) +handlers.update(common.get_handlers('group')) +handlers.update(common.get_handlers('pam')) +handlers.update(common.get_handlers('passwd')) +handlers.update(common.get_handlers('shadow')) + +def acceptconnection(session): + # accept a new connection + conn, addr = nslcd_serversocket.accept() + # See: http://docs.python.org/library/socket.html#socket.socket.settimeout + fp = None + try: + # probably use finally + # indicate new connection to logging module (genrates unique id) + log_newsession() + # log connection + try: + uid, gid, pid = getpeercred(conn) + logging.debug('connection from pid=%r uid=%r gid=%r', pid, uid, gid) + except: + raise # FIXME: handle exception gracefully + # create a stream object + fp = TIOStream(conn) + # read request + version = fp.read_int32() + if version != constants.NSLCD_VERSION: + logging.debug('wrong nslcd version id (%r)', version) + return + action = fp.read_int32() + try: + handler = handlers[action] + except KeyError: + logging.warn('invalid action id: %r', action) + return + handler(fp, session, uid)() + finally: + if fp: + fp.close() + +def disable_nss_ldap(): + """Disable the nss_ldap module to avoid lookup loops.""" + import ctypes + lib = ctypes.CDLL(config.NSS_LDAP_SONAME) + ctypes.c_int.in_dll(lib, '_nss_ldap_enablelookups').value = 0 + +def worker(): + # create a new LDAP session + #session = myldap_create_session() + session = ldap.initialize(cfg.ldap_uri) + # start waiting for incoming connections + while True: + # wait for a new connection + acceptconnection(session) + # FIXME: handle exceptions + +if __name__ == '__main__': + # parse options + parse_cmdline() + # clean the environment + os.environ.clear() + os.putenv('HOME', '/') + os.putenv('TMPDIR', '/tmp') + os.putenv('LDAPNOINIT', '1') + # disable ldap lookups of host names to avoid lookup loop + disable_nss_ldap() + # set log level + if cfg.debug: + logging.getLogger().setLevel(logging.DEBUG) + # FIXME: implement + #if myldap_set_debuglevel(cfg.debug) != LDAP_SUCCESS: + # sys.exit(1) + # read configuration file + cfg.read(config.NSLCD_CONF_PATH) + # set a default umask for the pidfile and socket + os.umask(0022) + # see if someone already locked the pidfile + pidfile = mypidfile.MyPIDLockFile(config.NSLCD_PIDFILE) + # see if --check option was given + if cfg.check: + if pidfile.is_locked(): + logging.debug('pidfile (%s) is locked', config.NSLCD_PIDFILE) + sys.exit(0) + else: + logging.debug('pidfile (%s) is not locked', config.NSLCD_PIDFILE) + sys.exit(1) + # normal check for pidfile locked + if pidfile.is_locked(): + logging.error('daemon may already be active, cannot acquire lock (%s)', config.NSLCD_PIDFILE) + sys.exit(1) + # daemonize + if cfg.debug: + daemon = pidfile + else: + daemon = daemon.DaemonContext( + pidfile=pidfile, + signal_map={ + signal.SIGTERM: 'terminate', + signal.SIGINT: 'terminate', + signal.SIGPIPE: None, + }) + # start daemon + with daemon: + # start normal logging + if not cfg.debug: + log_startlogging(); + logging.info('version %s starting', config.VERSION) + # create socket + nslcd_serversocket = create_socket(); + # drop all supplemental groups + try: + os.setgroups(()) + except OSError, e: + logging.warn('cannot setgroups(()) (ignored): %s', e) + # change to nslcd gid + if cfg.gid is not None: + import grp + os.setgid(grp.getgrnam(cfg.gid).gr_gid) + # change to nslcd uid + if cfg.uid is not None: + import pwd + u = pwd.getpwnam(cfg.uid) + os.setuid(u.pw_uid) + os.environ['HOME'] = u.pw_dir + logging.info('accepting connections') + # start worker threads + threads = [] + for i in range(cfg.threads): + thread = threading.Thread(target=worker, name='thread%d' % i) + thread.setDaemon(True) + thread.start() + logging.debug('started thread %s' % thread.getName()) + threads.append(thread) + # wait for all threads to die + for thread in threads: + thread.join(10000) diff --git a/pynslcd/shadow.py b/pynslcd/shadow.py new file mode 100644 index 0000000..3f2a5d7 --- /dev/null +++ b/pynslcd/shadow.py @@ -0,0 +1,116 @@ + +# shadow.py - lookup functions for shadownet addresses +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import constants +import common + +import ldap.filter + + +class ShadowRequest(common.Request): + + filter = '(objectClass=shadowAccount)' + + attmap_uid = 'uid' + attmap_userPassword = 'userPassword' + attmap_shadowLastChange = 'shadowLastChange' + attmap_shadowMin = 'shadowMin' + attmap_shadowMax = 'shadowMax' + attmap_shadowWarning = 'shadowWarning' + attmap_shadowInactive = 'shadowInactive' + attmap_shadowExpire = 'shadowExpire' + attmap_shadowFlag = 'shadowFlag' + + attributes = ( 'uid', 'userPassword', 'shadowLastChange', 'shadowMin', + 'shadowMax', 'shadowWarning', 'shadowInactive', + 'shadowExpire', 'shadowFlag' ) + + bases = ( 'ou=people,dc=test,dc=tld', ) + + def write(self, entry): + dn, attributes = entry + # get name and check against requested name + names = attributes.get(self.attmap_uid, []) + if not names: + print 'Error: entry %s does not contain %s value' % ( dn, self.attmap_uid) + return + if self.name: + if self.name not in names: + return + names = ( self.name, ) + # get password + (passwd, ) = attributes.get(self.attmap_userPassword, ['x']) + if not passwd or self.calleruid != 0: + passwd = '*'; + # function for making an int + def mk_int(attr): + try: + return + except TypeError: + return None + # get lastchange date + lastchangedate = int(attributes.get(self.attmap_shadowLastChange, [-1])[0]) + # we expect an AD 64-bit datetime value; + # we should do date=date/864000000000-134774 + # but that causes problems on 32-bit platforms, + # first we devide by 1000000000 by stripping the + # last 9 digits from the string and going from there */ + if self.attmap_shadowLastChange == 'pwdLastSet': + lastchangedate = ( lastchangedate / 864000000000 ) - 134774 + # get longs + mindays = int(attributes.get(self.attmap_shadowMin, [-1])[0]) + maxdays = int(attributes.get(self.attmap_shadowMax, [-1])[0]) + warndays = int(attributes.get(self.attmap_shadowWarning, [-1])[0]) + inactdays = int(attributes.get(self.attmap_shadowInactive, [-1])[0]) + expiredate = int(attributes.get(self.attmap_shadowExpire, [-1])[0]) + flag = int(attributes.get(self.attmap_shadowFlag, [0])[0]) + if self.attmap_shadowFlag == 'pwdLastSet': + if flag & 0x10000: + maxdays = 99999 + flag = 0 + # write results + for name in names: + self.fp.write_int32(constants.NSLCD_RESULT_BEGIN) + self.fp.write_string(name) + self.fp.write_string(passwd) + self.fp.write_int32(lastchangedate) + self.fp.write_int32(mindays) + self.fp.write_int32(maxdays) + self.fp.write_int32(warndays) + self.fp.write_int32(inactdays) + self.fp.write_int32(expiredate) + self.fp.write_int32(flag) + + +class ShadowByNameRequest(ShadowRequest): + + action = constants.NSLCD_ACTION_SHADOW_BYNAME + + def read_parameters(self): + self.name = self.fp.read_string() + + def mk_filter(self): + return '(&%s(%s=%s))' % ( self.filter, + self.attmap_uid, ldap.filter.escape_filter_chars(self.name) ) + + +class ShadowAllRequest(ShadowRequest): + + action = constants.NSLCD_ACTION_SHADOW_ALL diff --git a/pynslcd/tio.py b/pynslcd/tio.py new file mode 100644 index 0000000..acba81f --- /dev/null +++ b/pynslcd/tio.py @@ -0,0 +1,98 @@ + +# tio.py - I/O functions +# +# Copyright (C) 2010 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import struct +import os +import socket +import errno + +# definition for reading and writing INT32 values +_int32 = struct.Struct('i') + +# FIXME: use something from config.py to determine the correct size +_uid_t = struct.Struct('i') + +# FIXME: use something from config.py to determine the correct size +_gid_t = struct.Struct('i') + +# FIXME: use something from config.py to determine the correct size +_struct_timeval = struct.Struct('ll') + +class TIOStreamError(Exception): + pass + +class TIOStream(object): + """File-like object that allows reading and writing nslcd-protocol + entities.""" + + def __init__(self, conn): + conn.setblocking(1) + conn.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, _struct_timeval.pack(0, 500000)) + conn.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, _struct_timeval.pack(60, 0)) + self.fp = os.fdopen(conn.fileno(), 'w+b', 1024*1024) + + def read(self, size): + return self.fp.read(size) + + def read_int32(self): + return _int32.unpack(self.read(_int32.size))[0] + + def read_uid_t(self): + return _uid_t.unpack(self.read(_uid_t.size))[0] + + def read_gid_t(self): + return _gid_t.unpack(self.read(_gid_t.size))[0] + + def read_string(self, maxsize=None): + len = self.read_int32() + if maxsize and len >= maxsize: + raise TIOStreamError() + return self.read(len) + + def write(self, value): + self.fp.write(value) + + def write_int32(self, value): + self.write(_int32.pack(value)) + + def write_uid_t(self, value): + self.write(_uid_t.pack(value)) + + def write_gid_t(self, value): + self.write(_gid_t.pack(value)) + + def write_string(self, value): + self.write_int32(len(value)) + self.write(value) + + def write_stringlist(self, value): + lst = tuple(value) + self.write_int32(len(lst)) + for string in lst: + self.write_string(string) + + def close(self): + try: + self.fp.close() + except IOError: + pass + + def __del__(self): + self.close() |