pam: use a pam object method instead of pam module function
[cascardo/ipsilon.git] / ipsilon / info / infoldap.py
1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
2
3 from ipsilon.info.common import InfoProviderBase
4 from ipsilon.info.common import InfoProviderInstaller
5 from ipsilon.util.plugin import PluginObject
6 from ipsilon.util.policy import Policy
7 from ipsilon.util import config as pconfig
8 import ldap
9 import subprocess
10
11
12 # TODO: fetch mapping from configuration
13 ldap_mapping = [
14     ['cn', 'fullname'],
15     ['commonname', 'fullname'],
16     ['sn', 'surname'],
17     ['mail', 'email'],
18     ['destinationindicator', 'country'],
19     ['postalcode', 'postcode'],
20     ['st', 'state'],
21     ['statetorprovincename', 'state'],
22     ['streetaddress', 'street'],
23     ['telephonenumber', 'phone'],
24 ]
25
26
27 class InfoProvider(InfoProviderBase):
28
29     def __init__(self, *pargs):
30         super(InfoProvider, self).__init__(*pargs)
31         self.mapper = Policy(ldap_mapping)
32         self.name = 'ldap'
33         self.description = """
34 Info plugin that uses LDAP to retrieve user data. """
35         self.new_config(
36             self.name,
37             pconfig.String(
38                 'server url',
39                 'The LDAP server url.',
40                 'ldap://example.com'),
41             pconfig.Template(
42                 'user dn template',
43                 'Template to turn username into DN.',
44                 'uid=%(username)s,ou=People,dc=example,dc=com'),
45             pconfig.Pick(
46                 'tls',
47                 'What TLS level show be required',
48                 ['Demand', 'Allow', 'Try', 'Never', 'NoTLS'],
49                 'Demand'),
50             pconfig.String(
51                 'bind dn',
52                 'DN to bind as, if empty uses anonymous bind.',
53                 'uid=ipsilon,ou=People,dc=example,dc=com'),
54             pconfig.String(
55                 'bind password',
56                 'Password to use for bind operation'),
57             pconfig.String(
58                 'base dn',
59                 'The base dn to look for users and groups',
60                 'dc=example,dc=com'),
61         )
62
63     @property
64     def server_url(self):
65         return self.get_config_value('server url')
66
67     @property
68     def tls(self):
69         return self.get_config_value('tls')
70
71     @property
72     def bind_dn(self):
73         return self.get_config_value('bind dn')
74
75     @property
76     def bind_password(self):
77         return self.get_config_value('bind password')
78
79     @property
80     def user_dn_tmpl(self):
81         return self.get_config_value('user dn template')
82
83     @property
84     def base_dn(self):
85         return self.get_config_value('base dn')
86
87     def _ldap_bind(self):
88
89         tls = self.tls.lower()
90         tls_req_opt = None
91         if tls == "never":
92             tls_req_opt = ldap.OPT_X_TLS_NEVER
93         elif tls == "demand":
94             tls_req_opt = ldap.OPT_X_TLS_DEMAND
95         elif tls == "allow":
96             tls_req_opt = ldap.OPT_X_TLS_ALLOW
97         elif tls == "try":
98             tls_req_opt = ldap.OPT_X_TLS_TRY
99         if tls_req_opt is not None:
100             ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_opt)
101
102         conn = ldap.initialize(self.server_url)
103
104         if tls != "notls":
105             if not self.server_url.startswith("ldaps"):
106                 conn.start_tls_s()
107
108         conn.simple_bind_s(self.bind_dn, self.bind_password)
109
110         return conn
111
112     def _get_user_data(self, conn, dn):
113         result = conn.search_s(dn, ldap.SCOPE_BASE)
114         if result is None or result == []:
115             raise Exception('User object could not be found!')
116         elif len(result) > 1:
117             raise Exception('No unique user object could be found!')
118         data = dict()
119         for name, value in result[0][1].iteritems():
120             if isinstance(value, list) and len(value) == 1:
121                 value = value[0]
122             data[name] = value
123         return data
124
125     def _get_user_groups(self, conn, base, username):
126         # TODO: fixme to support RFC2307bis schemas
127         results = conn.search_s(base, ldap.SCOPE_SUBTREE,
128                                 filterstr='memberuid=%s' % username)
129         if results is None or results == []:
130             self.debug('No groups for %s' % username)
131             return []
132         groups = []
133         for r in results:
134             if 'cn' in r[1]:
135                 groups.append(r[1]['cn'][0])
136         return groups
137
138     def get_user_data_from_conn(self, conn, dn, base, username):
139         reply = dict()
140         try:
141             ldapattrs = self._get_user_data(conn, dn)
142             self.debug('LDAP attrs for %s: %s' % (dn, ldapattrs))
143             userattrs, extras = self.mapper.map_attributes(ldapattrs)
144             groups = self._get_user_groups(conn, base, username)
145             reply = userattrs
146             reply['_groups'] = groups
147             reply['_extras'] = {'ldap': extras}
148         except Exception, e:  # pylint: disable=broad-except
149             self.error('Error fetching/mapping LDAP user data: %s' % e)
150
151         return reply
152
153     def get_user_attrs(self, user):
154         try:
155             dn = self.user_dn_tmpl % {'username': user}
156         except ValueError as e:
157             self.error(
158                 'DN generation failed with template %s, user %s: %s'
159                 % (self.user_dn_tmpl, user, e)
160             )
161             return {}
162         except Exception as e:  # pylint: disable=broad-except
163             self.error(
164                 'Unhandled error generating DN from %s, user %s: %s'
165                 % (self.user_dn_tmpl, user, e)
166             )
167             return {}
168
169         try:
170             conn = self._ldap_bind()
171             base = self.base_dn
172             return self.get_user_data_from_conn(conn, dn, base, user)
173         except ldap.LDAPError as e:
174             self.error(
175                 'LDAP search failed for DN %s on base %s: %s' %
176                 (dn, base, e)
177             )
178             return {}
179         except Exception as e:  # pylint: disable=broad-except
180             self.error(
181                 'Unhandled LDAP error for DN %s on base %s: %s' %
182                 (dn, base, e)
183             )
184             return {}
185
186
187 class Installer(InfoProviderInstaller):
188
189     def __init__(self, *pargs):
190         super(Installer, self).__init__()
191         self.name = 'ldap'
192         self.pargs = pargs
193
194     def install_args(self, group):
195         group.add_argument('--info-ldap', choices=['yes', 'no'], default='no',
196                            help='Use LDAP to populate user attrs')
197         group.add_argument('--info-ldap-server-url', action='store',
198                            help='LDAP Server Url')
199         group.add_argument('--info-ldap-bind-dn', action='store',
200                            help='LDAP Bind DN')
201         group.add_argument('--info-ldap-bind-pwd', action='store',
202                            help='LDAP Bind Password')
203         group.add_argument('--info-ldap-user-dn-template', action='store',
204                            help='LDAP User DN Template')
205         group.add_argument('--info-ldap-base-dn', action='store',
206                            help='LDAP Base DN')
207
208     def configure(self, opts, changes):
209         if opts['info_ldap'] != 'yes':
210             return
211
212         # Add configuration data to database
213         po = PluginObject(*self.pargs)
214         po.name = 'ldap'
215         po.wipe_data()
216         po.wipe_config_values()
217         config = dict()
218         if 'info_ldap_server_url' in opts:
219             config['server url'] = opts['info_ldap_server_url']
220         elif 'ldap_server_url' in opts:
221             config['server url'] = opts['ldap_server_url']
222         if 'info_ldap_bind_dn' in opts:
223             config['bind dn'] = opts['info_ldap_bind_dn']
224         if 'info_ldap_bind_pwd' in opts:
225             config['bind password'] = opts['info_ldap_bind_pwd']
226         if 'info_ldap_user_dn_template' in opts:
227             config['user dn template'] = opts['info_ldap_user_dn_template']
228         elif 'ldap_bind_dn_template' in opts:
229             config['user dn template'] = opts['ldap_bind_dn_template']
230         if 'info_ldap_tls_level' in opts and opts['info_ldap_tls_level']:
231             config['tls'] = opts['info_ldap_tls_level']
232         elif 'ldap_tls_level' in opts and opts['ldap_tls_level']:
233             config['tls'] = opts['ldap_tls_level']
234         else:
235             config['tls'] = 'Demand'
236         if 'info_ldap_base_dn' in opts and opts['info_ldap_base_dn']:
237             config['base dn'] = opts['info_ldap_base_dn']
238         elif 'ldap_base_dn' in opts and opts['ldap_base_dn']:
239             config['base dn'] = opts['ldap_base_dn']
240         po.save_plugin_config(config)
241
242         # Update global config to add info plugin
243         po.is_enabled = True
244         po.save_enabled_state()
245
246         # For selinux enabled platforms permit httpd to connect to ldap,
247         # ignore if it fails
248         try:
249             subprocess.call(['/usr/sbin/setsebool', '-P',
250                              'httpd_can_connect_ldap=on'])
251         except Exception:  # pylint: disable=broad-except
252             pass