Add LDAP test
[cascardo/ipsilon.git] / ipsilon / login / authldap.py
1 # Copyright (C) 2014  Ipsilon Contributors, see COPYING for license
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         authed = False
65         errmsg = None
66
67         if username and password:
68             try:
69                 userattrs = self._authenticate(username, password)
70                 authed = True
71             except Exception, e:  # pylint: disable=broad-except
72                 errmsg = "Authentication failed"
73                 self.error("Exception raised: [%s]" % repr(e))
74         else:
75             errmsg = "Username or password is missing"
76             self.error(errmsg)
77
78         if authed:
79             return self.lm.auth_successful(self.trans, username, 'password',
80                                            userdata=userattrs)
81
82         context = self.create_tmpl_context(
83             username=username,
84             error=errmsg,
85             error_password=not password,
86             error_username=not username
87         )
88         self.lm.set_auth_error()
89         # pylint: disable=star-args
90         return self._template('login/form.html', **context)
91
92
93 class LoginManager(LoginManagerBase):
94
95     def __init__(self, *args, **kwargs):
96         super(LoginManager, self).__init__(*args, **kwargs)
97         self.name = 'ldap'
98         self.path = 'ldap'
99         self.page = None
100         self.ldap_info = None
101         self.service_name = 'ldap'
102         self.description = """
103 Form based login Manager that uses a simple bind LDAP operation to perform
104 authentication. """
105         self.new_config(
106             self.name,
107             pconfig.String(
108                 'server url',
109                 'The LDAP server url.',
110                 'ldap://example.com'),
111             pconfig.Template(
112                 'bind dn template',
113                 'Template to turn username into DN.',
114                 'uid=%(username)s,ou=People,dc=example,dc=com'),
115             pconfig.String(
116                 'base dn',
117                 'The base dn to look for users and groups',
118                 'dc=example,dc=com'),
119             pconfig.Condition(
120                 'get user info',
121                 'Get user info via ldap using user credentials',
122                 True),
123             pconfig.Pick(
124                 'tls',
125                 'What TLS level show be required',
126                 ['Demand', 'Allow', 'Try', 'Never', 'NoTLS'],
127                 'Demand'),
128             pconfig.String(
129                 'username text',
130                 'Text used to ask for the username at login time.',
131                 'Username'),
132             pconfig.String(
133                 'password text',
134                 'Text used to ask for the password at login time.',
135                 'Password'),
136             pconfig.String(
137                 'help text',
138                 'Text used to guide the user at login time.',
139                 'Provide your Username and Password')
140         )
141
142     @property
143     def help_text(self):
144         return self.get_config_value('help text')
145
146     @property
147     def username_text(self):
148         return self.get_config_value('username text')
149
150     @property
151     def password_text(self):
152         return self.get_config_value('password text')
153
154     @property
155     def server_url(self):
156         return self.get_config_value('server url')
157
158     @property
159     def tls(self):
160         return self.get_config_value('tls')
161
162     @property
163     def get_user_info(self):
164         return self.get_config_value('get user info')
165
166     @property
167     def bind_dn_tmpl(self):
168         return self.get_config_value('bind dn template')
169
170     @property
171     def base_dn(self):
172         return self.get_config_value('base dn')
173
174     def get_tree(self, site):
175         self.page = LDAP(site, self, 'login/ldap')
176         return self.page
177
178
179 class Installer(LoginManagerInstaller):
180
181     def __init__(self, *pargs):
182         super(Installer, self).__init__()
183         self.name = 'ldap'
184         self.pargs = pargs
185
186     def install_args(self, group):
187         group.add_argument('--ldap', choices=['yes', 'no'], default='no',
188                            help='Configure LDAP authentication')
189         group.add_argument('--ldap-server-url', action='store',
190                            help='LDAP Server Url')
191         group.add_argument('--ldap-bind-dn-template', action='store',
192                            help='LDAP Bind DN Template')
193         group.add_argument('--ldap-tls-level', action='store', default=None,
194                            help='LDAP TLS level')
195         group.add_argument('--ldap-base-dn', action='store',
196                            help='LDAP Base DN')
197
198     def configure(self, opts):
199         if opts['ldap'] != 'yes':
200             return
201
202         # Add configuration data to database
203         po = PluginObject(*self.pargs)
204         po.name = 'ldap'
205         po.wipe_data()
206         po.wipe_config_values()
207
208         config = dict()
209         if 'ldap_server_url' in opts:
210             config['server url'] = opts['ldap_server_url']
211         if 'ldap_bind_dn_template' in opts:
212             config['bind dn template'] = opts['ldap_bind_dn_template']
213         if 'ldap_tls_level' in opts and opts['ldap_tls_level'] is not None:
214             config['tls'] = opts['ldap_tls_level']
215         else:
216             config['tls'] = 'Demand'
217         if 'ldap_base_dn' in opts and opts['ldap_base_dn'] is not None:
218             config['base dn'] = opts['ldap_base_dn']
219         po.save_plugin_config(config)
220
221         # Update global config to add login plugin
222         po.is_enabled = True
223         po.save_enabled_state()
224
225         # For selinux enabled platforms permit httpd to connect to ldap,
226         # ignore if it fails
227         try:
228             subprocess.call(['/usr/sbin/setsebool', '-P',
229                              'httpd_can_connect_ldap=on'])
230         except Exception:  # pylint: disable=broad-except
231             pass