Validate options of the LDAP auth plugin on installation
[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 container %(username)s'
237                 )
238                 return False
239             config['bind dn template'] = opts['ldap_bind_dn_template']
240         if 'ldap_tls_level' in opts and opts['ldap_tls_level'] is not None:
241             config['tls'] = opts['ldap_tls_level']
242         else:
243             config['tls'] = 'Demand'
244         if 'ldap_base_dn' in opts and opts['ldap_base_dn'] is not None:
245             config['base dn'] = opts['ldap_base_dn']
246             test_dn = config['base dn']
247         else:
248             # default set in the config object
249             test_dn = 'dc=example,dc=com'
250
251         # Test the LDAP connection anonymously
252         try:
253             lh = ldap_connect(config['server url'], config['tls'])
254             lh.simple_bind_s('', '')
255             lh.search_s(test_dn, ldap.SCOPE_BASE,
256                         attrlist=['objectclasses'])
257         except ldap.INSUFFICIENT_ACCESS:
258             logging.warn('Anonymous access not allowed, continuing')
259         except ldap.UNWILLING_TO_PERFORM:  # probably minSSF issue
260             logging.warn('LDAP server unwilling to perform, expect issues')
261         except ldap.SERVER_DOWN:
262             logging.warn('LDAP server is down')
263         except ldap.NO_SUCH_OBJECT:
264             logging.error('Base DN not found')
265             return False
266         except ldap.LDAPError as e:
267             logging.error(e)
268             return False
269
270         po.save_plugin_config(config)
271
272         # Update global config to add login plugin
273         po.is_enabled = True
274         po.save_enabled_state()
275
276         # For selinux enabled platforms permit httpd to connect to ldap,
277         # ignore if it fails
278         try:
279             subprocess.call(['/usr/sbin/setsebool', '-P',
280                              'httpd_can_connect_ldap=on'])
281         except Exception:  # pylint: disable=broad-except
282             pass