infoldap is of course LDAP
[cascardo/ipsilon.git] / ipsilon / info / infoldap.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2014 Ipsilon Project Contributors
4 #
5 # See the file named COPYING for the project license
6
7 from ipsilon.info.common import InfoProviderBase
8 from ipsilon.info.common import InfoProviderInstaller
9 from ipsilon.info.common import InfoMapping
10 from ipsilon.util.plugin import PluginObject
11 from ipsilon.util.log import Log
12 import ldap
13
14
15 # TODO: fetch mapping from configuration
16 ldap_mapping = {
17     'cn': 'fullname',
18     'commonname': 'fullname',
19     'sn': 'surname',
20     'mail': 'email',
21     'destinationindicator': 'country',
22     'postalcode': 'postcode',
23     'st': 'state',
24     'statetorprovincename': 'state',
25     'streetaddress': 'street',
26     'telephonenumber': 'phone',
27 }
28
29
30 class InfoProvider(InfoProviderBase, Log):
31
32     def __init__(self):
33         super(InfoProvider, self).__init__()
34         self.mapper = InfoMapping()
35         self.mapper.set_mapping(ldap_mapping)
36         self.name = 'ldap'
37         self.description = """
38 Info plugin that uses LDAP to retrieve user data. """
39         self._options = {
40             'server url': [
41                 """ The LDAP server url """,
42                 'string',
43                 'ldap://example.com'
44             ],
45             'tls': [
46                 " What TLS level show be required " +
47                 "(Demand, Allow, Try, Never, NoTLS) ",
48                 'string',
49                 'Demand'
50             ],
51             'bind dn': [
52                 """ User DN to bind as, if empty uses anonymous bind. """,
53                 'string',
54                 'uid=ipsilon,ou=People,dc=example,dc=com'
55             ],
56             'bind password': [
57                 """ Password to use for bind operation """,
58                 'string',
59                 'Password'
60             ],
61             'user dn template': [
62                 """ Template to turn username into DN. """,
63                 'string',
64                 'uid=%(username)s,ou=People,dc=example,dc=com'
65             ],
66         }
67
68     @property
69     def server_url(self):
70         return self.get_config_value('server url')
71
72     @property
73     def tls(self):
74         return self.get_config_value('tls')
75
76     @property
77     def bind_dn(self):
78         return self.get_config_value('bind dn')
79
80     @property
81     def bind_password(self):
82         return self.get_config_value('bind password')
83
84     @property
85     def user_dn_tmpl(self):
86         return self.get_config_value('user dn template')
87
88     def _ldap_bind(self):
89
90         tls = self.tls.lower()
91         tls_req_opt = None
92         if tls == "never":
93             tls_req_opt = ldap.OPT_X_TLS_NEVER
94         elif tls == "demand":
95             tls_req_opt = ldap.OPT_X_TLS_DEMAND
96         elif tls == "allow":
97             tls_req_opt = ldap.OPT_X_TLS_ALLOW
98         elif tls == "try":
99             tls_req_opt = ldap.OPT_X_TLS_TRY
100         if tls_req_opt is not None:
101             ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_opt)
102
103         conn = ldap.initialize(self.server_url)
104
105         if tls != "notls":
106             if not self.server_url.startswith("ldaps"):
107                 conn.start_tls_s()
108
109         conn.simple_bind_s(self.bind_dn, self.bind_password)
110
111         return conn
112
113     def _get_user_data(self, conn, dn):
114         result = conn.search_s(dn, ldap.SCOPE_BASE)
115         if result is None or result == []:
116             raise Exception('User object could not be found!')
117         elif len(result) > 1:
118             raise Exception('No unique user object could be found!')
119         data = dict()
120         for name, value in result[0][1].iteritems():
121             if type(value) is list and len(value) == 1:
122                 value = value[0]
123             data[name] = value
124         return data
125
126     def _get_user_groups(self, conn, dn, ldapattrs):
127         # TODO: fixme to support RFC2307bis schemas
128         if 'memberuid' in ldapattrs:
129             return ldapattrs['memberuid']
130         else:
131             return []
132
133     def get_user_data_from_conn(self, conn, dn):
134         reply = dict()
135         try:
136             ldapattrs = self._get_user_data(conn, dn)
137             userattrs, extras = self.mapper.map_attrs(ldapattrs)
138             groups = self._get_user_groups(conn, dn, ldapattrs)
139             reply['userdata'] = userattrs
140             reply['groups'] = groups
141             reply['extras'] = {'ldap': extras}
142         except Exception, e:  # pylint: disable=broad-except
143             self.error(e)
144
145         return reply
146
147     def get_user_attrs(self, user):
148         try:
149             conn = self._ldap_bind()
150             dn = self.user_dn_tmpl % {'username': user}
151             return self.get_user_data_from_conn(conn, dn)
152         except Exception, e:  # pylint: disable=broad-except
153             self.error(e)
154             return {}
155
156
157 class Installer(InfoProviderInstaller):
158
159     def __init__(self):
160         super(Installer, self).__init__()
161         self.name = 'ldap'
162
163     def install_args(self, group):
164         group.add_argument('--info-ldap', choices=['yes', 'no'], default='no',
165                            help='Use LDAP to populate user attrs')
166         group.add_argument('--info-ldap-server-url', action='store',
167                            help='LDAP Server Url')
168         group.add_argument('--info-ldap-bind-dn', action='store',
169                            help='LDAP Bind DN')
170         group.add_argument('--info-ldap-bind-pwd', action='store',
171                            help='LDAP Bind Password')
172         group.add_argument('--info-ldap-user-dn-template', action='store',
173                            help='LDAP User DN Template')
174
175     def configure(self, opts):
176         if opts['info_ldap'] != 'yes':
177             return
178
179         # Add configuration data to database
180         po = PluginObject()
181         po.name = 'ldap'
182         po.wipe_data()
183         po.wipe_config_values(self.facility)
184         config = dict()
185         if 'info_ldap_server_url' in opts:
186             config['server url'] = opts['info_ldap_server_url']
187         elif 'ldap_server_url' in opts:
188             config['server url'] = opts['ldap_server_url']
189         config = {'bind dn': opts['info_ldap_bind_dn']}
190         config = {'bind password': opts['info_ldap_bind_pwd']}
191         config = {'user dn template': opts['info_ldap_user_dn_template']}
192         if 'info_ldap_bind_dn' in opts:
193             config['bind dn'] = opts['info_ldap_bind_dn']
194         if 'info_ldap_bind_pwd' in opts:
195             config['bind password'] = opts['info_ldap_bind_pwd']
196         if 'info_ldap_user_dn_template' in opts:
197             config['user dn template'] = opts['info_ldap_user_dn_template']
198         elif 'ldap_bind_dn_template' in opts:
199             config['user dn template'] = opts['ldap_bind_dn_template']
200         config['tls'] = 'Demand'
201         po.set_config(config)
202         po.save_plugin_config(self.facility)
203
204         # Replace global config, only one plugin info can be used
205         po.name = 'global'
206         globalconf = po.get_plugin_config(self.facility)
207         if 'order' in globalconf:
208             order = globalconf['order'].split(',')
209         else:
210             order = []
211         order.append('ldap')
212         globalconf['order'] = ','.join(order)
213         po.set_config(globalconf)
214         po.save_plugin_config(self.facility)