Use the new Policy engine for login/info mapping
[cascardo/ipsilon.git] / ipsilon / info / infoldap.py
1 # Copyright (C) 2014 Ipsilon Project Contributors
2 #
3 # See the file named COPYING for the project license
4
5 from ipsilon.info.common import InfoProviderBase
6 from ipsilon.info.common import InfoProviderInstaller
7 from ipsilon.util.plugin import PluginObject
8 from ipsilon.util.policy import Policy
9 from ipsilon.util import config as pconfig
10 import ldap
11
12
13 # TODO: fetch mapping from configuration
14 ldap_mapping = [
15     ['cn', 'fullname'],
16     ['commonname', 'fullname'],
17     ['sn', 'surname'],
18     ['mail', 'email'],
19     ['destinationindicator', 'country'],
20     ['postalcode', 'postcode'],
21     ['st', 'state'],
22     ['statetorprovincename', 'state'],
23     ['streetaddress', 'street'],
24     ['telephonenumber', 'phone'],
25 ]
26
27
28 class InfoProvider(InfoProviderBase):
29
30     def __init__(self, *pargs):
31         super(InfoProvider, self).__init__(*pargs)
32         self.mapper = Policy(ldap_mapping)
33         self.name = 'ldap'
34         self.description = """
35 Info plugin that uses LDAP to retrieve user data. """
36         self.new_config(
37             self.name,
38             pconfig.String(
39                 'server url',
40                 'The LDAP server url.',
41                 'ldap://example.com'),
42             pconfig.Template(
43                 'user dn template',
44                 'Template to turn username into DN.',
45                 'uid=%(username)s,ou=People,dc=example,dc=com'),
46             pconfig.Pick(
47                 'tls',
48                 'What TLS level show be required',
49                 ['Demand', 'Allow', 'Try', 'Never', 'NoTLS'],
50                 'Demand'),
51             pconfig.String(
52                 'bind dn',
53                 'DN to bind as, if empty uses anonymous bind.',
54                 'uid=ipsilon,ou=People,dc=example,dc=com'),
55             pconfig.String(
56                 'bind password',
57                 'Password to use for bind operation'),
58         )
59
60     @property
61     def server_url(self):
62         return self.get_config_value('server url')
63
64     @property
65     def tls(self):
66         return self.get_config_value('tls')
67
68     @property
69     def bind_dn(self):
70         return self.get_config_value('bind dn')
71
72     @property
73     def bind_password(self):
74         return self.get_config_value('bind password')
75
76     @property
77     def user_dn_tmpl(self):
78         return self.get_config_value('user dn template')
79
80     def _ldap_bind(self):
81
82         tls = self.tls.lower()
83         tls_req_opt = None
84         if tls == "never":
85             tls_req_opt = ldap.OPT_X_TLS_NEVER
86         elif tls == "demand":
87             tls_req_opt = ldap.OPT_X_TLS_DEMAND
88         elif tls == "allow":
89             tls_req_opt = ldap.OPT_X_TLS_ALLOW
90         elif tls == "try":
91             tls_req_opt = ldap.OPT_X_TLS_TRY
92         if tls_req_opt is not None:
93             ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_opt)
94
95         conn = ldap.initialize(self.server_url)
96
97         if tls != "notls":
98             if not self.server_url.startswith("ldaps"):
99                 conn.start_tls_s()
100
101         conn.simple_bind_s(self.bind_dn, self.bind_password)
102
103         return conn
104
105     def _get_user_data(self, conn, dn):
106         result = conn.search_s(dn, ldap.SCOPE_BASE)
107         if result is None or result == []:
108             raise Exception('User object could not be found!')
109         elif len(result) > 1:
110             raise Exception('No unique user object could be found!')
111         data = dict()
112         for name, value in result[0][1].iteritems():
113             if type(value) is list and len(value) == 1:
114                 value = value[0]
115             data[name] = value
116         return data
117
118     def _get_user_groups(self, conn, dn, ldapattrs):
119         # TODO: fixme to support RFC2307bis schemas
120         if 'memberuid' in ldapattrs:
121             return ldapattrs['memberuid']
122         else:
123             return []
124
125     def get_user_data_from_conn(self, conn, dn):
126         reply = dict()
127         try:
128             ldapattrs = self._get_user_data(conn, dn)
129             userattrs, extras = self.mapper.map_attributes(ldapattrs)
130             groups = self._get_user_groups(conn, dn, ldapattrs)
131             reply = userattrs
132             reply['_groups'] = groups
133             reply['_extras'] = {'ldap': extras}
134         except Exception, e:  # pylint: disable=broad-except
135             self.error(e)
136
137         return reply
138
139     def get_user_attrs(self, user):
140         try:
141             conn = self._ldap_bind()
142             dn = self.user_dn_tmpl % {'username': user}
143             return self.get_user_data_from_conn(conn, dn)
144         except Exception, e:  # pylint: disable=broad-except
145             self.error(e)
146             return {}
147
148
149 class Installer(InfoProviderInstaller):
150
151     def __init__(self, *pargs):
152         super(Installer, self).__init__()
153         self.name = 'ldap'
154         self.pargs = pargs
155
156     def install_args(self, group):
157         group.add_argument('--info-ldap', choices=['yes', 'no'], default='no',
158                            help='Use LDAP to populate user attrs')
159         group.add_argument('--info-ldap-server-url', action='store',
160                            help='LDAP Server Url')
161         group.add_argument('--info-ldap-bind-dn', action='store',
162                            help='LDAP Bind DN')
163         group.add_argument('--info-ldap-bind-pwd', action='store',
164                            help='LDAP Bind Password')
165         group.add_argument('--info-ldap-user-dn-template', action='store',
166                            help='LDAP User DN Template')
167
168     def configure(self, opts):
169         if opts['info_ldap'] != 'yes':
170             return
171
172         # Add configuration data to database
173         po = PluginObject(*self.pargs)
174         po.name = 'ldap'
175         po.wipe_data()
176         po.wipe_config_values()
177         config = dict()
178         if 'info_ldap_server_url' in opts:
179             config['server url'] = opts['info_ldap_server_url']
180         elif 'ldap_server_url' in opts:
181             config['server url'] = opts['ldap_server_url']
182         config = {'bind dn': opts['info_ldap_bind_dn']}
183         config = {'bind password': opts['info_ldap_bind_pwd']}
184         config = {'user dn template': opts['info_ldap_user_dn_template']}
185         if 'info_ldap_bind_dn' in opts:
186             config['bind dn'] = opts['info_ldap_bind_dn']
187         if 'info_ldap_bind_pwd' in opts:
188             config['bind password'] = opts['info_ldap_bind_pwd']
189         if 'info_ldap_user_dn_template' in opts:
190             config['user dn template'] = opts['info_ldap_user_dn_template']
191         elif 'ldap_bind_dn_template' in opts:
192             config['user dn template'] = opts['ldap_bind_dn_template']
193         config['tls'] = 'Demand'
194         po.save_plugin_config(config)
195
196         # Update global config to add info plugin
197         po.is_enabled = True
198         po.save_enabled_state()