summaryrefslogtreecommitdiffstats
path: root/doc/server/plugins/grouping/ldap.txt
blob: 90590a272d0a206084c86fd4b82c9e491e9db08f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
.. -*- mode: rst -*-

.. _server-plugins-grouping-ldap:

====
Ldap
====

.. warning::
    This plugin is considered experimental and has known issues (see below).

Purpose
-------

This plugin makes it possible to fetch data from an LDAP directory, process it
and attach it to your metadata.

Installation
------------

__ http://www.python-ldap.org/

First, you need to install the `python-ldap library`__. On debian-based systems this is
accomplished by::

    aptitude install python-ldap

To enable the plugin, add "Ldap" to the plugins line in your ``bcfg2.conf``.
Then add a new directory called "Ldap" to the root of your Bcfg2 repository and
define your queries in a file called ``config.py`` using the information in the
next section.

Configuration
-------------

As processing LDAP search results can get pretty complex, the configuration has 
to be written in Python.

Here is a minimal example to get you started::

    from Bcfg2.Server.Plugins.Ldap import LdapConnection, LdapQuery, LdapSubQuery, register_query
    
    conn_default = LdapConnection()
    conn_default.binddn = "uid=example,ou=People,dc=example,dc=com"
    conn_default.bindpw = "foobat"
    
    @register_query
    class ExampleQuery(LdapQuery):
        name = "example"
        base = "ou=People,dc=example,dc=com"
        scope = "one"
        attrs = ["cn", "uid"]
        connection = conn_default
    
        def prepare_query(self, metadata):
            self.filter = "(personalServer=" + metadata.hostname + ")"
    
        def process_result(self, metadata):
            if not self.result:
                admin_uid = None
                admin_name = "This server has no admin."
            return { 
                "admin_uid" : self.result[0][1]["uid"],
                "admin_name" : self.result[0][1]["cn"]
            }

The first line provides three classes for dealing with connections and queries
(details below) and a decorator function for registering your queries with the plugin.

In this example our LDAP directory has a number of user objects in it. Each of those
may have a personal server they administer. Whenever metadata for this machine is being
generated by the Bcfg2 server, the UID and name of the admin are retrieved from LDAP.

In your bundles and config templates, you can access this data via the metadata object::

    ${metadata.Ldap["example"]["admin_name"]}

Class reference
---------------

LdapConnection
++++++++++++++

.. class:: LdapConnection

   This class represents an LDAP connection. Every query must be associated with exactly 
   one connection.
   
.. attribute:: LdapConnection.binddn 
   
   DN used to authenticate against LDAP (required).
   
.. attribute:: LdapConnection.bindpw
   
   Password for the previously mentioned **binddn** (required).
  
.. attribute:: LdapConnection.host
   
   Hostname of host running the LDAP server (defaults to "localhost").

.. attribute:: LdapConnection.port
   
   Port where LDAP server is listening (defaults to 389).

You may pass any of these attributes as keyword arguments when creating the connection object.

LdapQuery
+++++++++

.. class:: LdapQuery
   
   This class defines a single query that may adapt itself depending on the current metadata.

.. attribute:: LdapQuery.attrs
   
   Can be used to retrieve only a certain subset of attributes. May either be a list of
   strings (attribute names) or ``None``, meaning all attributes (defaults to ``None``).

.. attribute:: LdapQuery.base
   
   This is the search base. Only LDAP entries below this DN will be included in your 
   search results (required).
   
.. attribute:: LdapQuery.connection
   
   Set this to an instance of the LdapConnection class (required).

.. attribute:: LdapQuery.filter
   
   LDAP search filter used to narrow down search results (defaults to ``(objectClass=*)``).

.. attribute:: LdapQuery.name
   
   This will be used as the dictionary key that provides access to the query results from
   the metadata object (``metadata.Ldap["NAMEGOESHERE"]``) (required).

.. attribute:: LdapQuery.scope
   
   Set this to one of "base", "one" or "sub" to specify LDAP search depth (defaults to "sub"). 

.. method:: LdapQuery.is_applicable(self, metadata)
   
   You can override this method to indicate whether this query makes sense for a given
   set of metadata (e.g. you need a query only for a certain bundle or group).
   
   (defaults to returning True)
   
.. method:: LdapQuery.prepare_query(self, metadata)
   
   Override this method to alter the query prior to execution. This is useful if your filter
   depends on the current metadata, e.g.::
   
       self.filter = "(cn=" + metadata.hostname + ")"
   
   (defaults to doing nothing)

.. method:: LdapQuery.process_result(self, metadata)
   
   You will probably override this method in every query to reformat the results from LDAP.
   The raw result is stored in ``self.result``, you must return the altered data. Note that LDAP
   search results are presented in this structure::
   
      (
          ("DN of first entry returned",
              {
                  "firstAttribute" : 1,
                  "secondAttribute" : 2,
              } 
          ),
          ("DN of second entry returned",
              {
                  "firstAttribute" : 1,
                  "secondAttribute" : 2,
              } 
          ),
      )
   
   Therefore, to return just the value of the firstAttribute of the second object returned,
   you'd write::
   
      return self.result[1][1][0]
   
   (defaults to returning ``self.result`` unaltered)

LdapSubQuery
++++++++++++

.. class:: LdapSubQuery

   Sometimes you need more than one query to obtain the data you need (e.g. use the first
   query to return all websites running on metadata.hostname and another query to find all
   customers that should have access to those sites).
   
   LdapSubQueries are the same as LdapQueries, except for that the methods
   
   * ``get_result()``
   * ``prepare_query()``
   * ``process_result()``
     
   allow any additional keyword arguments that may contain additional data as needed. Note 
   that ``get_result()`` will call ``prepare_query()`` and ``process_result()`` for you,
   so you shouldn't ever need to invoke these yourself, just override them.
   
Here is another example that uses LdapSubQuery::

	class WebSitesQuery(LdapSubQuery):
	    name = "web_sites"
	    filter = "(objectClass=webHostingSite)"
	    attrs = ["dc"]
	    connection = conn_default
	    
	    def prepare_query(self, metadata, base_dn):
	        self.base = base_dn
	    
	    def process_result(self, metadata):
	        [...] # build sites dict from returned dc attributes
	        return sites
	
	@register_query
	class WebPackagesQuery(LdapQuery):
	    name = "web_packages"
	    base = "dc=example,dc=com"
	    attrs = ["customerId"]
	    connection = conn_default
	    
	    def prepare_query(self, metadata):
	        self.filter = "(&(objectClass=webHostingPackage)(cn:dn:=" + metadata.hostname + "))"
	    
	    def process_result(self, metadata):
	        customers = {}
	        for customer in self.result:
	            dn = customer[0]
	            cid = customer[1]["customerId"][0]
	            customers[cid]["sites"] = WebSitesQuery().get_result(metadata, base_dn = dn)
	        return customers

This example assumes that we have a number of webhosting packages that contain various
sites. We need a first query ("web_packages") to get a list of the packages our customers
have and another query for each of those to find out what sites are contained in each 
package. The magic happens in the second class where ``WebSitesQuery.get_result()`` is 
called with the additional ``base_dn`` parameter that allows our LdapSubQuery to only 
search below that DN.

.. warning::
    Do NOT apply the ``register_query`` decorator to LdapSubQueries.

Known Issues
------------

* At this point there is no support for SSL/TLS.