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