Overview

Packages

  • Beatnik
  • None

Classes

  • Autogenerate
  • Beatnik
  • Beatnik_Driver
  • Beatnik_Driver_ldap2dns
  • Beatnik_Driver_pdnsgsql
  • Beatnik_Driver_sql
  • DeleteRecord
  • EditRecord
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Copyright 2005-2007 Alkaloid Networks <http://www.alkaloid.net>
  4:  *
  5:  * See the enclosed file COPYING for license information (GPL). If you
  6:  * did not receive this file, see http://www.horde.org/licenses/gpl.
  7:  *
  8:  * @author Ben Klang <ben@alkaloid.net>
  9:  * @package Beatnik
 10:  */
 11: class Beatnik_Driver_ldap2dns extends Beatnik_Driver
 12: {
 13:     /**
 14:      * Handle for the current database connection.
 15:      * @var object LDAP $_LDAP
 16:      */
 17:     var $_LDAP;
 18: 
 19:     /**
 20:      * Boolean indicating whether or not we're connected to the LDAP
 21:      * server.
 22:      * @var boolean $_connected
 23:      */
 24:     var $_connected = false;
 25: 
 26:     /**
 27:     * Constructs a new Beatnik LDAP driver object.
 28:     *
 29:     * @param array  $params    A hash containing connection parameters.
 30:     */
 31:     function Beatnik_Driver_ldap2dns($params = array())
 32:     {
 33:         parent::Beatnik_Driver($params);
 34:         $this->_connect();
 35:     }
 36: 
 37:     /**
 38:      * Get any record types  available specifically in this driver.
 39:      *
 40:      * @return array Records available only to this driver
 41:      */
 42:     function getRecDriverTypes()
 43:     {
 44:         return array(
 45:             'a+ptr' => 'A + PTR',
 46:         );
 47:     }
 48: 
 49:     /**
 50:      * Get any fields available specifically in this driver by record type.
 51:      *
 52:      * @param string $type Record type for which fields should be returned
 53:      *
 54:      * @return array Fields specific to this driver
 55:      */
 56:     function getRecDriverFields($type) {
 57:         $recset = array();
 58:         switch($type) {
 59:         case 'a+ptr':
 60:             $recset['hostname'] = array(
 61:                 'name' => 'Hostname',
 62:                 'description' => 'Hostname',
 63:                 'type' => 'text',
 64:                 'maxlength' => 0,
 65:                 'required' => true,
 66:                 'infoset' => 'basic',
 67:                 'index' => 1,
 68:             );
 69:             $recset['cipaddr'] = array(
 70:                 'name' => 'IP Address',
 71:                 'description' => 'IP Address to be forward and reverse mapped',
 72:                 'type' => 'ipaddress',
 73:                 'maxlength' => 0,
 74:                 'required' => true,
 75:                 'infoset' => 'basic',
 76:                 'index' => 2,
 77:             );
 78:             break;
 79:         }
 80: 
 81:         $recset['timestamp'] = array(
 82:             'name' => 'Timestamp',
 83:             'description' => '"Do Not Issue Before/After" Timestamp',
 84:             'type' => 'int',
 85:             'maxlength' => 0,
 86:             'required' => false,
 87:             'infoset' => 'advanced',
 88:             'index' => 100,
 89:         );
 90:         $recset['location'] = array(
 91:             'name' => 'Location',
 92:             'description' => 'Location Restriction',
 93:             'type' => 'text',
 94:             'maxlength' => 2,
 95:             'required' => false,
 96:             'infoset' => 'advanced',
 97:             'index' => 101,
 98:         );
 99: 
100:         return $recset;
101:     }
102: 
103:     /**
104:      * Gets all zones for accessible to the user matching the filter
105:      *
106:      * @access private
107:      *
108:      * @return array Array with zone records numerically indexed
109:      */
110:     function _getDomains()
111:     {
112:         static $zonedata = array();
113:         if (count($zonedata) > 0) {
114:             # If at least one element is in the array then we should have valid
115:             # cached data.
116:             return $zonedata;
117:         }
118: 
119:         // Record format
120:         // $zonedata =
121:         //       zone array ( # numerically indexed
122:         //                   zonename => zone domain name
123:         //                   serial => zone SOA serial number
124:         //                   refresh => zone SOA refresh
125:         //                   retry => zone SOA retry
126:         //                   expire => zone SOA expiry
127:         //                   minimum => zone SOA minimum
128:         //                   admin => zone contact admin
129:         //                   zonemaster => SOA master NS
130:         //           )
131: 
132:         $res = ldap_list($this->_LDAP,
133:             $this->_params['basedn'],
134:             "(objectClass=dnszone)");
135: 
136:         if ($res === false) {
137:             throw new Beatnik_Exception("Unable to locate any DNS zones " .
138:             "underneath ".$this->_params['basedn']);
139:         }
140: 
141:         $res = ldap_get_entries($this->_LDAP, $res);
142: 
143:         if ($res === false) {
144:             throw new Beatnik_Exception(sprintf(_("Unable to retrieve data from LDAP results: %s"), @ldap_error($this->_LDAP)));
145:         }
146: 
147:         $fields = Beatnik::getRecFields('soa');
148:         $i = 0;
149:         # FIXME: Add some way to handle missing zone data
150:         # FIXME: Don't forget to remove error silencers (@whatever)
151:         while ($i < $res['count']) {
152:             $tmp = array();
153: 
154:             foreach ($fields as $field => $fieldinfo) {
155:                 $key = strtolower($this->_getAttrByField($field));
156:                 if ($key === null) {
157:                     // This is not a field we are concerned with.  Skip it
158:                     continue;
159:                 }
160:                 // Special case for 'dn' as it's not treated as an array
161:                 if ($key == 'dn') {
162:                     $val = @ldap_explode_dn($res[$i]['dn'], 1);
163:                     $tmp[$field] = $val[0];
164:                     continue;
165:                 }
166:                 @$tmp[$field] = $res[$i][$key][0];
167:             }
168: 
169:             # Push the zone on the stack
170:             $zonedata[] = $tmp;
171: 
172:             # Next zone, please
173:             $i++;
174:         }
175:         return $zonedata;
176:     }
177: 
178:     /**
179:      * Map LDAP Attributes to application record fields
180:      *
181:      * @access private
182:      *
183:      * @param $field string  LDAP Attribute for which a record field should be
184:      *                       returned
185:      *
186:      * @return string  Application record field name
187:      */
188:     function _getAttrByField($field)
189:     {
190:         $field = strtolower($field);
191:         $fields = array(
192:             'hostname' => 'dnsdomainname',
193:             'zonename' => 'dnszonename', # FIXME This will go away for ldap2dns 0.4.x
194:             'serial' => 'dnsserial',
195:             'refresh' => 'dnsrefresh',
196:             'retry' => 'dnsretry',
197:             'expire' => 'dnsexpire',
198:             'minimum' => 'dnsminimum',
199:             'zonecontact' => 'dnsadminmailbox',
200:             'zonens' => 'dnszonemaster',
201:             'ttl' => 'dnsttl',
202:             'timestamp' => 'dnstimestamp',
203:             'location' => 'dnslocation',
204:             'ipaddr' => 'dnsipaddr',
205:             'ip6addr' => 'dnsipaddr',
206:             'cipaddr' => 'dnscipaddr',
207:             'pointer' => 'dnscname',
208:             'pref' => 'dnspreference',
209:             'priority' => 'dnssrvpriority',
210:             'weight' => 'dnssrvweight',
211:             'port' => 'dnssrvport',
212:             'text' => 'dnscname', # FIXME THIS WILL CHANGE IN ldap2dns 0.5.0!!!
213:             'id' => 'dn',
214:         );
215: 
216:         if (!isset($fields[$field])) {
217:             return null;
218:         }
219: 
220:         return $fields[$field];
221: 
222:     }
223: 
224:     /**
225:      * Gets all records associated with the given zone
226:      *
227:      * @param string $domain Retrieve records for this domain
228:      *
229:      * @return array Array with zone records
230:      */
231:     function getRecords($domain)
232:     {
233:         $domain = $this->cleanFilterString($domain);
234:         $dn = $this->_params['dn'].'='.$domain.','.$this->_params['basedn'];
235:         $res = @ldap_list($this->_LDAP, $dn, '(objectClass=dnsrrset)');
236: 
237:         if ($res === false) {
238:             throw new Beatnik_Exception("Unable to locate any DNS data for $domain");
239:         }
240: 
241:         # FIXME Cache these results
242:         $zonedata = array();
243:         $res = @ldap_get_entries($this->_LDAP, $res);
244:         if ($res === false) {
245:             throw new Beatnik_Exception(sprintf(_("Internal error: %s"), @ldap_error($this->_LDAP)));
246:         }
247: 
248:         $i = 0;
249:         while ($i < $res['count']) {
250:             $rectype = $res[$i]['dnstype'][0];
251:             // Special case for A+PTR records
252:             if ($rectype == 'a' && isset($res[$i]['dnscipaddr'])) {
253:                 $rectype = 'a+ptr';
254:             }
255:             if (!isset($zonedata[$rectype])) {
256:                 # Initialize this type if it hasn't already been done
257:                 $zonedata[$rectype] = array();
258:             }
259:             $tmp = array();
260:             foreach (Beatnik::getRecFields($rectype) as $field => $fielddata) {
261:                 $key = $this->_getAttrByField($field);
262:                 if ($key === null) {
263:                     // Not a key we care about
264:                     continue;
265:                 }
266:                 // Special case for 'dn' as it's not treated as an array
267:                 if ($key == 'dn') {
268:                     $val = @ldap_explode_dn($res[$i]['dn'], 1);
269:                     $tmp[$field] = $val[0];
270:                     continue;
271:                 }
272: 
273:                 // Only the first value is used.  All other are ignored.
274:                 $tmp[$field] = @$res[$i][$key][0];
275:             }
276:             # Push the record on the stack
277:             $zonedata[$rectype][] = $tmp;
278: 
279:             # Next entry, please.
280:             $i++;
281:         }
282: 
283:         return $zonedata;
284:     }
285: 
286:     /**
287:      * Delete record from backend
288:      *
289:      * @access private
290:      *
291:      * @param array $info  Reference to array of record information for deletion
292:      *
293:      * @return boolean true on success
294:      */
295:     function _deleteRecord(&$info)
296:     {
297:         // Ensure we have a record ID before continuing
298:         if (!isset($info['id'])) {
299:             throw new Beatnik_Exception(_("Unable to delete record: No record ID specified."));
300:         }
301: 
302:         // Attribute used to identify objects
303:         $dnattr = $this->_params['dn'];
304: 
305:         $suffix = $dnattr . '=' . $_SESSION['beatnik']['curdomain']['zonename'] . ',' . $this->_params['basedn'];
306:         if ($info['rectype'] == 'soa') {
307:             // FIXME: Add recursion
308:             throw new Beatnik_Exception(_("Unsupported recursive delete."));
309: 
310:             $domain = $this->cleanDNString($info['zonename']);
311:             $dn = $suffix;
312:         } else {
313:             $domain = $this->cleanDNString($_SESSION['beatnik']['curdomain']['zonename']);
314:             // Strip the array fluff and add the attribute
315:             $dn = $dnattr . '=' . $this->cleanDNString($info['id']) . ',' . $suffix;
316:         }
317: 
318:         $res = @ldap_delete($this->_LDAP, $dn);
319:         if ($res === false) {
320:             throw new Beatnik_Exception(sprintf(_("Unable to delete record.  Reason: %s"), @ldap_error($this->_LDAP)));
321:         }
322:         return true;
323:     }
324: 
325:     /**
326:      * Saves a new or edited record to the DNS backend
327:      *
328:      * @access private
329:      *
330:      * @param array $info Array from Horde_Form with record data
331:      *
332:      * @return mixed  The new or modified record ID on success;
333:      */
334:     function _saveRecord($info)
335:     {
336:         // Make sure we have a valid record type
337:         $rectype = strtolower($info['rectype']);
338:         $rdata = false;
339:         foreach (Beatnik::getRecTypes() as $rtype => $rdata) {
340:             if ($rectype == $rtype) {
341:                 break;
342:             }
343:             $rdata = false;
344:         }
345: 
346:         if (!$rdata) {
347:             throw new Beatnik_Exception(_("Invalid record type specified."));
348:         }
349: 
350:         $recfields = Beatnik::getRecFields($rectype);
351: 
352:         $entry = array();
353: 
354:         if ($rectype == 'a+ptr') {
355:             // Special case for A+PTR Records
356:             $entry['dnstype'] = 'a';
357:         } else {
358:             $entry['dnstype'] = $rectype;
359:         }
360: 
361:         $id = strtoupper($rectype);
362: 
363:         // Apply each piece of submitted data to the new/updated object
364:         foreach ($recfields as $field => $fdata) {
365:             // Translate the key to an LDAP attribute
366:             $key = $this->_getAttrByField($field);
367: 
368:             if ($key === null || $key == 'dn') {
369:                 // Skip the DN or any other key we don't care about
370:                 continue;
371:             }
372: 
373:             if (!isset($info[$field]) && isset($fdata['default'])) {
374:                 // No value specified.  Use the default
375:                 $val = $fdata['default'];
376:             } else {
377:                 // Only populate the field if there is actual data
378:                 if (isset($info[$field]) && strlen($info[$field])) {
379:                     $entry[$key] = $info[$field];
380:                 } else {
381:                     // $info[$field] was possibly unset
382:                     $info[$field] = '';
383:                     // If the record previously had data, we have to send an
384:                     // empty array to remove the attribute.  However, always
385:                     // sending an empty attribute causes PHP to return with
386:                     // "Protocol Error".  Hence this somewhat expensive check:
387:                     if (isset($info['id'])) {
388:                         list($type, $record) = $this->getRecord($info['id']);
389:                         if ($record && isset($record[$field])) {
390:                             $entry[$key] = array();
391:                         }
392:                     }
393:                 }
394:             }
395: 
396:             if (!isset($entry[$key]) && $fdata['required']) {
397:                 // No value available but required field
398:                 throw new Beatnik_Exception(sprintf(_("Missing required field %s to save record."), $fdata['name']));
399:             }
400: 
401:             // Construct an ID for this object as a tuple of its data.
402:             // This guarantees uniqueness.
403:             $id .= '-'.$this->cleanDNString($info[$field]);
404:         }
405: 
406:         // Create and populate the DN
407:         $key = $this->_params['dn'];
408:         $dn = '';
409:         // Special case for SOA records.
410:         if ($rectype == 'soa') {
411:             $domain = $this->cleanDNString($info['zonename']);
412:             $entry[$key] = $domain;
413:             $id = $domain;
414:             $dn = $key.'='.$domain;
415:             $suffix = $this->_params['basedn'];
416:         } else {
417:             // Everything else gets full id for DN
418:             $id = $this->cleanDNString($id);
419:             $entry[$key] = $id;
420:             $dn = $key.'='.$id;
421:             // The domain is held in the session
422:             $domain = $this->cleanDNString($_SESSION['beatnik']['curdomain']['zonename']);
423:             // Prepare the DN suffix
424:             $suffix = $key.'='.$domain.','.$this->_params['basedn'];
425:         }
426: 
427:         // Check to see if this is a modification
428:         if (isset($info['id'])) {
429:             // Get the base name of the old object
430:             $oldRDN = $key . '=' . $this->cleanDNString($info['id']);
431:             if ($dn != $oldRDN) {
432:                 // We have an old DN but it doesn't match the new DN.
433:                 // Need to rename the old object
434:                 if ($rectype == 'soa') {
435:                     throw new Beatnik_Exception(_("Unsupported operation: cannot rename a domain."));
436:                 }
437:                 $res = @ldap_rename($this->_LDAP, $oldRDN . ',' . $suffix,
438:                     $dn, $suffix, true);
439:                 if ($res === false) {
440:                     throw new Beatnik_Exception(sprintf(_("Unable to rename old object.  Reason: %s"), @ldap_error($this->_LDAP)));
441:                 }
442:             }
443: 
444:             // Finish appending the DN suffix information
445:             $dn .= ',' . $suffix;
446: 
447:             // Modify the existing record
448:             $res = @ldap_mod_replace($this->_LDAP, $dn, $entry);
449:             if ($res === false) {
450:                 throw new Beatnik_Exception(sprintf(_("Unable to modify record.  Reason: %s"), @ldap_error($this->_LDAP)));
451:             }
452: 
453:         } else {
454:             // Must be a new record
455:             // Append the suffix to the DN to make it fully qualified
456:             $dn .= ',' . $suffix;
457:             // Create the necessary objectClass definitions
458:             $entry['objectclass'] = array();
459:             $entry['objectclass'][] = 'top';
460:             $entry['objectclass'][] = 'dnszone';
461:             if ($rectype != 'soa') {
462:                 // An objectclass to hold the non-SOA record information
463:                 $entry['objectclass'][] = 'dnsrrset';
464:             }
465:             $res = @ldap_add($this->_LDAP, $dn, $entry);
466:             if ($res === false) {
467:                 throw new Beatnik_Exception(sprintf(_("Unable to add record to LDAP. Reason: %s"), @ldap_error($this->_LDAP)));
468:             }
469:         }
470: 
471:         return $id;
472:     }
473: 
474:     function cleanFilterString($string) {
475:         return preg_replace(
476:             array('/\*/',   '/\(/',   '/\)/',   '/\x00/'),
477:             array('\2a', '\28', '\29', '\00'),
478:             $string
479:         );
480:     }
481: 
482:     function cleanDNString($string) {
483:         return preg_replace(
484:             array('/=/', '/,/', '/\+/'),
485:             array('-', '~', ''),
486:             $string);
487:     }
488: 
489:     /**
490:      * Attempts to open a connection to the LDAP server.
491:      *
492:      * @access private
493:      *
494:      * @return boolean    True on success.
495:      * @throws Beatnik_Exception
496:      *
497:      * @access private
498:      */
499:     function _connect()
500:     {
501:         if (!$this->_connected) {
502:             Horde::assertDriverConfig($this->_params, 'storage',
503:                 array('hostspec', 'basedn', 'binddn', 'password', 'dn'));
504: 
505:             $port = (isset($this->_params['port'])) ?
506:                 $this->_params['port'] : 389;
507: 
508:             $this->_LDAP = ldap_connect($this->_params['hostspec'], $port);
509:             if (!$this->_LDAP) {
510:                 throw new Beatnik_Exception("Unable to connect to LDAP server $hostname on $port");
511:             }
512:             $res = ldap_set_option($this->_LDAP, LDAP_OPT_PROTOCOL_VERSION, $this->_params['version']);
513:             if ($res === false) {
514:                 throw new Beatnik_Exception("Unable to set LDAP protocol version");
515:             }
516:             $res = ldap_bind($this->_LDAP, $this->_params['binddn'], $this->_params['password']);
517:             if ($res === false) {
518:                 throw new Beatnik_Exception("Unable to bind to the LDAP server. Check authentication credentials.");
519:             }
520: 
521:             $this->_connected = true;
522:         }
523:         return true;
524:     }
525: }
526: 
API documentation generated by ApiGen