1
/****************************************************************************
3
| Copyright (c) 2007 Novell, Inc.
6
| This program is free software; you can redistribute it and/or
7
| modify it under the terms of version 2 of the GNU General Public License as
8
| published by the Free Software Foundation.
10
| This program is distributed in the hope that it will be useful,
11
| but WITHOUT ANY WARRANTY; without even the implied warranty of
12
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
| GNU General Public License for more details.
15
| You should have received a copy of the GNU General Public License
16
| along with this program; if not, contact Novell, Inc.
18
| To contact Novell about this file by physical or electronic mail,
19
| you may find current contact information at www.novell.com
21
| Author: Bruce Getter <bgetter@novell.com>
22
|***************************************************************************/
26
using Novell.Directory.Ldap;
27
using Novell.Directory.Ldap.Utilclass;
30
using Simias.LdapProvider;
33
namespace Simias.ADLdapProvider
36
/// Service class used to get an execution context
37
/// so we can register ourselves with the external
40
class ADSync : Simias.IIdentitySyncProvider
43
private readonly string name = "Active Directory LDAP Synchronization";
44
private readonly string description = "Active Directory LDAP Synchronization provider to synchronize identities from Active Directory to a simias domain";
45
private bool abort = false;
47
private Simias.LdapProvider.Status syncStatus;
48
private static LdapSettings ldapSettings;
49
private Exception syncException;
51
private Store store = null;
52
private Domain domain = null;
54
private LdapConnection conn = null;
55
private Simias.IdentitySync.State state = null;
58
/// Used to log messages.
60
private static readonly ISimiasLog log =
61
SimiasLogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
66
/// Gets the name of the provider.
68
public string Name { get{ return name; } }
71
/// Gets the description of the provider.
73
public string Description { get{ return description; } }
76
#region Private Methods
78
private void ProcessSearchObjects( LdapConnection conn, LdapSettings settings )
80
foreach ( string searchContext in settings.SearchContexts )
82
string[] searchAttributes = { "objectClass" };
84
log.Debug( "SearchObject: " + searchContext );
88
LdapEntry ldapEntry = conn.Read( searchContext, searchAttributes );
89
LdapAttribute attrObjectClass = ldapEntry.getAttribute( "objectClass" );
90
String[] values = attrObjectClass.StringValueArray;
92
if ( IsUser( values ) == true )
94
// Process SearchDN as
95
log.Debug( "Processing User Object..." );
96
ProcessSearchUser( conn, searchContext );
98
else if ( IsGroup( values ) == true )
100
// Process SearchDN as
101
log.Debug( "Processing Group Object..." );
102
ProcessSearchGroup( conn, searchContext );
104
else if ( IsContainer( values ) == true )
106
// Process SearchDN as Container
107
log.Debug( "Processing Container Object..." );
108
ProcessSearchContainer( conn, searchContext );
112
log.Debug( "Invalid objectClass: " + values[0] );
113
log.Debug( attrObjectClass.ToString() );
116
catch( SimiasShutdownException s )
118
log.Error( s.Message );
121
catch ( LdapException e )
123
log.Error( e.LdapErrorMessage );
124
log.Error( e.StackTrace );
126
catch ( Exception e )
128
log.Error( e.Message );
129
log.Error( e.StackTrace );
134
private string BuildGuidFilter( string guid )
136
Guid cGuid = new Guid( guid );
137
byte[] bGuid = cGuid.ToByteArray();
139
string guidFilter = "(objectGUID=";
142
// The CSharpLdap SDK expects each byte to
143
// be zero padded else an exception is thrown
144
for( int i = 0; i < 16; i++ )
147
tmp = Convert.ToString( bGuid[i], 16 );
148
if ( tmp.Length == 1 )
159
private string GetLdapGuid( LdapEntry entry )
161
string ldapGuid = null;
165
LdapAttribute guidAttr = entry.getAttribute( "objectGUID" );
166
if ( guidAttr != null && guidAttr.StringValue.Length != 0 )
168
byte[] bGuid = new byte[8];
169
for( int i = 0; i < 8; i++ )
171
bGuid[i] = (byte) guidAttr.ByteValue[i];
176
BitConverter.ToInt32( bGuid, 0 ),
177
BitConverter.ToInt16( bGuid, 4 ),
178
BitConverter.ToInt16( bGuid, 6 ),
179
(byte) guidAttr.ByteValue[8],
180
(byte) guidAttr.ByteValue[9],
181
(byte) guidAttr.ByteValue[10],
182
(byte) guidAttr.ByteValue[11],
183
(byte) guidAttr.ByteValue[12],
184
(byte) guidAttr.ByteValue[13],
185
(byte) guidAttr.ByteValue[14],
186
(byte) guidAttr.ByteValue[15] );
188
ldapGuid = cGuid.ToString();
195
private bool IsUser( String[] objectClasses )
199
foreach( string s in objectClasses )
201
string lower = s.ToLower();
202
if ( lower == "inetorgperson" )
206
else if ( lower == "organizationalPerson" )
214
log.Error( "IsUser failed with exception" );
215
log.Error( e.Message );
220
private bool IsGroup( String[] objectClasses )
224
foreach( string s in objectClasses )
226
if ( s.ToLower() == "groupofnames" )
234
log.Error( "IsGroup failed with exception" );
235
log.Error( e.Message );
240
private bool IsContainer( String[] objectClasses )
242
bool isContainer = false;
246
foreach( string s in objectClasses )
248
string lower = s.ToLower();
249
if ( lower == "organization" )
251
log.Debug( "Processing Organization Object..." );
255
else if ( lower == "organizationalunit" )
258
log.Debug( "Processing OrganizationalUnit Object..." );
261
else if ( lower == "country" )
264
log.Debug( "Processing Country Object..." );
267
else if ( lower == "locality" )
270
log.Debug( "Processing Locality Object..." );
273
else if ( lower == "container" )
276
log.Debug( "Processing Container Object..." );
283
log.Error( "IsContainer failed with exception" );
284
log.Error( e.Message );
290
private void ProcessSearchUser( LdapConnection connection, String searchUser )
292
// Since the first version of the iFolder 3.0 only
293
// exposes a username, firstname, lastname and full
294
// name, we'll limit the scope of the search
295
string[] searchAttributes = {
297
ldapSettings.NamingAttribute,
304
log.Debug( "ProcessSearchUser(" + searchUser + ")" );
308
LdapEntry ldapEntry = connection.Read( searchUser, searchAttributes );
309
ProcessUserEntry( ldapEntry );
311
catch( SimiasShutdownException s )
315
catch( LdapException e )
317
log.Error( e.LdapErrorMessage );
318
log.Error( e.StackTrace );
322
log.Error( e.Message );
323
log.Error( e.StackTrace );
327
// If the configured Simias Admin is different than the SimiasAdmin
328
// identified in the store, make all the changes necessary to
329
// make the configured admin the store admin.
330
private void ChangeSimiasAdmin( LdapConnection conn )
332
char[] dnDelimiters = {',', '='};
333
LdapEntry entry = null;
337
string[] searchAttributes = { "cn", "sn", "objectGUID" };
341
// Nothing in the config for a SimiasAdmin - we're done here
342
if ( ldapSettings.AdminDN == null || ldapSettings.AdminDN == "" )
347
// If the SimiasAdmin has been changed in the Simias.config, which BTW
348
// is not something that is exposed in the normal management UI,
349
// we need to verify the new SimiasAdmin exists in the directory,
350
// check if the new admin exists in local domain memberlist (and
351
// if he doesn't create him), transfer default domain ownership
352
// to the new SimiasAdmin and lastly transfer ownership of all
353
// orphaned iFolders to the new SimiasAdmin.
357
entry = conn.Read( ADSync.ldapSettings.AdminDN, searchAttributes );
359
catch( LdapException lEx )
361
log.Error( "Could not verify the newly configured Simias Administrator in the directory" );
362
log.Error( lEx.Message );
364
catch( Exception e1 )
366
log.Error( "Could not verify the newly configured Simias Administrator in the directory" );
367
log.Error( e1.Message );
375
ldapGuid = GetLdapGuid( entry );
376
if ( ldapGuid == null || ldapGuid == "" )
381
// Get the common name from the Simias.config.AdminDN entry
382
string[] components = ADSync.ldapSettings.AdminDN.Split( dnDelimiters );
383
commonName = ( components[0].ToLower() == "cn" ) ? components[1] : components[0];
384
if ( commonName == null || commonName == "" )
389
store = Store.GetStore();
390
if ( domain == null )
392
domain = store.GetDomain( store.DefaultDomain );
393
if ( domain == null )
395
throw new SimiasException( "Enterprise domain does not exist!" );
399
Member member = domain.GetMemberByName( commonName );
400
if ( member == null )
402
// Create the member with the Ldap guid
404
new Member( commonName, ldapGuid, Simias.Storage.Access.Rights.ReadOnly );
405
member.Properties.ModifyProperty( "DN", ldapSettings.AdminDN );
408
Property lguid = new Property( "LdapGuid", ldapGuid );
409
lguid.LocalProperty = true;
410
member.Properties.ModifyProperty( lguid );
411
domain.Commit( member );
413
// Transfer ownership of all collections owned by the
414
// previous admin that have the orphaned property
416
ICSList subList = store.GetCollectionsByOwner( domain.Owner.ID, domain.ID );
417
foreach ( ShallowNode sn in subList )
419
// Get the collection object for this node.
420
Collection c = store.GetCollectionByID( sn.CollectionID );
423
orphaned = c.Properties.GetSingleProperty( "OrphanedOwner" );
424
if ( orphaned != null )
426
dn = c.Owner.Properties.GetSingleProperty( "DN" );
429
c.PreviousOwner = dn.Value.ToString();
433
c.Commit( c.ChangeOwner( member, Simias.Storage.Access.Rights.ReadWrite ) );
438
// For now I'm just going to leave the LdapGuid property
439
// on the old SimiasAdmin
440
dn = domain.Owner.Properties.GetSingleProperty( "DN" );
443
domain.PreviousOwner = dn.Value.ToString();
447
domain.Commit( domain.ChangeOwner( member, Simias.Storage.Access.Rights.ReadWrite ) );
449
catch( Exception vsa )
451
log.Error( vsa.Message );
452
log.Error( vsa.StackTrace );
456
// The SimiasAdmin is processed differently than normal simias users because
457
// the account is aleady created in the Simias store before LdapSync runs
458
// so the GUID has already been created. The SimiasAdmin must always exist in the
459
// store and the DN entry in the store must be correct with the Distinguished
460
// Name in the directory. LdapSync counts on the AdminDN entry in Simias.config
461
// to be updated if the admin is moved in the directory.
462
private void ProcessSimiasAdmin( LdapConnection conn )
464
// Since the first version of the iFolder 3.0 only
465
// exposes a username, firstname, lastname and full
466
// name, we'll limit the scope of the search
467
string[] searchAttributes = {
469
ldapSettings.NamingAttribute,
475
char[] dnDelimiters = {',', '='};
476
LdapEntry entry = null;
477
LdapAttribute timeStampAttr = null;
478
Member cMember = null;
480
string ldapGuid = null;
482
log.Debug( "ProcessSimiasAdmin( " + ldapSettings.AdminDN + ")" );
484
string namingContext = "";
485
entry = conn.Read( "", new string[] { "defaultNamingContext" } );
488
namingContext = entry.getAttribute( "defaultNamingContext" ).StringValue;
491
if ( domain == null )
493
store = Store.GetStore();
494
domain = store.GetDomain( store.DefaultDomain );
495
if ( domain == null )
497
throw new SimiasException( "Enterprise domain does not exist!" );
501
// If the DN property has never been set on the SimiasAdmin,
503
cMember = domain.Owner;
504
dn = cMember.Properties.GetSingleProperty( "DN" );
505
if ( dn == null || dn.Value.ToString() == "" )
507
if ( ldapSettings.AdminDN != null && ldapSettings.AdminDN != "" )
509
dn = new Property( "DN", ldapSettings.AdminDN );
510
cMember.Properties.ModifyProperty( dn );
514
// Check if the Simias Admin has changed in configuration
515
if ( ldapSettings.AdminDN != null && ldapSettings.AdminDN != "" &&
516
dn.Value.ToString() != ldapSettings.AdminDN )
518
ChangeSimiasAdmin( conn );
519
cMember = domain.Owner;
522
// The Simias admin is tracked in the directory by the directory
523
// guid. Make sure the guid is stored in the node
524
Property lguidProp = cMember.Properties.GetSingleProperty( "LdapGuid" );
525
if ( lguidProp == null )
527
// This must be the first time thru so let's get the directory
528
// entry based on the configured DN
531
entry = conn.Read( ldapSettings.AdminDN, searchAttributes );
533
catch( LdapException lEx )
535
log.Error( "The Simias Administrator does not exist in the Ldap directory as configured in Simias.config!" );
536
log.Error( lEx.Message );
538
catch( Exception e1 )
540
log.Error( "The Simias Administrator does not exist in the Ldap directory as configured in Simias.config!" );
541
log.Error( e1.Message );
546
ldapGuid = GetLdapGuid( entry );
547
lguidProp = new Property( "LdapGuid", ldapGuid );
548
lguidProp.LocalProperty = true;
549
cMember.Properties.ModifyProperty( lguidProp );
554
ldapGuid = lguidProp.Value.ToString();
557
if ( ldapGuid != null )
563
// Now go find the SimiasAdmin in the Ldap directory
564
string guidFilter = BuildGuidFilter( ldapGuid );
565
LdapSearchResults results =
568
LdapConnection.SCOPE_SUB,
569
"(&(objectclass=organizationalPerson)" + guidFilter + ")",
572
if ( results.hasMore() == true )
574
entry = results.next();
577
catch ( LdapException e )
579
log.Error( e.LdapErrorMessage );
580
log.Error( e.StackTrace );
582
catch ( Exception e )
584
log.Error( e.Message );
585
log.Error( e.StackTrace );
591
// check if the ldap object's time stamp has changed
595
timeStampAttr = entry.getAttribute( "modifytimestamp" );
597
cMember.Properties.GetSingleProperty( "LdapTimeStamp" );
599
if ( ( pStamp == null ) ||
601
(string) pStamp.Value != timeStampAttr.StringValue ) )
603
// The time stamp changed let's look at first and
608
bool changed = false;
610
// If we're tracking by ldap see if the naming attribute
612
LdapAttribute namingAttr = entry.getAttribute( ldapSettings.NamingAttribute );
613
if ( namingAttr != null && namingAttr.StringValue.Length != 0 )
615
if ( namingAttr.StringValue != cMember.Name )
617
cMember.Name = namingAttr.StringValue;
621
LdapAttribute givenAttr = entry.getAttribute( "givenName" );
622
if ( givenAttr != null && givenAttr.StringValue.Length != 0 )
624
if ( givenAttr.StringValue != cMember.Given )
627
cMember.Given = givenAttr.StringValue;
631
LdapAttribute sirAttr = entry.getAttribute( "sn" );
632
if ( sirAttr != null && sirAttr.StringValue.Length != 0 )
634
if ( sirAttr.StringValue != cMember.Family )
636
cMember.Family = sirAttr.StringValue;
642
// If the entry has changed and we have a valid
644
if ( changed == true &&
645
cMember.Given != null &&
646
cMember.Given != "" &&
647
cMember.Family != null &&
648
cMember.Family != "" )
650
cMember.FN = cMember.Given + " " + cMember.Family;
653
// Did the distinguished name change?
654
Property dnProp = cMember.Properties.GetSingleProperty( "DN" );
655
if ( dnProp != null && ( dnProp.ToString() != entry.DN ) )
657
dnProp.Value = entry.DN;
658
cMember.Properties.ModifyProperty( "DN", dnProp );
663
pStamp = new Property( "LdapTimeStamp", timeStampAttr.StringValue );
664
pStamp.LocalProperty = true;
665
cMember.Properties.ModifyProperty( pStamp );
672
log.Error( "The Simias administrator could not be verified in the directory!" );
673
log.Error( "Please update Simias.config with a valid Ldap user" );
678
log.Error( "The Simias administrator could not be verified in the directory!" );
679
log.Error( "Please update Simias.config with a valid Ldap user" );
682
// Now matter what always update the sync guid so
683
// the SimiasAdmin won't be deleted from Simias
685
cMember.Properties.ModifyProperty( state.SyncGuid );
686
domain.Commit( cMember );
689
private void ProcessSearchGroup( LdapConnection conn, String searchGroup )
691
string[] searchAttributes = {
696
log.Debug( "ProcessSearchGroup(" + searchGroup + ")" );
702
LdapEntry ldapEntry = conn.Read( searchGroup, searchAttributes );
703
String[] members = ldapEntry.getAttribute("member").StringValueArray;
705
foreach( String member in members )
707
// Check if the sync engine wants us to abort
708
if ( this.abort == true )
713
log.Debug( " Processing member: " + member );
715
ProcessSearchUser( conn, member );
718
catch( SimiasShutdownException s )
722
catch( LdapException e )
724
log.Error( e.LdapErrorMessage );
725
log.Error( e.StackTrace );
729
log.Error( e.Message );
730
log.Error( e.StackTrace );
733
log.Debug( "Processed " + count.ToString() + " entries" );
736
private void ProcessSearchContainer(LdapConnection conn, String searchContainer)
738
String searchFilter = "(objectclass=user)";
739
string[] searchAttributes = {
741
ldapSettings.NamingAttribute,
748
log.Debug( "ProcessSearchContainer(" + searchContainer + ")" );
751
LdapSearchConstraints searchConstraints = new LdapSearchConstraints();
752
searchConstraints.MaxResults = 0;
754
LdapSearchQueue queue =
757
LdapConnection.SCOPE_SUB,
761
(LdapSearchQueue) null,
764
LdapMessage ldapMessage;
765
while( ( ldapMessage = queue.getResponse() ) != null )
767
// Check if the sync engine wants us to abort
768
if ( this.abort == true )
773
if ( ldapMessage is LdapSearchResult )
775
LdapEntry cEntry = ((LdapSearchResult) ldapMessage).Entry;
783
ProcessUserEntry( cEntry );
786
catch( SimiasShutdownException s )
788
log.Error( s.Message );
791
catch( LdapException e )
793
log.Error( " Failed processing: " + cEntry.DN );
794
log.Error( e.LdapErrorMessage );
795
log.Error( e.StackTrace );
799
log.Error( " Failed processing: " + cEntry.DN );
800
log.Error( e.Message );
801
log.Error( e.StackTrace );
806
log.Debug( "Processed " + count.ToString() + " entries" );
809
private void ProcessUserEntry( LdapEntry entry )
811
log.Debug( "ProcessUserEntry(" + entry.DN + ")" );
813
string commonName = String.Empty;
814
string firstName = String.Empty;
815
string lastName = String.Empty;
816
string fullName = String.Empty;
817
string distinguishedName = String.Empty;
818
string ldapGuid = null;
820
char[] dnDelimiters = {',', '='};
821
LdapAttribute timeStampAttr = null;
823
bool attrError = false;
826
// get the last update time
827
timeStampAttr = entry.getAttribute( "modifytimestamp" );
829
ldapGuid = GetLdapGuid( entry );
830
distinguishedName = entry.DN;
832
// retrieve from configuration the directory attribute configured
833
// for naming in Simias.
834
LdapAttribute cAttr =
835
entry.getAttribute( ldapSettings.NamingAttribute );
836
if ( cAttr != null && cAttr.StringValue.Length != 0 )
838
commonName = cAttr.StringValue;
841
if ( ldapSettings.NamingAttribute.ToLower() == LdapSettings.DefaultNamingAttribute.ToLower() )
843
// If the naming attribute is default (cn) then we want to continue
844
// to work the way we previously did so we don't break any existing installs.
846
// If the distinguishing attribute did not exist,
847
// then make the Simias username the first component
849
string[] components = entry.DN.Split( dnDelimiters );
850
commonName = components[1];
853
LdapAttribute givenAttr = entry.getAttribute( "givenName" );
854
if ( givenAttr != null && givenAttr.StringValue.Length != 0 )
856
firstName = givenAttr.StringValue as string;
859
LdapAttribute sirAttr = entry.getAttribute( "sn" );
860
if ( sirAttr != null && sirAttr.StringValue.Length != 0 )
862
lastName = sirAttr.StringValue as string;
865
if ( firstName != null && lastName != null )
867
fullName = firstName + " " + lastName;
870
catch( Exception gEx )
872
log.Error( gEx.Message );
873
log.Error( gEx.StackTrace );
875
state.ReportError( gEx.Message );
879
// No exception were generated gathering member info
880
// so call the sync engine to process this member
881
if ( attrError == false )
883
if ( timeStampAttr != null && timeStampAttr.StringValue.Length != 0 )
885
Property ts = new Property( "LdapTimeStamp", timeStampAttr.StringValue );
886
ts.LocalProperty = true;
887
Property[] propertyList = { ts };
914
#region Public Methods
916
/// Call to abort an in process synchronization
918
/// <returns>N/A</returns>
925
/// Call to inform a provider to start a synchronization cycle
927
/// <returns> True - provider successfully finished a sync cycle,
928
/// False - provider failed the sync cycle
930
public bool Start( Simias.IdentitySync.State State )
932
log.Debug( "Start called" );
942
ldapSettings = LdapSettings.Get( Store.StorePath );
943
log.Debug( "new LdapConnection" );
944
conn = new LdapConnection();
946
log.Debug( "Connecting to: " + ldapSettings.Host + " on port: " + ldapSettings.Port.ToString() );
947
conn.SecureSocketLayer = ldapSettings.SSL;
948
conn.Connect( ldapSettings.Host, ldapSettings.Port );
950
ProxyUser proxy = new ProxyUser();
952
log.Debug( "Binding as: " + proxy.UserDN );
953
conn.Bind( proxy.UserDN, proxy.Password );
955
ProcessSimiasAdmin( conn );
956
ProcessSearchObjects( conn, ldapSettings );
958
catch( SimiasShutdownException s )
960
log.Error( s.Message );
962
syncStatus = Simias.LdapProvider.Status.SyncThreadDown;
964
catch( LdapException e )
966
log.Error( e.LdapErrorMessage );
967
log.Error( e.StackTrace );
971
? Simias.LdapProvider.Status.LdapConnectionFailure
972
: Simias.LdapProvider.Status.LdapAuthenticationFailure;
974
state.ReportError( e.LdapErrorMessage );
978
log.Error( e.Message );
979
log.Error( e.StackTrace );
981
syncStatus = Simias.LdapProvider.Status.InternalException;
983
state.ReportError( e.Message );
989
log.Debug( "Disconnecting Ldap connection" );
998
log.Error( e.Message );
999
log.Error( e.StackTrace );
1000
State.ReportError( e.Message );