Report to user if an LDAP error occurs
[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
12
13 class LDAP(LoginFormBase, Log):
14
15     def __init__(self, site, mgr, page):
16         super(LDAP, self).__init__(site, mgr, page)
17         self.ldap_info = None
18
19     def _ldap_connect(self):
20
21         tls = self.lm.tls.lower()
22         tls_req_opt = None
23         if tls == "never":
24             tls_req_opt = ldap.OPT_X_TLS_NEVER
25         elif tls == "demand":
26             tls_req_opt = ldap.OPT_X_TLS_DEMAND
27         elif tls == "allow":
28             tls_req_opt = ldap.OPT_X_TLS_ALLOW
29         elif tls == "try":
30             tls_req_opt = ldap.OPT_X_TLS_TRY
31         if tls_req_opt is not None:
32             ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_opt)
33
34         conn = ldap.initialize(self.lm.server_url)
35
36         if tls != "notls":
37             if not self.lm.server_url.startswith("ldaps"):
38                 conn.start_tls_s()
39         return conn
40
41     def _authenticate(self, username, password):
42
43         conn = self._ldap_connect()
44         dn = self.lm.bind_dn_tmpl % {'username': username}
45         conn.simple_bind_s(dn, password)
46
47         # Bypass info plugins to optimize data retrieval
48         if self.lm.get_user_info:
49             self.lm.info = None
50
51             if not self.ldap_info:
52                 self.ldap_info = LDAPInfo(self._site)
53
54             base = self.lm.base_dn
55             return self.ldap_info.get_user_data_from_conn(conn, dn, base,
56                                                           username)
57
58         return None
59
60     def POST(self, *args, **kwargs):
61         username = kwargs.get("login_name")
62         password = kwargs.get("login_password")
63         userattrs = None
64         authok = False
65         errmsg = None
66
67         if username and password:
68             try:
69                 userattrs = self._authenticate(username, password)
70                 authok = True
71             except ldap.INVALID_CREDENTIALS as e:
72                 errmsg = "Authentication failed"
73                 self.error(errmsg)
74             except ldap.LDAPError as e:
75                 errmsg = 'Internal system error'
76                 if isinstance(e, ldap.TIMEOUT):
77                     self.error('LDAP request timed out')
78                 else:
79                     desc = e.args[0]['desc'].strip()
80                     info = e.args[0].get('info', '').strip()
81                     self.error("%s: %s %s" % (e.__class__.__name__,
82                                               desc, info))
83             except Exception as e:  # pylint: disable=broad-except
84                 errmsg = 'Internal system error'
85                 self.error("Exception raised: [%s]" % repr(e))
86         else:
87             self.error("Username or password is missing")
88
89         if authok:
90             return self.lm.auth_successful(self.trans, username, 'password',
91                                            userdata=userattrs)
92
93         context = self.create_tmpl_context(
94             username=username,
95             error=errmsg,
96             error_password=not password,
97             error_username=not username
98         )
99         self.lm.set_auth_error()
100         return self._template('login/form.html', **context)
101
102
103 class LoginManager(LoginManagerBase):
104
105     def __init__(self, *args, **kwargs):
106         super(LoginManager, self).__init__(*args, **kwargs)
107         self.name = 'ldap'
108         self.path = 'ldap'
109         self.page = None
110         self.ldap_info = None
111         self.service_name = 'ldap'
112         self.description = """
113 Form based login Manager that uses a simple bind LDAP operation to perform
114 authentication. """
115         self.new_config(
116             self.name,
117             pconfig.String(
118                 'server url',
119                 'The LDAP server url.',
120                 'ldap://example.com'),
121             pconfig.Template(
122                 'bind dn template',
123                 'Template to turn username into DN.',
124                 'uid=%(username)s,ou=People,dc=example,dc=com'),
125             pconfig.String(
126                 'base dn',
127                 'The base dn to look for users and groups',
128                 'dc=example,dc=com'),
129             pconfig.Condition(
130                 'get user info',
131                 'Get user info via ldap using user credentials',
132                 True),
133             pconfig.Pick(
134                 'tls',
135                 'What TLS level show be required',
136                 ['Demand', 'Allow', 'Try', 'Never', 'NoTLS'],
137                 'Demand'),
138             pconfig.String(
139                 'username text',
140                 'Text used to ask for the username at login time.',
141                 'Username'),
142             pconfig.String(
143                 'password text',
144                 'Text used to ask for the password at login time.',
145                 'Password'),
146             pconfig.String(
147                 'help text',
148                 'Text used to guide the user at login time.',
149                 'Provide your Username and Password')
150         )
151
152     @property
153     def help_text(self):
154         return self.get_config_value('help text')
155
156     @property
157     def username_text(self):
158         return self.get_config_value('username text')
159
160     @property
161     def password_text(self):
162         return self.get_config_value('password text')
163
164     @property
165     def server_url(self):
166         return self.get_config_value('server url')
167
168     @property
169     def tls(self):
170         return self.get_config_value('tls')
171
172     @property
173     def get_user_info(self):
174         return self.get_config_value('get user info')
175
176     @property
177     def bind_dn_tmpl(self):
178         return self.get_config_value('bind dn template')
179
180     @property
181     def base_dn(self):
182         return self.get_config_value('base dn')
183
184     def get_tree(self, site):
185         self.page = LDAP(site, self, 'login/ldap')
186         return self.page
187
188
189 class Installer(LoginManagerInstaller):
190
191     def __init__(self, *pargs):
192         super(Installer, self).__init__()
193         self.name = 'ldap'
194         self.pargs = pargs
195
196     def install_args(self, group):
197         group.add_argument('--ldap', choices=['yes', 'no'], default='no',
198                            help='Configure LDAP authentication')
199         group.add_argument('--ldap-server-url', action='store',
200                            help='LDAP Server Url')
201         group.add_argument('--ldap-bind-dn-template', action='store',
202                            help='LDAP Bind DN Template')
203         group.add_argument('--ldap-tls-level', action='store', default=None,
204                            help='LDAP TLS level')
205         group.add_argument('--ldap-base-dn', action='store',
206                            help='LDAP Base DN')
207
208     def configure(self, opts, changes):
209         if opts['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
218         config = dict()
219         if 'ldap_server_url' in opts:
220             config['server url'] = opts['ldap_server_url']
221         if 'ldap_bind_dn_template' in opts:
222             config['bind dn template'] = opts['ldap_bind_dn_template']
223         if 'ldap_tls_level' in opts and opts['ldap_tls_level'] is not None:
224             config['tls'] = opts['ldap_tls_level']
225         else:
226             config['tls'] = 'Demand'
227         if 'ldap_base_dn' in opts and opts['ldap_base_dn'] is not None:
228             config['base dn'] = opts['ldap_base_dn']
229         po.save_plugin_config(config)
230
231         # Update global config to add login plugin
232         po.is_enabled = True
233         po.save_enabled_state()
234
235         # For selinux enabled platforms permit httpd to connect to ldap,
236         # ignore if it fails
237         try:
238             subprocess.call(['/usr/sbin/setsebool', '-P',
239                              'httpd_can_connect_ldap=on'])
240         except Exception:  # pylint: disable=broad-except
241             pass