* API: full support of active directory as account storage, tested so far with Samba4

This commit is contained in:
Ralf Becker 2013-05-22 17:22:20 +00:00
parent f993f20723
commit 4037993dc5
10 changed files with 1668 additions and 79 deletions

View File

@ -0,0 +1,150 @@
<?php
/**
* Addressbook - ADS Backend
*
* @link http://www.egroupware.org
* @author Ralf Becker <rb@stylite.de>
* @package addressbook
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @version $Id$
*/
/**
* Active directory backend for accounts (not yet AD contacts)
*
* We use ADS objectGUID as contact ID and UID.
*
* All values used to construct filters need to run through ldap::quote(),
* to be save against LDAP query injection!!!
*
* @todo get saving of contacts working: fails while checking of container exists ...
*/
class addressbook_ads extends addressbook_ldap
{
/**
* LDAP searches only a limited set of attributes for performance reasons,
* you NEED an index for that columns, ToDo: make it configurable
* minimum: $this->columns_to_search = array('n_family','n_given','org_name','email');
*/
var $search_attributes = array(
'n_family','n_middle','n_given','org_name','org_unit',
'adr_one_location','note','email','samaccountname',
);
/**
* Filter used for accounts addressbook
* @var string
*/
var $accountsFilter = '(objectclass=user)';
/**
* Accounts ADS object
*
* @var accounts_ads
*/
protected $accounts_ads;
/**
* constructor of the class
*
* @param array $ldap_config=null default use from $GLOBALS['egw_info']['server']
* @param resource $ds=null ldap connection to use
*/
function __construct(array $ldap_config=null, $ds=null)
{
//$this->db_data_cols = $this->stock_contact_fields + $this->non_contact_fields;
$this->accountName = $GLOBALS['egw_info']['user']['account_lid'];
if ($ldap_config)
{
$this->ldap_config = $ldap_config;
}
else
{
$this->ldap_config =& $GLOBALS['egw_info']['server'];
}
$this->accounts_ads = $GLOBALS['egw']->accounts->backend;
//$this->personalContactsDN = 'ou=personal,ou=contacts,'. $this->ldap_config['ldap_contact_context'];
//$this->sharedContactsDN = 'ou=shared,ou=contacts,'. $this->ldap_config['ldap_contact_context'];
$this->allContactsDN = $this->accountContactsDN = $this->accounts_ads->ads_context();
if ($ds)
{
$this->ds = $ds;
}
else
{
$this->connect();
}
$this->ldapServerInfo = ldapserverinfo::get($this->ds, $this->ldap_config['ads_host']);
// AD seems to use user, instead of inetOrgPerson
$this->schema2egw['user'] = $this->schema2egw['inetorgperson'];
$this->schema2egw['user'] += array(
'account_id' => 'objectsid',
'account_lid' => 'samaccountname',
'contact_uid' => 'objectguid',
);
foreach($this->schema2egw as $schema => $attributes)
{
$this->all_attributes = array_merge($this->all_attributes,array_values($attributes));
}
$this->all_attributes = array_values(array_unique($this->all_attributes));
$this->charset = translation::charset();
}
/**
* connect to LDAP server
*
* @param boolean $admin=false true (re-)connect with admin not user credentials, eg. to modify accounts
*/
function connect($admin=false)
{
$this->ds = $this->accounts_ads->ldap_connection();
}
/**
* reads contact data
*
* @param string/array $contact_id contact_id or array with values for id or account_id
* @return array/boolean data if row could be retrived else False
*/
function read($contact_id)
{
if (is_array($contact_id) && isset($contact_id['account_id']) ||
!is_array($contact_id) && substr($contact_id,0,8) == 'account:')
{
$account_id = (int)(is_array($contact_id) ? $contact_id['account_id'] : substr($contact_id,8));
$contact_id = $GLOBALS['egw']->accounts->id2name($account_id, 'person_id');
}
$contact_id = ldap::quote(!is_array($contact_id) ? $contact_id :
(isset ($contact_id['id']) ? $contact_id['id'] : $contact_id['uid']));
$rows = $this->_searchLDAP($this->allContactsDN, "(objectguid=$contact_id)", $this->all_attributes, ADDRESSBOOK_ALL);
return $rows ? $rows[0] : false;
}
/**
* Special handling for mapping data of ADA user objectclass to eGW contact
*
* Please note: all regular fields are already copied!
*
* @internal
* @param array &$contact already copied fields according to the mapping
* @param array $data eGW contact data
*/
function _user2egw(&$contact, $data)
{
$contact['account_id'] = $this->accounts_ads->objectsid2account_id($data['objectsid']);
$contact['id'] = $contact['contact_uid'] = $this->accounts_ads->objectguid2str($data['objectguid']);
// ignore system accounts
if ($contact['account_id'] < accounts_ads::MIN_ACCOUNT_ID) return false;
$this->_inetorgperson2egw($contact, $data);
}
}

View File

@ -59,6 +59,22 @@ class addressbook_ldap
*/
var $sharedContactsDN;
/**
* @var string $accountContactsDN holds the base DN for accounts addressbook
*/
var $accountContactsDN;
/**
* Filter used for accounts addressbook
* @var string
*/
var $accountsFilter = '(objectclass=posixaccount)';
/**
* @var string $allContactsDN holds the base DN of all addressbook
*/
var $allContactsDN;
/**
* @var int $total holds the total count of found rows
*/
@ -71,6 +87,17 @@ class addressbook_ldap
*/
var $charset;
/**
* LDAP searches only a limited set of attributes for performance reasons,
* you NEED an index for that columns, ToDo: make it configurable
* minimum: $this->columns_to_search = array('n_family','n_given','org_name','email');
*/
var $search_attributes = array(
'n_family','n_middle','n_given','org_name','org_unit',
'adr_one_location','adr_two_location','note',
'email','mozillasecondemail','uidnumber',
);
/**
* maps between diverse ldap schema and the eGW internal names
*
@ -82,6 +109,7 @@ class addressbook_ldap
'posixaccount' => array(
'account_id' => 'uidnumber',
'account_lid' => 'uid',
'shadowexpire',
),
'inetorgperson' => array(
'n_fn' => 'cn',
@ -230,6 +258,8 @@ class addressbook_ldap
}
$this->personalContactsDN = 'ou=personal,ou=contacts,'. $this->ldap_config['ldap_contact_context'];
$this->sharedContactsDN = 'ou=shared,ou=contacts,'. $this->ldap_config['ldap_contact_context'];
$this->accountsContactsDN = $this->ldap_config['ldap_context'];
$this->allContactsDN = $this->ldap_config['ldap_contact_context'];
if ($ds)
{
@ -260,11 +290,17 @@ class addressbook_ldap
/**
* connect to LDAP server
*
* @param boolean $admin=false true (re-)connect with admin not user credentials, eg. to modify accounts
*/
function connect()
function connect($admin = false)
{
if ($admin)
{
$this->ds = $GLOBALS['egw']->ldap->ldapConnect();
}
// if ldap is NOT the contact repository, we only do accounts and need to use the account-data
if (substr($GLOBALS['egw_info']['server']['contact_repository'],-4) != 'ldap') // not (ldap or sql-ldap)
elseif (substr($GLOBALS['egw_info']['server']['contact_repository'],-4) != 'ldap') // not (ldap or sql-ldap)
{
$this->ldap_config['ldap_contact_host'] = $this->ldap_config['ldap_host'];
$this->ldap_config['ldap_contact_context'] = $this->ldap_config['ldap_context'];
@ -364,10 +400,10 @@ class addressbook_ldap
$data['account_id'] == $GLOBALS['egw_info']['user']['account_id']))
{
// account
$baseDN = $this->ldap_config['ldap_context'];
$baseDN = $this->accountContactsDN;
$cn = false;
// we need an admin connection
$this->ds = $GLOBALS['egw']->ldap->ldapConnect();
$this->ds = $this->connect(true);
// for sql-ldap we need to account_lid/uid as id, NOT the contact_id in id!
if ($GLOBALS['egw_info']['server']['contact_repository'] == 'sql-ldap')
@ -606,7 +642,6 @@ class addressbook_ldap
*/
function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='',$need_full_no_count=false)
{
//_debug_array($criteria); print "OrderBY: $order_by";_debug_array($extra_cols);_debug_array($filter);
#$order_by = explode(',',$order_by);
#$order_by = explode(' ',$order_by);
#$sort = $order_by[0];
@ -650,20 +685,19 @@ class addressbook_ldap
}
elseif (!isset($filter['owner']))
{
$searchDN = $this->ldap_config['ldap_contact_context'];
$searchDN = $this->allContactsDN;
$addressbookType = ADDRESSBOOK_ALL;
}
else
{
$searchDN = $this->ldap_config['ldap_context'];
$searchDN = $this->accountContactsDN;
$addressbookType = ADDRESSBOOK_ACCOUNTS;
}
// create the search filter
switch($addressbookType)
{
case ADDRESSBOOK_ACCOUNTS:
$objectFilter = '(objectclass=posixaccount)';
$objectFilter = $this->accountsFilter;
break;
default:
$objectFilter = '(objectclass=inetorgperson)';
@ -867,16 +901,14 @@ class addressbook_ldap
$this->total = 0;
$_attributes[] = 'entryUUID';
$_attributes[] = 'uid';
$_attributes[] = 'uidNumber';
$_attributes[] = 'objectClass';
$_attributes[] = 'createTimestamp';
$_attributes[] = 'modifyTimestamp';
$_attributes[] = 'creatorsName';
$_attributes[] = 'modifiersName';
$_attributes[] = 'shadowExpire';
//echo "<p>ldap_search($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit)</p>\n";
//echo "<p>ldap_search($this->ds, '$_ldapContext', '$_filter', ".array2string($_attributes).", 0, $this->ldapLimit)</p>\n";
if($_addressbooktype == ADDRESSBOOK_ALL)
{
$result = ldap_search($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit);
@ -885,22 +917,13 @@ class addressbook_ldap
{
$result = @ldap_list($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit);
}
if(!$result) return array();
$entries = ldap_get_entries($this->ds, $result);
if(!$result || !$entries = ldap_get_entries($this->ds, $result)) return array();
$this->total = $entries['count'];
$shadowExpireNow = floor((time()-date('Z'))/86400);
foreach((array)$entries as $i => $entry)
foreach($entries as $i => $entry)
{
if (!is_int($i)) continue; // eg. count
// exclude expired or deactivated accounts
if (isset($entry['shadowexpire']) && $entry['shadowexpire'][0] <= $shadowExpireNow)
{
--$this->total;
continue;
}
$contact = array(
'id' => $entry['uid'][0] ? $entry['uid'][0] : $entry['entryuuid'][0],
'tid' => 'n', // the type id for the addressbook
@ -914,7 +937,7 @@ class addressbook_ldap
}
foreach($this->schema2egw[$objectclass] as $egwFieldName => $ldapFieldName)
{
if(!empty($entry[$ldapFieldName][0]) && !isset($contact[$egwFieldName]))
if(!empty($entry[$ldapFieldName][0]) && !is_int($egwFieldName) && !isset($contact[$egwFieldName]))
{
$contact[$egwFieldName] = translation::convert($entry[$ldapFieldName][0],'utf-8');
}
@ -922,7 +945,11 @@ class addressbook_ldap
$objectclass2egw = '_'.$objectclass.'2egw';
if (method_exists($this,$objectclass2egw))
{
$this->$objectclass2egw($contact,$entry);
if (($ret=$this->$objectclass2egw($contact,$entry)) === false)
{
--$this->total;
continue 2;
}
}
}
// read binary jpegphoto only for one result == call by read
@ -1003,8 +1030,10 @@ class addressbook_ldap
{
return false;
}
//error_log(__METHOD__."('$baseDN') !ldap_read({$this->ds}, '$baseDN', 'objectclass=*') ldap_errno()=".ldap_errno($this->ds).', ldap_error()='.ldap_error($this->ds).get_class($this));
if(ldap_errno($this->ds) != 32 || substr($baseDN,0,3) != 'cn=')
{
error_log(__METHOD__."('$baseDN') baseDN does NOT exist and we cant/wont create it! ldap_errno()=".ldap_errno($this->ds).', ldap_error()='.ldap_error($this->ds));
return $this->_error(__LINE__); // baseDN does NOT exist and we cant/wont create it
}
// create a admin connection to add the needed DN
@ -1156,6 +1185,27 @@ class addressbook_ldap
}
}
/**
* Special handling for mapping data of posixAccount objectclass to eGW contact
*
* Please note: all regular fields are already copied!
*
* @internal
* @param array &$contact already copied fields according to the mapping
* @param array $data eGW contact data
*/
function _posixaccount2egw(&$contact,$data)
{
static $shadowExpireNow;
if (!isset($shadowExpireNow)) $shadowExpireNow = floor((time()-date('Z'))/86400);
// exclude expired or deactivated accounts
if (isset($data['shadowexpire']) && $data['shadowexpire'][0] <= $shadowExpireNow)
{
return false;
}
}
/**
* Special handling for mapping data of the mozillaAbPersonAlpha objectclass to eGW contact
*

View File

@ -114,16 +114,6 @@ class addressbook_so
*/
var $memberships;
/**
* LDAP searches only a limited set of attributes for performance reasons,
* you NEED an index for that columns, ToDo: make it configurable
* minimum: $this->columns_to_search = array('n_family','n_given','org_name','email');
*/
var $ldap_search_attributes = array(
'n_family','n_middle','n_given','org_name','org_unit',
'adr_one_location','adr_two_location','note',
'email','mozillasecondemail','uidnumber',
);
/**
* In SQL we can search all columns, though a view make on real sense
*/
@ -232,8 +222,7 @@ class addressbook_so
{
$this->contact_repository = 'ldap';
$this->somain = new addressbook_ldap();
$this->columns_to_search = $this->ldap_search_attributes;
$this->columns_to_search = $this->somain->search_attributes;
}
else // sql or sql->ldap
{
@ -250,12 +239,13 @@ class addressbook_so
{
$this->grants = $this->get_grants($this->user,$contact_app);
}
if ($this->account_repository == 'ldap' && $this->contact_repository == 'sql')
if ($this->account_repository != 'sql' && $this->contact_repository == 'sql')
{
if ($this->account_repository != $this->contact_repository)
{
$this->so_accounts = new addressbook_ldap();
$this->account_cols_to_search = $this->ldap_search_attributes;
$class = 'addressbook_'.$this->account_repository;
$this->so_accounts = new $class();
$this->account_cols_to_search = $this->so_accounts->search_attributes;
}
else
{

View File

@ -319,8 +319,7 @@ class accounts
$account_search[$serial]['total'] = $this->total;
}
// search via ldap backend
elseif ($this->config['account_repository'] == 'ldap')
//not correct for php<5.1 elseif ((method_exists($this,'search')) // implements its on search function ==> use it
elseif (method_exists($this->backend, 'search')) // implements its on search function ==> use it
{
$account_search[$serial]['data'] = $this->backend->search($param);
$account_search[$serial]['total'] = $this->total = $this->backend->total;
@ -1048,11 +1047,11 @@ class accounts
{
if ($instance->get_type($account_id) == 'u')
{
$account['memberships'] = $instance->backend->memberships($account_id);
if (!isset($account['memberships'])) $account['memberships'] = $instance->backend->memberships($account_id);
}
else
{
$account['members'] = $instance->backend->members($account_id);
if (!isset($account['members'])) $account['members'] = $instance->backend->members($account_id);
}
egw_cache::setInstance(__CLASS__, 'account-'.$account_id, $account, self::READ_CACHE_TIMEOUT);
}

File diff suppressed because it is too large Load Diff

View File

@ -638,7 +638,7 @@ class accounts_ldap
function search($param)
{
//echo "<p>accounts_ldap::search(".print_r($param,true)."): ".microtime()."</p>\n";
$account_search = &$this->cache['account_search'];
$account_search =& accounts::$cache['account_search'];
// check if the query is cached
$serial = serialize($param);
@ -919,7 +919,7 @@ class accounts_ldap
if ($which == 'account_lid' && $account_type !== 'u') // groups only support account_lid
{
$sri = ldap_search($this->ds, $this->group_context, '(&(cn=' . $name . ')(objectclass=posixgroup))');
$sri = ldap_search($this->ds, $this->group_context, '(&(cn=' . $name . ')(objectclass=posixgroup))', array('gidNumber'));
$allValues = ldap_get_entries($this->ds, $sri);
if (@$allValues[0]['gidnumber'][0])
@ -936,7 +936,7 @@ class accounts_ldap
return False;
}
$sri = ldap_search($this->ds, $this->user_context, '(&('.$to_ldap[$which].'=' . $name . ')(objectclass=posixaccount))');
$sri = ldap_search($this->ds, $this->user_context, '(&('.$to_ldap[$which].'=' . $name . ')(objectclass=posixaccount))', array('uidNumber'));
$allValues = ldap_get_entries($this->ds, $sri);

View File

@ -36,42 +36,22 @@ class auth_ads implements auth_backend
return False;
}
if(!$ldap = @ldap_connect($GLOBALS['egw_info']['server']['ads_host']))
{
//echo "<p>Failed connecting to ADS server '".$GLOBALS['egw_info']['server']['ads_host']."' for authenication, execution stopped</p>\n";
$GLOBALS['egw']->log->message('F-Abort, Failed connecting to ADS server for authenication, execution stopped');
$GLOBALS['egw']->log->commit();
return False;
}
//echo "<p>Connected to LDAP server '".$GLOBALS['egw_info']['server']['ads_host']."' for authenication</p>\n";
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
$adldap = accounts_ads::get_adldap();
// bind with username@ads_domain, only if a non-empty password given, in case anonymous search is enabled
if(empty($passwd) || !@ldap_bind($ldap,$username.'@'.$GLOBALS['egw_info']['server']['ads_domain'],$passwd))
if(empty($passwd) || !$adldap->authenticate($username, $passwd))
{
//echo "<p>Cant bind with '$username@".$GLOBALS['egw_info']['server']['ads_domain']."' with PW '$passwd' !!!</p>\n";
//error_log(__METHOD__."('$username', ".(empty($passwd) ? "'') passwd empty" : '$passwd) adldap->authenticate() returned false')." --> returning false");
return False;
}
//echo "<p>Bind with '$username@".$GLOBALS['egw_info']['server']['ads_domain']."' with PW '$passwd'.</p>\n";
$attributes = array('samaccountname','givenName','sn','mail','homeDirectory');
$filter = "(samaccountname=$username)";
// automatic create dn from domain: domain.com ==> DC=domain,DC=com
$base_dn = array();
foreach(explode('.',$GLOBALS['egw_info']['server']['ads_domain']) as $dc)
if (($allValues = $adldap->user()->info($username, $attributes)))
{
$base_dn[] = 'DC='.$dc;
$allValues[0]['objectsid'][0] = $adldap->utilities()->getTextSID($allValues[0]['objectsid'][0]);
}
$base_dn = implode(',',$base_dn);
//error_log(__METHOD__."('$username', \$passwd) allValues=".array2string($allValues));
//echo "<p>Trying ldap_search(,$base_dn,$filter,".print_r($attributes,true)."</p>\n";
$sri = ldap_search($ldap, $base_dn, $filter, $attributes);
$allValues = ldap_get_entries($ldap, $sri);
//_debug_array($allValues);
if ($allValues['count'] > 0)
if ($allValues && $allValues['count'] > 0)
{
if($GLOBALS['egw_info']['server']['case_sensitive_username'] == true)
{
@ -92,6 +72,8 @@ class auth_ads implements auth_backend
}
if ($GLOBALS['egw_info']['server']['auto_create_acct'])
{
$GLOBALS['auto_create_acct']['account_id'] = accounts_ads::sid2account_id($allValues[0]['objectsid'][0]);
// create a global array with all availible info about that account
foreach(array(
'givenname' => 'firstname',
@ -102,6 +84,7 @@ class auth_ads implements auth_backend
$GLOBALS['auto_create_acct'][$acct_name] =
translation::convert($allValues[0][$ldap_name][0],'utf-8');
}
//error_log(__METHOD__."() \$GLOBALS[auto_create_acct]=".array2string($GLOBALS['auto_create_acct']));
return True;
}
}
@ -109,8 +92,38 @@ class auth_ads implements auth_backend
return False;
}
/**
* changes password
*
* @param string $old_passwd must be cleartext
* @param string $new_passwd must be cleartext
* @param int $account_id account id of user whose passwd should be changed
* @return boolean true if password successful changed, false otherwise
*/
function change_password($old_passwd, $new_passwd, $_account_id=0)
{
return false; // Cant change passwd in ADS
if (!($adldap = accounts_ads::get_adldap()) || !($adldap->getUseSSL() || $adldap->getUseTLS()))
{
error_log(__METHOD__."('$old_passwd', '$new_passwd', $account_id) adldap=".array2string($adldap)." returning false");
return false; // Cant change passwd in ADS
}
if(!$account_id || $GLOBALS['egw_info']['flags']['currentapp'] == 'login')
{
$admin = false;
$username = $GLOBALS['egw_info']['user']['account_lid'];
}
else
{
$admin = true;
$username = $GLOBALS['egw']->accounts->id2name($account_id);
}
// Check the old_passwd to make sure this is legal
if(!$admin && !$adldap->authenticate($username, $old_passwd))
{
//error_log(__METHOD__."() old password '$old_passwd' for '$username' is wrong!");
return false;
}
return $adldap->user()->password($username, $new_passwd);
}
}

View File

@ -96,10 +96,14 @@ else
if($_POST['delete_all'])
{
/* Now, clear out existing tables */
foreach(array($GLOBALS['egw_setup']->accounts_table,$GLOBALS['egw_setup']->prefs_table,$GLOBALS['egw_setup']->acl_table,'egw_access_log') as $table)
foreach(array($GLOBALS['egw_setup']->accounts_table,$GLOBALS['egw_setup']->acl_table,'egw_access_log') as $table)
{
$GLOBALS['egw_setup']->db->delete($table,'1=1',__LINE__,__FILE__);
}
// keep default and forced prefs from installed apps
$GLOBALS['egw_setup']->db->delete($GLOBALS['egw_setup']->prefs_table,'preferences_owner NOT IN (-1,-2)',__LINE__,__FILE__);
// remove accounts from addressbook
$GLOBALS['egw_setup']->db->delete('egw_addressbook','account_id IS NOT NULL',__LINE__,__FILE__);
}
/* Create the demo groups */
$defaultgroupid = (int)$GLOBALS['egw_setup']->add_account('Default','Default','Group',False,False);

View File

@ -999,7 +999,7 @@ class setup
{
// load the configuration from the database
foreach($this->db->select($this->config_table,'config_name,config_value',
"config_name LIKE 'ldap%' OR config_name LIKE 'account_%' OR config_name LIKE '%encryption%' OR config_name='auth_type'",
"config_name LIKE 'ads%' OR config_name LIKE 'ldap%' OR config_name LIKE 'account_%' OR config_name LIKE '%encryption%' OR config_name='auth_type'",
__LINE__,__FILE__) as $row)
{
$GLOBALS['egw_info']['server'][$row['config_name']] = $config[$row['config_name']] = $row['config_value'];

View File

@ -276,7 +276,7 @@
<select name="newsettings[account_repository]">
<option value="sql"{selected_account_repository_sql}>SQL</option>
<option value="ldap"{selected_account_repository_ldap}>LDAP</option>
<!--<option value="contacts"{selected_account_repository_contacts}>Contacts - EXPERIMENTAL</option>-->
<option value="ads"{selected_account_repository_ads}>Active Directory</option>
</select>
</td>
</tr>
@ -539,6 +539,29 @@
<td>{lang_Domain_name}:</td>
<td><input name="newsettings[ads_domain]" value="{value_ads_domain}" size="40" /></td>
</tr>
<tr class="row_off">
<td>{lang_Admin_user}<br/>{lang_optional,_if_only_authentication_AND_anonymous_search_is_enabled}:</td>
<td><input name="newsettings[ads_admin_user]" value="{value_ads_admin_user}" size="40" /></td>
</tr>
<tr class="row_on">
<td>{lang_Password}:</td>
<td><input type="password" name="newsettings[ads_admin_passwd]" value="{value_ads_admin_passwd}" size="40" /></td>
</tr>
<tr class="row_off">
<td>{lang_use_TLS_or_SSL} {lang_required_to_change_passwords}<br/>
<a href="http://adldap.sourceforge.net/wiki/doku.php?id=ldap_over_ssl" target="_blank">{lang_needs_extra_configuration_on_DC_and_webserver!}</a>:</td>
<td>
<select name="newsettings[ads_connection]">
<option value="">{lang_No}</option>
<option value="tls"{selected_ads_connection_tls}>TLS</option>
<option value="ssl"{selected_ads_connection_ssl}>SSL</option>
</select>
</td>
</tr>
<tr class="row_on">
<td>{lang_Context_to_create_users}:<br/>{lang_eg._"CN=Users,DC=domain,DC=com"_for_ADS_domain_"domain.com"}</td>
<td><input name="newsettings[ads_context]" value="{value_ads_context}" size="80" /></td>
</tr>
<tr class="row_off">
<td colspan="2">&nbsp;</td>