Arthur de Jong

Open Source / Free Software developer

summaryrefslogtreecommitdiffstats
path: root/pynslcd/cfg.py
diff options
context:
space:
mode:
Diffstat (limited to 'pynslcd/cfg.py')
-rw-r--r--pynslcd/cfg.py257
1 files changed, 246 insertions, 11 deletions
diff --git a/pynslcd/cfg.py b/pynslcd/cfg.py
index 8677679..42aa2fd 100644
--- a/pynslcd/cfg.py
+++ b/pynslcd/cfg.py
@@ -18,6 +18,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
+import re
+import sys
+
import ldap
@@ -30,22 +33,254 @@ gid = None
# the LDAP server to use
# FIXME: support multiple servers and have a fail-over mechanism
-ldap_uri = 'ldapi:///'
+uri = None
+# LDAP protocol version to use (perhaps fix at 3?)
+ldap_version = ldap.VERSION3
+# the DN to use when binding
+binddn = None
+bindpw = None
+# the DN to use to perform password modifications as root
+rootpwmoddn = None
+rootpwmodpw = None
+
+# SASL configuration
+sasl_mech = None
+sasl_realm = None
+sasl_authcid = None
+sasl_authzid = None
+sasl_secprops = None
+# LDAP bases to search
+bases = []
# default search scope for searches
scope = ldap.SCOPE_SUBTREE
-# LDAP search bases to search
-bases = ('dc=test, dc=tld', )
+deref = ldap.DEREF_NEVER
+referrals = True
-# the users for which no initgroups() searches should be done
-nss_initgroups_ignoreusers = []
+# timing configuration
+bind_timelimit = 10
+timelimit = ldap.NO_LIMIT
+idle_timelimit = 0
+reconnect_sleeptime = 1
+reconnect_retrytime = 10
-# the DN to use to perform password modifications as root
-rootpwmoddn = 'cn=admin, dc=test, dc=tld'
-rootpwmodpw = 'test'
+# SSL/TLS options
+ssl = None
+tls_reqcert = None
+tls_cacertdir = None
+tls_cacertfile = None
+tls_randfile = None
+tls_ciphers = None
+tls_cert = None
+tls_key = None
+
+
+# other options
+pagesize = 0
+nss_initgroups_ignoreusers = set()
+nss_min_uid = 0
+validnames = re.compile(r'^[a-z0-9._@$][a-z0-9._@$ \\~-]{0,98}[a-z0-9._@$~-]$', re.IGNORECASE)
+pam_authz_search = None
+
+
+# allowed boolean values
+_boolean_options = {'on': True, 'yes': True, 'true': True, '1': True,
+ 'off': False, 'no': False, 'false': False, '0': False}
+
+# allowed values for scope option
+_scope_options = dict(sub=ldap.SCOPE_SUBTREE, subtree=ldap.SCOPE_SUBTREE,
+ one=ldap.SCOPE_ONELEVEL, onelevel=ldap.SCOPE_ONELEVEL,
+ base=ldap.SCOPE_BASE)
+
+# allowed values for the deref option
+_deref_options = dict(never=ldap.DEREF_NEVER,
+ searching=ldap.DEREF_SEARCHING,
+ finding=ldap.DEREF_FINDING,
+ always=ldap.DEREF_ALWAYS)
+
+# allowed values for the ssl option
+_ssl_options = dict(start_tls='STARTTLS', starttls='STARTTLS',
+ on='LDAPS', off=None)
+
+# allowed values for the tls_reqcert option
+_tls_reqcert_options = {'never': ldap.OPT_X_TLS_NEVER,
+ 'no': ldap.OPT_X_TLS_NEVER,
+ 'allow': ldap.OPT_X_TLS_ALLOW,
+ 'try': ldap.OPT_X_TLS_TRY,
+ 'demand': ldap.OPT_X_TLS_DEMAND,
+ 'yes': ldap.OPT_X_TLS_DEMAND,
+ 'hard': ldap.OPT_X_TLS_HARD}
+
+
+def _get_maps():
+ # separate function as not to pollute the namespace and avoid import loops
+ import alias, ether, group, host, netgroup, network, passwd
+ import protocol, rpc, service, shadow
+ return dict(alias=alias, aliases=alias,
+ ether=ether, ethers=ether,
+ group=group,
+ host=host, hosts=host,
+ netgroup=netgroup,
+ network=network, networks=network,
+ passwd=passwd,
+ protocol=protocol, protocols=protocol,
+ rpc=rpc,
+ service=service, services=service,
+ shadow=shadow,
+ none=sys.modules[__name__])
+
+
+class ParseError(Exception):
+
+ def __init__(self, filename, lineno, message):
+ self.message = '%s:%d: %s' % (filename, lineno, message)
+
+ def __repr__(self):
+ return self.message
+
+ __str__ = __repr__
-# FIXME: implement reading configuration from file
-def read(cfgfile):
- pass
+def read(filename):
+ maps = _get_maps()
+ lineno = 0
+ for line in open(filename, 'r'):
+ lineno += 1
+ line = line.strip()
+ # skip comments and blank lines
+ if re.match('(#.*)?$', line, re.IGNORECASE):
+ continue
+ # parse options with a single integer argument
+ m = re.match('(?P<keyword>threads|ldap_version|bind_timelimit|timelimit|idle_timelimit|reconnect_sleeptime|reconnect_retrytime|pagesize|nss_min_uid)\s+(?P<value>\d+)',
+ line, re.IGNORECASE)
+ if m:
+ globals()[m.group('keyword').lower()] = int(m.group('value'))
+ continue
+ # parse options with a single boolean argument
+ m = re.match('(?P<keyword>referrals)\s+(?P<value>%s)' %
+ '|'.join(_boolean_options.keys()),
+ line, re.IGNORECASE)
+ if m:
+ globals()[m.group('keyword').lower()] = _boolean_options[m.group('value').lower()]
+ continue
+ # parse options with a single no-space value
+ m = re.match('(?P<keyword>uid|gid|bindpw|rootpwmodpw|sasl_mech)\s+(?P<value>\S+)',
+ line, re.IGNORECASE)
+ if m:
+ globals()[m.group('keyword').lower()] = m.group('value')
+ continue
+ # parse options with a single value that can contain spaces
+ m = re.match('(?P<keyword>binddn|rootpwmoddn|sasl_realm|sasl_authcid|sasl_authzid|sasl_secprops|krb5_ccname|tls_cacertdir|tls_cacertfile|tls_randfile|tls_ciphers|tls_cert|tls_key)\s+(?P<value>\S.*)', line, re.IGNORECASE)
+ if m:
+ globals()[m.group('keyword').lower()] = m.group('value')
+ continue
+ # uri <URI>
+ m = re.match('uri\s+(?P<uri>\S+)', line, re.IGNORECASE)
+ if m:
+ # FIXME: support multiple URI values
+ # FIXME: support special DNS and DNS:domain values
+ global uri
+ uri = m.group('uri')
+ continue
+ # base <MAP>? <BASEDN>
+ m = re.match('base\s+((?P<map>%s)\s+)?(?P<value>\S.*)' %
+ '|'.join(maps.keys()),
+ line, re.IGNORECASE)
+ if m:
+ mod = maps[str(m.group('map')).lower()]
+ if not hasattr(mod, 'bases'):
+ mod.bases = []
+ mod.bases.append(m.group('value'))
+ continue
+ # filter <MAP> <SEARCHFILTER>
+ m = re.match('filter\s+(?P<map>%s)\s+(?P<value>\S.*)' %
+ '|'.join(maps.keys()),
+ line, re.IGNORECASE)
+ if m:
+ mod = maps[m.group('map').lower()]
+ mod.filter = m.group('value')
+ continue
+ # scope <MAP>? <SCOPE>
+ m = re.match('scope\s+((?P<map>%s)\s+)?(?P<value>%s)' % (
+ '|'.join(maps.keys()),
+ '|'.join(_scope_options.keys())),
+ line, re.IGNORECASE)
+ if m:
+ keyword = m.group('keyword').lower()
+ mod = maps[str(m.group('map')).lower()]
+ mod.scope = _scope_options[m.group('keyword').lower()]
+ continue
+ # map <MAP> <ATTRIBUTE> <ATTMAPPING>
+ m = re.match('map\s+(?P<map>%s)\s+(?P<attribute>\S+)\s+(?P<value>\S.*)' %
+ '|'.join(maps.keys()),
+ line, re.IGNORECASE)
+ if m:
+ mod = maps[m.group('map').lower()]
+ attribute = m.group('attribute')
+ if attribute not in mod.attmap:
+ raise ParseError(filename, lineno, 'attribute %s unknown' % attribute)
+ mod.attmap[attribute] = m.group('value')
+ # TODO: filter out attributes that cannot be an expression
+ continue
+ # deref <DEREF>
+ m = re.match('deref\s+(?P<value>%s)' % '|'.join(_deref_options.keys()),
+ line, re.IGNORECASE)
+ if m:
+ global deref
+ deref = _deref_options[m.group('value').lower()]
+ continue
+ # nss_initgroups_ignoreusers <USER,USER>|<ALLLOCAL>
+ m = re.match('nss_initgroups_ignoreusers\s+(?P<value>\S.*)',
+ line, re.IGNORECASE)
+ if m:
+ global nss_initgroups_ignoreusers
+ users = m.group('value')
+ if users.lower() == 'alllocal':
+ # get all users known to the system currently (since nslcd isn't yet
+ # running, this should work)
+ import pwd
+ users = (x.pw_name for x in pwd.getpwall())
+ else:
+ users = users.split(',')
+ # TODO: warn about unknown users
+ nss_initgroups_ignoreusers.update(users)
+ continue
+ # pam_authz_search <FILTER>
+ m = re.match('pam_authz_search\s+(?P<value>\S.*)', line, re.IGNORECASE)
+ if m:
+ global pam_authz_search
+ from attmap import Expression
+ pam_authz_search = Expression(m.group('value'))
+ # TODO: check pam_authz_search expression to only contain username, service, ruser, rhost, tty, hostname, fqdn, dn or uid variables
+ continue
+ # ssl <on|off|start_tls>
+ m = re.match('ssl\s+(?P<value>%s)' %
+ '|'.join(_ssl_options.keys()),
+ line, re.IGNORECASE)
+ if m:
+ global ssl
+ ssl = _ssl_options[m.group('value').lower()]
+ continue
+ # tls_reqcert <demand|hard|yes...>
+ m = re.match('tls_reqcert\s+(?P<value>%s)' %
+ '|'.join(_tls_reqcert_options.keys()),
+ line, re.IGNORECASE)
+ if m:
+ global tls_reqcert
+ tls_reqcert = _tls_reqcert_options[m.group('value').lower()]
+ continue
+ # validnames /REGEX/i?
+ m = re.match('validnames\s+/(?P<value>.*)/(?P<flags>[i]?)$',
+ line, re.IGNORECASE)
+ if m:
+ global validnames
+ flags = 0 | re.IGNORECASE if m.group('flags') == 'i' else 0
+ validnames = re.compile(m.group('value'), flags=flags)
+ continue
+ # unrecognised line
+ raise ParseError(filename, lineno, 'error parsing line %r' % line)
+ # dump config (debugging code)
+ for k, v in globals().items():
+ if not k.startswith('_'):
+ print '%s=%r' % (k, v)