Kerberos realm authentication using LDAP authorization via MS Windows Active Directory
Some details on the SAMBA configuration. Original Samba, ADS, Winbind documention was found http://gentoo-wiki.com/HOWTO_Adding_a_Samba_Server_into_an_existing_AD_Domain.
This particular installation of Samba requires a few additional packages and features to ensure that Active Directory authentication works properly. In this first step we add the necessary use flags to the Gentoo Portage package system.
%> echo net-fs/samba ads acl ldap kerberos pam winbind >> /etc/portage/package.use
%> echo net-nds/openldap sasl samba kerberos >> /etc/portage/package.use
%> echo dev-libs/cyrus-sasl authdaemond -java kerberos pam ldap sasl >> /etc/portage/package.use
Next we set our use flags prior to emerging the packages Samba requires for our AD authentication type.
%> USE="kerberos acl caps cups ipv6 ldap pam python readline winbind ads async automount doc examples fam quotas selinux swat syslog" /
emerge mit-krb5 pam_krb5 pam_ldap openldap nss_ldap openssl cyrus-sasl ntp samba -va
Because our primary method of authenticating users is to use the MIT KRB5 protocol we must ensure the configuration is correct which can be set by editing the /etc/krb5.conf. Below is a working example using the UTAH.EDU Kerberos Realm and specifying the allowed domains to use the for the authenticating sources.
[libdefaults]
default_realm = EXAMPLE.COM
[realms]
EXAMPLE.COM = {
kdc = kdc.example.com
}
[domain_realm]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM
example-2.com = EXAMPLE.COm
[loggin]
default = FILE:/var/log/krb5.log
[appdefaults]
pam = {
ticket_lifetime = 365d
renew_lifetime = 365d
forwardable = true
proxiable = false
retain_after_close = true
minimum_uid = 0
}
Here we configure the /etc/nsswitch.conf file to allow our Active Directory as an allowed source for looking up domain user accounts using the winbind service daemon which we just installed. As you can also see in the example the nis service is not used because we did not want to run an additional service for user permissions etc.
passwd: compat winbind
shadow: compat
group: compat winbind
# passwd: db files nis
# shadow: db files nis
# group: db files nis
hosts: files dns wins
networks: files dns
services: db files
protocols: db files
rpc: db files
ethers: db files
netmasks: files
netgroup: files
bootparams: files
automount: files
aliases: files
Now for the bulk of our setup. The /etc/samba/smb.conf file presented here has several features to take note of; (I tried to group similar configuration directives to make it easier to make modifications to later if needed). For details on the available options used in the example below please visit samba.org.
[global]
workgroup = EXAMPLE
realm = EXAMPLE.COM
server string = fs.example.com
netbios name = fs
password server = *
encrypt passwords = true
security = ads
lanman auth = no
ntlm auth = no
os level = 20
allow trusted domains = yes
auth methods = winbind
interfaces = eth0, lo
bind interfaces only = yes
socket options = TCP_NODELAY
hosts allow = 192.168.0.0/24, 127.0.0.1
hosts deny = 0.0.0.0/0
log level = 5 ads:3 auth:3 sam:3
log level = 1
log file = /var/log/samba/log.%m
max log size = 50
client signing = yes
client schannel = no
client use spnego = yes
client lanman auth = yes
client NTLMv2 auth = yes
client plaintext auth = no
preferred master = no
local master = no
domain master = no
wins proxy = no
dns proxy = No
obey pam restrictions = yes
template shell = /bin/bash
acl map full control = false
acl compatibility = Auto
nt acl support = yes
inherit permissions = yes
inherit owner = yes
create mask = 0755
inherit acls = yes
inherit owner = yes
inherit permissions = yes
map acl inherit = yes
template homedir = /home/Authenticated Users/%U
winbind uid = 1000-2000000
winbind gid = 500-2000000
winbind separator = +
winbind enum users = yes
winbind enum groups = yes
winbind nested groups = yes
winbind use default domain = yes
winbind offline logon = true
winbind nss info = rfc2307
idmap uid = 1000-2000000
idmap gid = 500-2000000
idmap domains = SCL
idmap config SCL:backend = ad
idmap config SCL:default = yes
idmap config SCL:schema_mode = rfc2307
idmap config SCL:range = 1000 - 300000000
[classes]
comment = Class software
browsable = yes
writeable = no
create mask = 0022
force create mode = 0022
directory mask = 0022
force directory mode = 0022
inherit permissions = yes
valid users = shares @EXAMPLE+Users
path = /fs/classes
[staff]
comment = Staff folders
browsable = yes
writeable = yes
create mask = 0220
force create mode = 0220
directory mask = 0220
force directory mode = 0220
valid users = @EXAMPLE+Labstaff
path = /fs/staff
[images]
comment = images
browsable = yes
writeable = yes
create mask = 0770
force create mode = 0770
directory mask = 0770
force directory mode = 0770
inherit permissions = yes
valid users = SCL+image, @EXAMPLE+PCGroup
path = /fs/images
[homes]
comment = %U Home directory
browsable = yes
writeable = yes
create mask = 0220
force create mode = 0220
directory mask = 0220
force directory mode = 0220
inherit permissions = yes
valid users = @EXAMPLE+PCGroup
path = /fs/labrats/%U
Next step is to tell our server how to authenticate users. This is where modifications to the PAM stack is needed. If this is not configured to use the pam_winbind.so authentication module in conjunction with the pam_krb5.so module only local authentication to the passwd database may be used.
#%PAM-1.0
auth required pam_mount.so
auth required pam_env.so
auth sufficient pam_winbind.so
auth sufficient pam_unix.so try_first_pass likeauth nullok
auth sufficient pam_krb5.so use_first_pass
auth required pam_deny.so
account required pam_unix.so
account sufficient pam_krb5.so ignore_root
account sufficient pam_winbind.so
password optional pam_krb5.so
password required pam_mount.so use_authtok
password required pam_cracklib.so difok=2 minlen=8 dcredit=2 ocredit=2 try_first_pass retry=3
password sufficient pam_unix.so try_first_pass use_authtok nullok md5 shadow
password required pam_deny.so
session required pam_mkhomedir.so umask=0022 skel=/etc/skel/
session required pam_limits.so
session required pam_unix.so
session optional pam_mount.so use_authtok
session optional pam_krb5.so
Now that the necessary software, libraries and preliminary configuration has been completed, we will now need to join the server to the domain as a 'member server' role. Below are the steps to take for this process.
Because Kerberos authentication is to be used we must first obtain a valid TGT for the domain administrator account (if the krb5.conf file was not configured this will fail).
%> kinit [email protected]
%> klist
Ticket cache: FILE:/tmp/krb5cc_0
Default principal: [email protected]
Valid starting Expires Service principal
05/28/08 05:39:23 05/28/08 15:39:23 krbtgt/[email protected]
renew until 06/04/08 05:39:23
Next step is to use the 'net' tool which accompanies the Samba package. Here we use it to join the domain with our recently kerberos authenticated domain administrator account.
%> net ads join [email protected]
[email protected]'s password:
Using short domain name -- EXAMPLE
Joined 'FS' to realm 'EXAMPLE.COM'
Because we would like this service to startup each time the machine turns on we must add it to the necessary run levels.
%> rc-update add samba boot
To start and stop the service the following command will work in Gentoo Linux distributions.
%> /etc/init.d/samba start|stop|restart
Because verification of the above steps is essential to this file sharing service I am going to touch on some of the various tools which can be used to perform this step.
Below are examples of using tools from Samba, winbind and existing linux installations for enumerating local and domain group accounts. These are helpful if you need to quickly test the domain membership server role.
- Using the net command `%> net ads groups`
- Using winbind `%> wbinfo -g`
- Using getent `%> getent group`
Here we perform some of the same tests except here we can view existing Active Directory user accounts.
- Using the net command `%> net ads users`
- Using winbind `%> wbinfo -u`
- Using getent `%> getent passwd`
Another good way to view extended domain membership information in terms of trusted authentication sources is the use of the following command. (example output provided).
%> wbinfo -m
Using the samba net tool we can view status information in terms of the machines details available as a member server within the active directory OU container.
%> net ads status
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
objectClass: computer
cn: valhalla
distinguishedName: CN=fs,CN=Computers,DC=example,DC=com
instanceType: 4
whenCreated: 20080522170259.0Z
whenChanged: 20080528145553.0Z
uSNCreated: 3833727
uSNChanged: 3921781
name: valhalla
objectGUID: 0fcc649c-017c-4cdc-b9df-d8c2f2bba965
userAccountControl: 69632
badPwdCount: 0
codePage: 0
countryCode: 0
badPasswordTime: 0
lastLogoff: 0
lastLogon: 128564610242815317
localPolicyFlags: 0
pwdLastSet: 0
primaryGroupID: 515
objectSid: S-1-5-21-2868754479-89028146-2101856903-111475
accountExpires: 9223372036854775807
logonCount: 282
sAMAccountName: fs$
sAMAccountType: 805306369
dNSHostName: fs.example.com
servicePrincipalName: HOST/fs.example.com
servicePrincipalName: HOST/FS
objectCategory: CN=Computer,CN=Schema,CN=Configuration,DC=example,DC=com
isCriticalSystemObject: FALSE
lastLogonTimestamp: 128559493793593750
Once Samba has been installed, configured, joined to the domain and tested you should now test authentication of domain accounts. The wbinfo tool which accompanies the winbind package allows for testing from the local server.
%> wbinfo --krb5auth=username%password
plaintext kerberos password authentication for [username%password] succeeded (requesting cctype: FILE)
credentials were put in: FILE:/tmp/krb5cc_0
Next you will want to ensure that valid accounts on the domain may authenticate from windows clients on your network. Here is an example that can be used from the MS-Dos prompt for testing.
%> net use x: \\fs\homes /user:USERNAME
The command completed successfully
Next we should test connectivity from linux clients to ensure that they may also use Active Directory accounts to map network shares from our Samba server.
%> mount -t cifs -o username=USERNAME //fs.example.com/homes /mnt/samba
Depending on the log level used when configuring the global section of the /etc/samba/smb.conf, viewing the contents of the various logs comes in handy when tracking down a problem with the installation, configuration or authentication of AD accounts.
- /var/log/samba/log.winbind
- /var/log/samba/log.winbind-idmap
- /var/log/samba/log.smb
There are several known problems when dealing with Linux to Windows compatibilities. As of this writing the dated POSIX extensions necessary for linux compatible accounts (RFC2307; uid, gid, default shell & home directory) should no longer be an issue with the introduction of the Windows 2008 Server OS. However some
Because you may run into problems with Windows domain user accounts failing the necessary UID/GID to SID mapping from Windows to Linux here are several methods of detecting the problem. Generally the problem illustrated here stems from creation of user accounts within Active Directory containing a 'space' character within the DN of the user account object.
First step that needs to take place is to obtain the users SID located within Active Directory.
%> wbinfo -n USERNAME
S-1-5-21-2868754479-89028146-2101856903-111473 User (1)
Once we have obtained the assigned account SID from Active Directory for the users account we can use that to obtain the UID/GID mapping by issuing the wbinfo tool the optional -S and/or -Y options as shown below in these failed examples.
%> wbinfo -i USERNAME
Could not get info for user USERNAME
%> wbinfo -S S-1-5-21-2868754479-89028146-2101856903-111473 User (1)
Could not convert sid S-1-5-21-2868754479-89028146-2101856903-111473 to uid
%> wbinfo -Y S-1-5-21-2868754479-89028146-2101856903-111473 User (1)
Could not convert sid S-1-5-21-2868754479-89028146-2101856903-111473 to gid
The two errors listed here can be verified by obtaining the relevant data from '/var/log/samba/log.winbindd'. Here is an example output indicating a problem with the DN of the account.
[2008/05/28 09:50:04, 10] nsswitch/winbindd_cache.c:cache_retrieve_response(2300)
Retrieving response for pid 24973
[2008/05/28 09:50:04, 5] nsswitch/winbindd_async.c:winbindd_sid2uid_recv(347)
sid2uid returned an error
Similar data can also be found in '/var/log/samba/log.winbindd-idmap'.
[2008/05/28 09:50:51, 10] nsswitch/winbindd_dual.c:child_process_request(479)
process_request: request fn DUAL_SID2UID
[2008/05/28 09:50:51, 3] nsswitch/winbindd_async.c:winbindd_dual_sid2uid(374)
[24634]: sid to uid S-1-5-21-2868754479-89028146-2101856903-111473
[2008/05/28 09:50:51, 10] nsswitch/idmap_util.c:idmap_sid_to_uid(105)
idmap_sid_to_uid: sid = [S-1-5-21-2868754479-89028146-2101856903-111473]
[2008/05/28 09:50:51, 10] nsswitch/idmap_util.c:idmap_sid_to_uid(125)
sid [S-1-5-21-2868754479-89028146-2101856903-111473] not mapped to an uid [2,1,2213796440]
I had a hard time finding methods of resolving this problem so to detail a solution here are the requirements, and steps to take.
As mentioned in RFC2307 linux user accounts are POSIX compliant and Microsoft Windows pre- 2003 Active Directory installations did not contain the necessary schema attributes necessary for interoperability. Here is a simple list of required schema objects necessary for POSIX (RFC2307) compliance in MS Window Active Directory objects.
- cn (Common name or username)
- uidNumber (UID for POSIX Unix accounts)
- gidNumber (GID for POSIX Unix accounts)
- loginShell (Default login shell for domain users)
- msSFUHomeDirectory (Default Home directory for domain users)
This verification is not necessary is using Windows 2003 Server or later.
When this server was first setup the need to fix user accounts en masse' was necessary as individual testing, and repair of 1000's of user accounts was impractical. A simple perl script was written to detect (using the 'wbinfo' commands previously mentioned) to find and repair invalid DN's within any specified OU in Active Directory.
- Please note that Enterprise Domain Authentication is required for this tool to function properly as modifications of the DN per user object are limited to these permissions.
=#### Wizard example usage with output ####=
This first example of usage with the tool is recommended as it will allow you to make modifications to one 'known' non POSIX compliant account at a time. This is great for testing accounts and tool functionality prior to running a fully automated test on any Active Directory OU.
NOTE: In the example shown below you will see the 'AcceptSecurityContext' error as a result of the importation of the ldif files for the non compliant RCF2307 accounts because in this example we are using a read-only access user type to test the application and show you examples. An enterprise domain administrator account is required for these modifications to work.
%> ./UID2SID.pl
Enter domain : EXAMPLE.COM
Enter username : username
Enter password : password
Enter login name of user : testaccount
Enter OU [Users]:
Enter object [USER]:
Searching for 'testaccount', please wait...
SEARCH RESULTS:
Found: '1' record(s) matching 'testaccount' in 'EXAMPLE.COM'
Found: '1' record(s) matching 'testaccount' that are not RFC2307 compliant
INVALID ACCOUNTS:
CN=Test Account
PROCESSING:
Importing: modify-zz-attribs-10.08.2009-testaccount.ldif
Importing: modify-dn-10.08.2009-testaccount.ldif
NON-POSIX FIX RESULTS:
ldap_bind: Invalid credentials (49)
additional info: 80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 525, vece
ldap_bind: Invalid credentials (49)
additional info: 80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 525, vece
=#### Automated example usage with output ####=
The tool has the ability to run in automated mode where it searches a specified OU in Active Directory for all available user accounts which it then tests for the SID to UID/GID mapping (RFC2307 POSIX compatibility) and if found generates a new .ldiff file which is then used to delete and create the account with a new DN.
%> ./UID2SID.pl --domain=DOMAIN.COM --user=USERNAME --pass=PASSWORD --ou="CN=Users,DC=DOMAIN,DC=COM" --obj=user
Searching for 'user' in 'CN=Users,DC=example,DC=com, please wait...
BIND DN: CN=domain-admin,CN=Users,DC=example,DC=com
BASE DN: CN=Users,DC=example,DC=com
Object Class: user
SEARCH RESULTS:
Found: '44' 'user' records in 'CN=Users,DC=example,DC=com'
Found: '6' 'user' records that are not RFC2307 compliant
INVALID ACCOUNTS:
CN=Ron
CN=mclame
CN=Joni
CN=Tonga
CN=Caroljean
CN=Matt
PROCESSING:
modify-dn-06.23.2008-MIrsik.ldif
PROCESSING:
modify-zz-attribs-06.23.2008-tongak.ldif
PROCESSING:
modify-dn-06.23.2008-mclame.ldif
PROCESSING:
modify-dn-06.23.2008-rhansen.ldif
PROCESSING:
modify-dn-06.23.2008-tongak.ldif
PROCESSING:
modify-dn-06.23.2008-jclayton.ldif
PROCESSING:
modify-dn-06.23.2008-chanson.ldif
PROCESSING:
modify-zz-attribs-06.23.2008-rhansen.ldif
PROCESSING:
modify-zz-attribs-06.23.2008-jclayton.ldif
PROCESSING:
modify-zz-attribs-06.23.2008-chanson.ldif
PROCESSING:
modify-zz-attribs-06.23.2008-MIrsik.ldif
PROCESSING:
modify-zz-attribs-06.23.2008-mclame.ldif
This section is a duplicate of the UID/GID to SID mapping originally mentioned but this example shows a valid mapping due to the POSIX compliance of the DN from Windows to Linux.
%> wbinfo -i USERNAME
USERNAME:*:1000015:513:USERNAME:/Users/Authenticated Users/USERNAME:/bin/false
%> wbinfo -n USERNAME
S-1-5-21-2868754479-89028146-2101856903-88486 User (1)
%> wbinfo -S S-1-5-21-2868754479-89028146-2101856903-88486
1000015
This tool was written in perl and requires the wbinfo and ldapsearch tools.
#!/usr/bin/perl
#
# Simple POSIX account fix for non-RFC2307 compliant user
# accounts in active directory or openldap directories
#
# [email protected]
#
# last revision: 08.26.2008
#
# grab our modules
use Getopt::Long;
use POSIX qw( strftime );
use Data::Dumper;
# temporary file storage directory
my $dir = ".UID2SID";
my $PATH = "/usr/bin";
# some defaults if schema attributes are missing
$uid = 100001;
$gid = 513;
$homedir = "/home/Authenticated Users";
$shell = "/bin/false";
# some globals
my ( $domain,
$user,
$pass,
$ou,
$container,
$obj,
$date );
# menu system data
GetOptions( "domain=s" => \$domain,
"user=s" => \$user,
"pass=s" => \$pass,
"ou=s" => \$ou,
"obj=s" => \$obj,
"verbose" => \$verbose,
"help" => \$help);
# initialize script sub-routines
$date = &GetDate;
if( ( defined( $domain ) ) && ( defined( $user ) ) && ( defined( $pass ) ) && ( defined( $ou ) ) && ( defined( $obj ) ) ) {
&MainSub( $domain, $user, $pass, $ou, $obj, $date );
} elsif( defined( $help ) ) {
&ShowSyntax;
} else {
&WizardWiz;
}
# get date
sub GetDate
{
return strftime( '%m.%d.%Y', localtime() );
}
# handle directory and temp files
sub CreateDir
{
if( -d $dir ) { return; }
my $dirc = "mkdir $dir";
`$dirc`;
return;
}
# Initialize command
sub GetLDAPQuery( $domain, $user, $pass, $ou, $obj, $date )
{
print "Searching for '$obj' in '$ou', please wait...\n";
my $cmd = "$PATH/ldapsearch -v -x -h $domain -D \"$user\" -w $pass -b \"$ou\" -s sub \"objectclass=$obj\"";
print "\tBIND DN: CN=$user,$ou\n";
print "\tBASE DN: $ou\n";
print "\tObject Class: $obj\n";
print "SEARCH RESULTS:\n";
my @results = `$cmd >> $dir/ldap-lookup-$date.log`;
if( defined( $verbose ) ) {
print "CMD: " . $cmd . "\n";
}
return @results;
}
# Initialize command
sub GetLDAPQueryEasy( $domain, $user, $pass, $nameOne, $date )
{
#break up domain
my @d = split(/\./, $domain);
my $ou = '';
for( my $i = 0; $i <= $#d; $i++ ) {
$ou .= "DC=" . $d[$i] . ",";
}
$ou = substr($ou,0,-1);
print "Searching for '$nameOne', please wait...\n";
my $cmd = "$PATH/ldapsearch -x -h $domain -D \"$user\" -w $pass -b \"$ou\" \"(sAMAccountName=$nameOne)\"";
print "SEARCH RESULTS:\n";
my @results = `$cmd >> $dir/ldap-lookup-$date.log`;
if( defined( $verbose ) ) {
print "CMD: " . $cmd . "\n";
}
return @results;
}
# Same search but limit to user
sub GetLDAPQueryAttribs( $domain, $uname, $pass, $ou, $obj, $date, $tmpName )
{
$domain = shift; $uname = shift; $pass = shift; $ou = shift; $obj = shift; $date = shift; $tmpName = shift;
my $cmd = "$PATH/ldapsearch -x -h $domain -D \"$uname\" -w $pass -b \"$ou\" \"(givenName=$tmpName)\" dn cn msSFUName msSFUHomeDirectory unixHomeDirectory uidNumber gidNumber loginShell sAMAccountName";
my @results = `$cmd >> $dir/ldap-modify-$date-$tmpName.log`;
if( defined( $verbose ) ) {
print $cmd . "\n";
}
return @results;
}
# Same search but limit to user through wizard
sub GetLDAPQueryAttribsEasy( $domain, $uname, $pass, $nameOne, $ou, $obj, $date, $tmpName )
{
$domain = shift; $uname = shift; $pass = shift; $nameOne = shift; $ou = shift; $obj = shift; $date = shift; $tmpName = shift;
my $cmd = "$PATH/ldapsearch -x -h $domain -D \"$uname\" -w $pass -b \"$ou\" \"(sAMAccountName=$nameOne)\" dn cn msSFUName msSFUHomeDirectory unixHomeDirectory uidNumber gidNumber loginShell sAMAccountName";
my @results = `$cmd >> $dir/ldap-modify-$date-$tmpName.log`;
if( defined( $verbose ) ) {
print $cmd . "\n";
}
return @results;
}
# Fix for CN, DN & distinguishedName attribs
sub GetLDAPQueryCNDNAttribs( $domain, $uname, $pass, $ou, $obj, $date, $tmpName )
{
$domain = shift; $uname = shift; $pass = shift; $ou = shift; $obj = shift; $date = shift; $tmpName = shift; my $a = '';
if( $tmpName =~ /^(\w+)\s(\w+)$/i ) { $a = $1 . $2; }
my $cmd = "$PATH/ldapsearch -x -h $domain -D \"$uname\" -w $pass -b \"$ou\" \"(cn=$tmpName)\" dn cn distinguishedName sAMAccountName >> $dir/ldap-modify-$date-" . $a . ".log";
my @results = `$cmd`;
if( defined( $verbose ) ) {
print "CMD: " . $cmd . "\n";
}
return @results;
}
# Fix for CN, DN & distinguishedName attribs
sub GetLDAPQueryCNDNAttribsEasy( $domain, $uname, $pass, $nameOne, $ou, $obj, $date, $tmpName )
{
$domain = shift; $uname = shift; $pass = shift; $nameOne = shift; $ou = shift; $obj = shift; $date = shift; $tmpName = shift; my $a = '';
if( $tmpName =~ /^(\w+)\s(\w+)$/i ) { $a = $1 . $2; }
my $cmd = "$PATH/ldapsearch -x -h $domain -D \"$uname\" -w $pass -b \"$ou\" \"(sAMAccountName=$nameOne)\" dn cn distinguishedName sAMAccountName >> $dir/ldap-modify-$date-" . $a . ".log";
my @results = `$cmd`;
if( defined( $verbose ) ) {
print "CMD: " . $cmd . "\n";
}
return @results;
}
# Same search but limit to user
sub SetLDAPAttribs( $domain, $uname, $ou, $container, $pass, $date, $file )
{
# ldapmodify -x -D "CN=Administrator,CN=Users,DC=SCL,DC=UTAH,DC=EDU" -w "pynn4cl3of3volution" -h scl.utah.edu -p 389 -f
my $domain = shift; my $uname = shift; my $ou = shift; my $container = shift; my $pass = shift; my $date = shift; my $file = shift;
#fix domain usernames
my @u = split(/\w+\\(\w+)/, $uname);
my $nuser = $u[1];
#break up domain
my @d = split(/\./, $domain);
my $ou1 = '';
for( my $i = 0; $i <= $#d; $i++ ) {
$ou1 .= "DC=" . $d[$i] . ",";
}
$ou1 = substr($ou1,0,-1);
if( $container eq "Users" ) { $container = "CN=".$container; } else { $container = "OU=".$container; }
my $cmd = "$PATH/ldapmodify -x -h $domain -D \"CN=$nuser,$container,$ou1\" -w $pass -f $dir/$file";
if( defined( $verbose ) ) {
print "CMD: " . $cmd . "\n";
}
`$cmd 2>> $dir/ldif-results-$date.log`;
}
# begin processing our ldif files
sub ProcessLDIF( $domain, $uname, $pass, $date )
{
my $domain = shift; my $uname = shift; my $pass = shift; my $date = shift; my $file = '';
opendir( DIR, $dir ) || die "ERROR: Could not open $dir for reading\n";
while( $file = readdir( DIR ) ) {
next if( -d $file );
if( $file =~ /\w+\.ldif$/i ) {
print "\tImporting: $file\n";
&SetLDAPAttribs( $domain, $uname, $ou, $container, $pass, $date, $file );
}
}
closedir( DIR );
return;
}
# parse our schema attributes
sub ParseResultsLDAPAttribs( $username )
{
my $username = shift;
if( $username =~ /^(\w+)\s(\w+)$/i ) { $username = $1 . $2; }
# open our temp file
open( RES, "$dir/ldap-modify-$date-$username.log" ) || die "ERROR: File could not be opened - $dir/ldap-modify-$date-$username.log\n";
flock( RES, 2 );
# initialize a new hash for results
my @res = ();
# begin looping over results
my( @lines ) = <RES>;
foreach my $line ( @lines ) {
chomp( $line );
my @userDataDetails = ();
# get our schema attributes into an array
if( $line =~ /^(dn)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(cn)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(msSFUName)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(unixHomeDirectory)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(msSFUHomeDirectory)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(uidNumber)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(gidNumber)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(loginShell)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(sAMAccountName)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
push( @res, @userDataDetails );
}
# clean it up
close( RES );
flock( RES, 8 );
unlink( "$dir/ldap-modify-$date-$username.log" );
return \@res;
}
# parse our schema attributes version 2
sub ParseResultsLDAPAttribsTwo( $username )
{
my $username = shift;
if( $username =~ /^(\w+)\s(\w+)$/i ) { $username = $1 . $2; }
# open our temp file
open( RES, "$dir/ldap-fixdn-$date-$username.log" ) || die "ERROR: File could not be opened - $dir/ldap-modify-$date-$username.log\n";
flock( RES, 2 );
# initialize a new hash for results
my @res = ();
# begin looping over results
my( @lines ) = <RES>;
foreach my $line ( @lines ) {
chomp( $line );
my @userDataDetails = ();
# get our schema attributes into an array
if( $line =~ /^(dn)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(cn)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(distinguishedName)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
if( $line =~ /^(sAMAccountName)\:\s(.*)$/i ) { push( @userDataDetails, $1 . "=" . $2 ); }
push( @res, @userDataDetails );
}
# clean it up
close( RES );
flock( RES, 8 );
unlink( "$dir/ldap-fixdn-$date-$username.log" );
return \@res;
}
# create an ldif file from ldap results on schema attributes
sub CreateLDIFOne
{
@array = shift; my $string = ''; my $name = ''; my $tmpName = '';
for my $x ( 0 .. $#array ) {
for my $y ( 0 .. $#{ $array[$x] } ) {
# fix for username shit
if( $array[$x][$y] =~ /sAMAccountName=(.*)/i ) {
$name = $1;
}
}
}
for my $x ( 0 .. $#array ) {
for my $y ( 0 .. $#{ $array[$x] } ) {
if( $array[$x][$y] ne '' ) {
if( $array[$x][$y] =~ /dn=CN=(.*)(,CN=.*)/i ) {
$string .= "dn: CN=$1$2\n"; my $tmpName = $1;
$string .= "changetype: modify\n";
}
# if( $array[$x][$y] =~ /cn=(.*)/i ) {
# $string .= "modify: cn\n";
# $string .= "cn: $name\n";
# }
if( $array[$x][$y] =~ /msSFUName=(.*)/i ) {
$string .= "modify: msSFUName\n";
$string .= "msSFUName: $name\n";
}
if( $array[$x][$y] =~ /unixHomeDirectory=(.*)\/($tmpName)/i ) {
$string .= "modify: unixHomeDirectory\n";
$string .= "unixHomeDirectory: $1/$name\n";
}
if( $array[$x][$y] =~ /msSFUHomeDirectory=(.*)\/($tmpName)/i ) {
$string .= "modify: msSFUHomeDirectory\n";
$string .= "msSFUHomeDirectory: $1/$name\n";
}
if( $array[$x][$y] =~ /uidNumber=(.*)/i ) {
$string .= "modify: uidNumber\n";
$string .= "uidNumber: $1\n";
}
if( $array[$x][$y] =~ /gidNumber=(.*)/i ) {
$string .= "modify: gidNumber\n";
$string .= "gidNumber: $1\n";
}
if( $array[$x][$y] =~ /loginShell=(.*)/i ) {
$string .= "modify: loginShell\n";
$string .= "loginShell: $1\n";
}
}
}
}
# one final check on string
$string = &CheckAttribs( $string, $name );
# make sure its not empty before creating ldif file
if( ( $string ne "" ) && ( $name ne "" ) ) {
open( RES, ">$dir/modify-zz-attribs-$date-$name.ldif" ) || die "ERROR: File could not be opened - $dir/modify-zz-attribs-$date-$name.ldif\n";
flock( RES, 2 );
print RES $string;
close( RES );
flock( RES, 8 );
}
}
# create an ldif file from ldap results on schema attributes
sub CreateLDIFTwo
{
@array = shift; my $string = ''; my $name = ''; my $tmpName = '';
for my $x ( 0 .. $#array ) {
for my $y ( 0 .. $#{ $array[$x] } ) {
# fix for username shit
if( $array[$x][$y] =~ /sAMAccountName=(.*)/i ) {
$name = $1;
}
}
}
# ensure we have something to fix in AD
if( ( defined( $name ) ) && ( $name ne "" ) ) {
for my $x ( 0 .. $#array ) {
for my $y ( 0 .. $#{ $array[$x] } ) {
if( $array[$x][$y] ne '' ) {
if( $array[$x][$y] =~ /dn=CN=(.*)(,CN=.*)/i ) {
$string .= "dn: CN=$1$2\n"; my $tmpName = $1; my $dn = $string;
$string .= "changetype: modrdn\n";
$string .= "newrdn: CN=$name\n";
$string .= "deleteoldrdn: 1\n";
}
}
}
}
}
# make sure its not empty before creating ldif file
if( ( $string ne "" ) && ( $name ne "" ) ) {
open( RES, ">$dir/modify-dn-$date-$name.ldif" ) || die "ERROR: File could not be opened - $dir/modify-dn-$date-$name.ldif\n";
flock( RES, 2 );
print RES $string;
close( RES );
flock( RES, 8 );
}
}
# apply defaults if some attributes are missing
sub CheckAttribs( $string, $name )
{
$string = shift; $name = shift;
if( $string !~ /msSFUName:/ ) { $string .= "modify: msSFUName\nmsSFUName: $name\n"; }
if( $string !~ /unixHomeDirectory:/ ) { $string .= "modify: unixHomeDirectory\nunixHomeDirectory: $homedir/$name\n"; }
if( $string !~ /msSFUHomeDirectory:/ ) { $string .= "modify: msSFUHomeDirectory\nmsSFUHomeDirectory: $homedir/$name\n"; }
if( $string !~ /uidNumber:/ ) { $string .= "modify: uidNumber\nuidNumber: " . $uid++ . "\n"; }
if( $string !~ /gidNumber:/ ) { $string .= "modify: gidNumber\ngidNumber: " . $gid . "\n"; }
if( $string !~ /loginShell:/ ) { $string .= "modify: loginShell\nloginShell: $shell\n"; }
return $string;
}
# parse our results from ldap
sub ParseResultsLDAP
{
# open our temp file
open( RES, "$dir/ldap-lookup-$date.log" ) || die "ERROR: File could not be opened - $dir/ldap-lookup-$date.log\n";
flock( RES, 2 );
# initialize a new hash for results
my $iznt = 0; my @schemaAttribs = ''; my %userDataDetails = (); my @data = '';
# begin looping over results
my( @lines ) = <RES>;
foreach my $line ( @lines ) {
chomp( $line );
# get our users into an array
if( $line =~ /^(cn)\:\s(.*)$/i ) {
push( @data, $2 );
}
}
# clean it up
close( RES );
flock( RES, 8 );
unlink( "$dir/ldap-lookup-$date.log" );
return @data;
}
# parse our winbind results and grab the invalids
sub ParseResultsWBINFO
{
# open our temp file
open( RES, "$dir/wbinfo-lookup-$date.log" ) || die "ERROR: File could not be opened - $dir/wbinfo-lookup-$date.log\n";
flock( RES, 2 );
# initialize a new hash for results
my @userData = '';
# begin looping over results
while( my $line = <RES> ) {
# look for our common name attribute
if( $line =~ /Could\snot\sget\sinfo\sfor\suser\s(.*)/i ) { push( @userData, $1 ); }
if( defined( $verbose ) ) {
print "\tERROR: " . $line . "\n";
}
}
# clean it up
close( RES );
flock( RES, 8 );
unlink( "$dir/wbinfo-lookup-$date.log" );
return @userData;
}
# attempt lookup of users using winbind
sub GetWBInfo( $user, $date )
{
$user = shift;
# grab ident of user data
my $cmd = "wbinfo -i $user 2>> $dir/wbinfo-lookup-$date.log";
my @results = `$cmd`;
if( defined( $verbose ) ) {
print "CMD: " . $cmd . "\n";
}
return @results;
}
# remove duplicate array values
sub RemDups
{
my %hash = map { $_ => 1 } @_;
my @unique = keys %hash;
return @unique;
}
# main application
sub MainSub( $domain, $user, $pass, $ou, $obj, $date )
{
# define our vars
my ( @wbInfo,
@userAttrib,
@invWBInfo,
@cnFix,
@cnFixRes ) = '';
my $uname = $user;
# create our directory data
&CreateDir;
# perform our ldap query
&GetLDAPQuery( \$domain, \$user, \$pass, \$ou, \$obj, \$date );
# parse the results
@userAttrib = &ParseResultsLDAP;
# ensure no duplicates are in results
@userAttrib = &RemDups( @userAttrib );
# ensure we found something
if( $#userAttrib == 0 ) {
print "\tERROR: Could not find any records... exiting\n";
exit;
}
# give a count to our user
print "\tFound: '$#userAttrib' '$obj' records in '$ou'\n";
# Now get our invalid accounts
for( my $i = 0; $i <= $#userAttrib; $i++ ) {
if( $userAttrib[$i] ne '' ) {
push( @wbInfo, &GetWBInfo( $userAttrib[$i], $date ) );
}
}
# Also provide fix for invalid CN, DN DistinguishedName attributes
for( my $j = 0; $j <= $#userAttrib; $j++ ) {
if( $userAttrib[$j] ne '' ) {
if( $userAttrib[$j] =~ /(\w+\s\w+)$/i ) {
$tmpName = $1;
push( @cnFix, $tmpName );
&GetLDAPQueryCNDNAttribs( $domain, $uname, $pass, $ou, $obj, $date, $tmpName );
}
}
}
# We should have any invalid entries here
@invWBInfo = &ParseResultsWBINFO;
# print count of invalid records
my $a = $#invWBInfo + $#cnFix;
if( $a >= 1 ) {
print "\tFound: '$a' '$obj' records that are not RFC2307 compliant\n";
# loop over results and attempt to set the required attribs
print "INVALID ACCOUNTS:\n";
my $tmpName = ''; my @results = ''; my @userDetails = ''; my %resultHash = ();
for( my $n = 0; $n <= $#invWBInfo; $n++ ) {
if( ( defined( $invWBInfo[$n] ) ) && ( $invWBInfo[$n] ne '' ) ) {
$tmpName = $invWBInfo[$n];
print "\tCN=$tmpName\n";
&GetLDAPQueryAttribs( $domain, $uname, $pass, $ou, $obj, $date, $tmpName );
push( @userDetails, &ParseResultsLDAPAttribs( $tmpName ) );
}
}
# loop over results with strange cn, dn attribs
for( my $m = 0; $m <= $#cnFix; $m++ ) {
if( ( defined( $cnFix[$m] ) ) && ( $cnFix[$m] ne '' ) ) {
print "\tCN=$cnFix[$m]\n";
$tmpName = $cnFix[$m];
if( $tmpName =~ /(\w+)\s(\w+)$/i ) {
$tmpName = $1 . $2;
}
push( @userDetails, &ParseResultsLDAPAttribs( $tmpName ) );
}
}
# clean it up a tad
sort( @userDetails );
# proceed to create an ldif from data
for( my $x = 0; $x <= $#userDetails; $x++ ) {
&CreateLDIFOne( $userDetails[$x] );
&CreateLDIFTwo( $userDetails[$x] );
}
# Lets finish it up
print "PROCESSING:\n";
&ProcessLDIF( $domain, $uname, $pass, $date );
# display log
if( defined( $verbose ) ) {
print "NON-POSIX FIX RESULTS:\n";
open( RES, "$dir/ldif-results-$date.log" ) || die "ERROR: File could not be opened - $dir/ldif-results-$date.log\n";
flock( RES, 2 );
my( @lines ) = <RES>;
foreach my $line ( @lines ) {
chomp( $line );
print "\t$line\n";
}
close( RES );
flock( RES, 8 );
}
} else {
print "\tFound: '0' '$obj' record(s) that are not RFC2307 compliant\n";
}
unlink( "$dir/ldif-results-$date.log" );
#`rm $dir/*`;
}
# main application
sub MainSubEasy( $domain, $user, $pass, $nameOne, $container, $obj, $date )
{
# define our vars
my ( @wbInfo,
@userAttrib,
@invWBInfo,
@cnFix,
@cnFixRes ) = '';
my $uname = $user;
#break up domain
my @d = split(/\./, $domain);
my $ou = '';
for( my $i = 0; $i <= $#d; $i++ ) {
$ou .= "DC=" . $d[$i] . ",";
}
$ou = substr($ou,0,-1);
# create our directory data
&CreateDir;
# perform our ldap query
&GetLDAPQueryEasy( \$domain, \$user, \$pass, \$nameOne, \$date );
# parse the results
@userAttrib = &ParseResultsLDAP;
# ensure no duplicates are in results
@userAttrib = &RemDups( @userAttrib );
# ensure we found something
if( $#userAttrib == 0 ) {
print "\tERROR: Could not find any records... exiting\n";
exit;
}
# give a count to our user
print "\tFound: '$#userAttrib' record(s) matching '$nameOne' in '$domain'\n";
# Now get our invalid accounts
for( my $i = 0; $i <= $#userAttrib; $i++ ) {
if( $userAttrib[$i] ne '' ) {
push( @wbInfo, &GetWBInfo( $userAttrib[$i], $date ) );
}
}
# Also provide fix for invalid CN, DN DistinguishedName attributes
for( my $j = 0; $j <= $#userAttrib; $j++ ) {
if( $userAttrib[$j] ne '' ) {
if( $userAttrib[$j] =~ /(\w+\s\w+)$/i ) {
$tmpName = $1;
push( @cnFix, $tmpName );
&GetLDAPQueryCNDNAttribsEasy( $domain, $uname, $pass, $nameOne, $ou, $obj, $date, $tmpName );
}
}
}
# We should have any invalid entries here
@invWBInfo = &ParseResultsWBINFO;
# print count of invalid records
my $a = $#invWBInfo + $#cnFix;
if( $a >= 1 ) {
print "\tFound: '$a' '$obj' records that are not RFC2307 compliant\n";
# loop over results and attempt to set the required attribs
print "INVALID ACCOUNTS:\n";
my $tmpName = ''; my @results = ''; my @userDetails = ''; my %resultHash = ();
for( my $n = 0; $n <= $#invWBInfo; $n++ ) {
if( ( defined( $invWBInfo[$n] ) ) && ( $invWBInfo[$n] ne '' ) ) {
$tmpName = $invWBInfo[$n];
print "\tCN=$tmpName\n";
&GetLDAPQueryAttribsEasy( $domain, $uname, $pass, $nameOne, $ou, $obj, $date, $tmpName );
push( @userDetails, &ParseResultsLDAPAttribs( $tmpName ) );
}
}
# loop over results with strange cn, dn attribs
for( my $m = 0; $m <= $#cnFix; $m++ ) {
if( ( defined( $cnFix[$m] ) ) && ( $cnFix[$m] ne '' ) ) {
print "\tCN=$cnFix[$m]\n";
$tmpName = $cnFix[$m];
if( $tmpName =~ /(\w+)\s(\w+)$/i ) {
$tmpName = $1 . $2;
}
push( @userDetails, &ParseResultsLDAPAttribs( $tmpName ) );
}
}
# clean it up a tad
sort( @userDetails );
# proceed to create an ldif from data
for( my $x = 0; $x <= $#userDetails; $x++ ) {
&CreateLDIFOne( $userDetails[$x] );
&CreateLDIFTwo( $userDetails[$x] );
}
# Lets finish it up
print "PROCESSING:\n";
&ProcessLDIF( $domain, $uname, $pass, $date );
# display log
print "NON-POSIX FIX RESULTS:\n";
open( RES, "$dir/ldif-results-$date.log" ) || die "ERROR: File could not be opened - $dir/ldif-results-$date.log\n";
flock( RES, 2 );
my( @lines ) = <RES>;
foreach my $line ( @lines ) {
chomp( $line );
print "\t$line\n";
}
close( RES );
flock( RES, 8 );
} else {
print "\tFound: '0' '$obj' record(s) that are not RFC2307 compliant\n";
}
unlink( "$dir/ldif-results-$date.log" );
#`rm $dir/*`;
}
# Our simple prompter
sub PromptUser {
my($prompt, $default) = @_;
my $defaultValue = $default ? "[$default]" : "";
print "$prompt $defaultValue: ";
chomp(my $input = <STDIN>);
return $input ? $input : $default;
}
# Wizard wiz sub routine
sub WizardWiz
{
$domain = &PromptUser("Enter domain");
$user = &PromptUser("Enter username");
$pass = &PromptUser("Enter password");
$nameOne = &PromptUser("Enter login name of user");
$container = &PromptUser("Enter OU", "Users");
$obj = &PromptUser("Enter object", "USER");
&MainSubEasy( $domain, $user, $pass, $nameOne, $container, $obj, $date );
}
# menu system
sub ShowSyntax
{
print <<EOF;
Winbind, Samba & Active Directory UID2SID mapping utility
------------------------------------------------------------
Queries Active directory for list of users and begins mapping of
the UID, GID & SID information to ensure Active Directory
Authentication is working.
Usage: ./UID2SID.pl --domain=[DOMAIN]
--user=[USERNAME]
--pass=[PASSWORD]
--ou=[DC=DOMAIN,DC=COM]
--obj=[USER|COMPUTER|GROUP]
--verbose
--help
EOF
exit;
}
Here is a simple method of searching Active directory using the ldapsearch utility.
%> ldapsearch -x -h DOMAIN -D USERNAME -w PASSWORD -b "cn=users,dc=EXAMPLE,dc=COM" "objectclass=user" | grep USERNAME
And the resulting output if an account exists
dn: CN=USERNAME LASTNAME,CN=Users,DC=EXAMPLE,DC=COM
cn: USERNAME LASTNAME
givenName: USERNAME
distinguishedName: CN=USERNAME LASTNAME,CN=Users,DC=DOMAIN,DC=COM
displayName: USERNAME LASTNAME
name: USERNAME LASTNAME
unixHomeDirectory: /Users/Authenticated Users/USERNAME LASTNAME