pam: use a pam object method instead of pam module function
[cascardo/ipsilon.git] / ipsilon / login / authldap.py
1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
2
3 from ipsilon.login.common import LoginFormBase, LoginManagerBase, \
4     LoginManagerInstaller
5 from ipsilon.util.plugin import PluginObject
6 from ipsilon.util.log import Log
7 from ipsilon.util import config as pconfig
8 from ipsilon.info.infoldap import InfoProvider as LDAPInfo
9 import ldap
10 import subprocess
11 import logging
12
13
14 def ldap_connect(server_url, tls):
15     tls = tls.lower()
16     tls_req_opt = None
17     if tls == "never":
18         tls_req_opt = ldap.OPT_X_TLS_NEVER
19     elif tls == "demand":
20         tls_req_opt = ldap.OPT_X_TLS_DEMAND
21     elif tls == "allow":
22         tls_req_opt = ldap.OPT_X_TLS_ALLOW
23     elif tls == "try":
24         tls_req_opt = ldap.OPT_X_TLS_TRY
25     if tls_req_opt is not None:
26         ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_opt)
27
28     conn = ldap.initialize(server_url)
29
30     if tls != "notls":
31         if not server_url.startswith("ldaps"):
32             conn.start_tls_s()
33
34     return conn
35
36
37 class LDAP(LoginFormBase, Log):
38
39     def __init__(self, site, mgr, page):
40         super(LDAP, self).__init__(site, mgr, page)
41         self.ldap_info = None
42
43     def _ldap_connect(self):
44         return ldap_connect(self.lm.server_url, self.lm.tls)
45
46     def _authenticate(self, username, password):
47
48         conn = self._ldap_connect()
49         dn = self.lm.bind_dn_tmpl % {'username': username}
50         conn.simple_bind_s(dn, password)
51
52         # Bypass info plugins to optimize data retrieval
53         if self.lm.get_user_info:
54             self.lm.info = None
55
56             if not self.ldap_info:
57                 self.ldap_info = LDAPInfo(self._site)
58
59             base = self.lm.base_dn
60             return self.ldap_info.get_user_data_from_conn(conn, dn, base,
61                                                           username)
62
63         return None
64
65     def POST(self, *args, **kwargs):
66         username = kwargs.get("login_name")
67         password = kwargs.get("login_password")
68         userattrs = None
69         authok = False
70         errmsg = None
71
72         if username and password:
73             try:
74                 userattrs = self._authenticate(username, password)
75                 authok = True
76             except ldap.INVALID_CREDENTIALS as e:
77                 errmsg = "Authentication failed"
78                 self.error(errmsg)
79             except ldap.LDAPError as e:
80                 errmsg = 'Internal system error'
81                 if isinstance(e, ldap.TIMEOUT):
82                     self.error('LDAP request timed out')
83                 else:
84                     desc = e.args[0]['desc'].strip()
85                     info = e.args[0].get('info', '').strip()
86                     self.error("%s: %s %s" % (e.__class__.__name__,
87                                               desc, info))
88             except Exception as e:  # pylint: disable=broad-except
89                 errmsg = 'Internal system error'
90                 self.error("Exception raised: [%s]" % repr(e))
91         else:
92             self.error("Username or password is missing")
93
94         if authok:
95             return self.lm.auth_successful(self.trans, username, 'password',
96                                            userdata=userattrs)
97
98         context = self.create_tmpl_context(
99             username=username,
100             error=errmsg,
101             error_password=not password,
102             error_username=not username
103         )
104         self.lm.set_auth_error()
105         return self._template('login/form.html', **context)
106
107
108 class LoginManager(LoginManagerBase):
109
110     def __init__(self, *args, **kwargs):
111         super(LoginManager, self).__init__(*args, **kwargs)
112         self.name = 'ldap'
113         self.path = 'ldap'
114         self.page = None
115         self.ldap_info = None
116         self.service_name = 'ldap'
117         self.description = """
118 Form based login Manager that uses a simple bind LDAP operation to perform
119 authentication. """
120         self.new_config(
121             self.name,
122             pconfig.String(
123                 'server url',
124                 'The LDAP server url.',
125                 'ldap://example.com'),
126             pconfig.Template(
127                 'bind dn template',
128                 'Template to turn username into DN.',
129                 'uid=%(username)s,ou=People,dc=example,dc=com'),
130             pconfig.String(
131                 'base dn',
132                 'The base dn to look for users and groups',
133                 'dc=example,dc=com'),
134             pconfig.Condition(
135                 'get user info',
136                 'Get user info via ldap using user credentials',
137                 True),
138             pconfig.Pick(
139                 'tls',
140                 'What TLS level show be required',
141                 ['Demand', 'Allow', 'Try', 'Never', 'NoTLS'],
142                 'Demand'),
143             pconfig.String(
144                 'username text',
145                 'Text used to ask for the username at login time.',
146                 'Username'),
147             pconfig.String(
148                 'password text',
149                 'Text used to ask for the password at login time.',
150                 'Password'),
151             pconfig.String(
152                 'help text',
153                 'Text used to guide the user at login time.',
154                 'Provide your Username and Password')
155         )
156
157     @property
158     def help_text(self):
159         return self.get_config_value('help text')
160
161     @property
162     def username_text(self):
163         return self.get_config_value('username text')
164
165     @property
166     def password_text(self):
167         return self.get_config_value('password text')
168
169     @property
170     def server_url(self):
171         return self.get_config_value('server url')
172
173     @property
174     def tls(self):
175         return self.get_config_value('tls')
176
177     @property
178     def get_user_info(self):
179         return self.get_config_value('get user info')
180
181     @property
182     def bind_dn_tmpl(self):
183         return self.get_config_value('bind dn template')
184
185     @property
186     def base_dn(self):
187         return self.get_config_value('base dn')
188
189     def get_tree(self, site):
190         self.page = LDAP(site, self, 'login/ldap')
191         return self.page
192
193
194 class Installer(LoginManagerInstaller):
195
196     def __init__(self, *pargs):
197         super(Installer, self).__init__()
198         self.name = 'ldap'
199         self.pargs = pargs
200
201     def install_args(self, group):
202         group.add_argument('--ldap', choices=['yes', 'no'], default='no',
203                            help='Configure LDAP authentication')
204         group.add_argument('--ldap-server-url', action='store',
205                            help='LDAP Server Url')
206         group.add_argument('--ldap-bind-dn-template', action='store',
207                            help='LDAP Bind DN Template')
208         group.add_argument('--ldap-tls-level', default='Demand',
209                            choices=['Demand', 'Allow', 'Try', 'Never',
210                                     'NoTLS'],
211                            help='LDAP TLS level')
212         group.add_argument('--ldap-base-dn', action='store',
213                            help='LDAP Base DN')
214
215     def configure(self, opts, changes):
216         if opts['ldap'] != 'yes':
217             return
218
219         # Add configuration data to database
220         po = PluginObject(*self.pargs)
221         po.name = 'ldap'
222         po.wipe_data()
223         po.wipe_config_values()
224
225         config = dict()
226         if 'ldap_server_url' in opts:
227             config['server url'] = opts['ldap_server_url']
228         else:
229             logging.error('LDAP Server URL is required')
230             return False
231         if 'ldap_bind_dn_template' in opts:
232             try:
233                 opts['ldap_bind_dn_template'] % {'username': 'test'}
234             except KeyError:
235                 logging.error(
236                     'Bind DN template does not contain %(username)s'
237                 )
238                 return False
239             except ValueError as e:
240                 logging.error(
241                     'Invalid syntax in Bind DN template: %s ',
242                     e
243                 )
244                 return False
245             config['bind dn template'] = opts['ldap_bind_dn_template']
246         if 'ldap_tls_level' in opts and opts['ldap_tls_level'] is not None:
247             config['tls'] = opts['ldap_tls_level']
248         else:
249             config['tls'] = 'Demand'
250         if 'ldap_base_dn' in opts and opts['ldap_base_dn'] is not None:
251             config['base dn'] = opts['ldap_base_dn']
252             test_dn = config['base dn']
253         else:
254             # default set in the config object
255             test_dn = 'dc=example,dc=com'
256
257         # Test the LDAP connection anonymously
258         try:
259             lh = ldap_connect(config['server url'], config['tls'])
260             lh.simple_bind_s('', '')
261             lh.search_s(test_dn, ldap.SCOPE_BASE,
262                         attrlist=['objectclasses'])
263         except ldap.INSUFFICIENT_ACCESS:
264             logging.warn('Anonymous access not allowed, continuing')
265         except ldap.UNWILLING_TO_PERFORM:  # probably minSSF issue
266             logging.warn('LDAP server unwilling to perform, expect issues')
267         except ldap.SERVER_DOWN:
268             logging.warn('LDAP server is down')
269         except ldap.NO_SUCH_OBJECT:
270             logging.error('Base DN not found')
271             return False
272         except ldap.LDAPError as e:
273             logging.error(e)
274             return False
275
276         po.save_plugin_config(config)
277
278         # Update global config to add login plugin
279         po.is_enabled = True
280         po.save_enabled_state()
281
282         # For selinux enabled platforms permit httpd to connect to ldap,
283         # ignore if it fails
284         try:
285             subprocess.call(['/usr/sbin/setsebool', '-P',
286                              'httpd_can_connect_ldap=on'])
287         except Exception:  # pylint: disable=broad-except
288             pass