Create a Solution to Synchronize Account Expiry : Step 2 – Build a new connector (aka ManagementAgent)
Here is the source code for the connector, what will import the information about the expiry and looks like this:
and here is the code, what you can compile using VisualStudio (community edition should work as well). You need to add the following dll as reference to the project:
C:\Program Files\Microsoft Azure AD Sync\Bin\Assemblies\Microsoft.MetadirectoryServicesEx.dll
you can find further information about the ECMA 2 API here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 |
using System; using System.Text; using System.Collections.Generic; using System.Collections.ObjectModel; using Microsoft.MetadirectoryServices; using System.Data.SqlClient; using System.Data; using System.Diagnostics; using ConnectionState = System.Data.ConnectionState; namespace AccountExpiredMA { public class EzmaExtension : IMAExtensible2CallImport, IMAExtensible2GetSchema, IMAExtensible2GetCapabilities, IMAExtensible2GetParameters { private int m_importDefaultPageSize = 12; private int m_importMaxPageSize = 50; SqlConnection conn; SqlCommand sqlCmd; private const string OBJECT_ID = "object_id"; private const string OBJECT_TYPE = "object_type"; private const string SOURCE_ANCHOR = "sourceAnchor"; private const string DISPLAY_NAME = "displayName"; private const string USER_PRINCIPAL_NAME = "userPrincipalName"; private const string EXPIRY_ATTRIBUTE_KEY = "Metaverse Attribute for accountExpires"; private const string DEFAULT_EXPIRY_ATTR = "extensionAttribute10"; private const string EXPIRY_DATE = "expiryDate"; private const string CURRENT_DATE = "currentDate"; private const string EXPIRED = "expired"; private const string SQL_SERVER_KEY = "SQL server"; private const string SQL_SERVER_DB_KEY = "SQL Database"; private string EXPIRY_ATTRIBUTE = DEFAULT_EXPIRY_ATTR; private string WATERMARK_ATTRIBUTE = "last_modification_date"; private Schema currentSchema; private const string OBJECT_TYPE_PERSON = "Person"; private const string TABLE_NAME = "expiredAccounts"; // // Constructor // public EzmaExtension() { // // TODO: Add constructor logic here // } /// <summary> /// Capabilities of the Extensible MA /// </summary> public MACapabilities Capabilities { get { MACapabilities myCapabilities = new MACapabilities(); myCapabilities.ConcurrentOperation = true; myCapabilities.ObjectRename = false; myCapabilities.DeleteAddAsReplace = true; myCapabilities.DeltaImport = true; myCapabilities.DistinguishedNameStyle = MADistinguishedNameStyle.None; myCapabilities.NoReferenceValuesInFirstExport = false; myCapabilities.Normalizations = MANormalizations.None; myCapabilities.ObjectConfirmation = MAObjectConfirmation.Normal; return myCapabilities; } } /// <summary> /// Parameter settings / configuration page of the Management Agent /// This function will be called for each of the pages /// </summary> /// <param name="configParameters"></param> /// <param name="page"></param> /// <returns></returns> public IList<ConfigParameterDefinition> GetConfigParameters(KeyedCollection<string, ConfigParameter> configParameters, ConfigParameterPage page) { List<ConfigParameterDefinition> configParametersDefinitions = new List<ConfigParameterDefinition>(); switch (page) { case ConfigParameterPage.Connectivity: configParametersDefinitions.Add(ConfigParameterDefinition.CreateStringParameter(SQL_SERVER_KEY, "")); configParametersDefinitions.Add(ConfigParameterDefinition.CreateStringParameter(SQL_SERVER_DB_KEY, string.Empty,"ADSync")); configParametersDefinitions.Add(ConfigParameterDefinition.CreateDividerParameter()); configParametersDefinitions.Add(ConfigParameterDefinition.CreateLabelParameter("Please enter the name of the metaverse attribute, what contains the expiry date.")); configParametersDefinitions.Add(ConfigParameterDefinition.CreateStringParameter(EXPIRY_ATTRIBUTE_KEY, string.Empty,DEFAULT_EXPIRY_ATTR)); break; case ConfigParameterPage.Global: //// not required for our purposes break; case ConfigParameterPage.Partition: //// partitioning is possible only with LDAP style DN break; case ConfigParameterPage.RunStep: //// no additional setting is required here break; } return configParametersDefinitions; } /// <summary> /// This function will be called if the parameters will be validated /// </summary> /// <param name="configParameters"></param> /// <param name="page"></param> /// <returns></returns> public ParameterValidationResult ValidateConfigParameters(KeyedCollection<string, ConfigParameter> configParameters, ConfigParameterPage page) { ParameterValidationResult myResults = new ParameterValidationResult(); return myResults; } /// <summary> /// This function build the schema of the management agent. we have here a fixed schema. /// The objectType contains only person, the anchor attribute is fixed. /// But there is an option to select / unselect attributes /// </summary> /// <param name="configParameters"></param> /// <returns></returns> public Schema GetSchema(KeyedCollection<string, ConfigParameter> configParameters) { //// set the name of the Exipry attribute to the attribute, what is added on the config page EXPIRY_ATTRIBUTE = configParameters[EXPIRY_ATTRIBUTE_KEY].Value; //// create the object type in the schema: Person Microsoft.MetadirectoryServices.SchemaType personType = Microsoft.MetadirectoryServices.SchemaType.Create(OBJECT_TYPE_PERSON, true); //// create the schema attributes SchemaAttribute CS_ATTR_Anchor = SchemaAttribute.CreateAnchorAttribute(OBJECT_ID, AttributeType.String, AttributeOperation.ImportOnly); SchemaAttribute CS_ATTR_ObjectType = SchemaAttribute.CreateSingleValuedAttribute(OBJECT_TYPE, AttributeType.String, AttributeOperation.ImportOnly); SchemaAttribute CS_ATTR_DisplayName = SchemaAttribute.CreateSingleValuedAttribute(DISPLAY_NAME, AttributeType.String, AttributeOperation.ImportOnly); SchemaAttribute CS_ATTR_UserPrincipalName = SchemaAttribute.CreateSingleValuedAttribute(USER_PRINCIPAL_NAME, AttributeType.String, AttributeOperation.ImportOnly); SchemaAttribute CS_ATTR_sourceAnchor = SchemaAttribute.CreateSingleValuedAttribute(SOURCE_ANCHOR, AttributeType.String, AttributeOperation.ImportOnly); SchemaAttribute CS_ATTR_expired = SchemaAttribute.CreateSingleValuedAttribute(EXPIRED, AttributeType.String, AttributeOperation.ImportOnly); SchemaAttribute CS_ATTR_expiryAttribute = SchemaAttribute.CreateSingleValuedAttribute(EXPIRY_ATTRIBUTE, AttributeType.String, AttributeOperation.ImportOnly); SchemaAttribute CS_ATTR_ExpiryDate = SchemaAttribute.CreateSingleValuedAttribute(EXPIRY_DATE, AttributeType.String, AttributeOperation.ImportOnly); //// add the attributes to the object type personType.Attributes.Add(CS_ATTR_Anchor); personType.Attributes.Add(CS_ATTR_ObjectType); personType.Attributes.Add(CS_ATTR_DisplayName); personType.Attributes.Add(CS_ATTR_UserPrincipalName); personType.Attributes.Add(CS_ATTR_sourceAnchor); personType.Attributes.Add(CS_ATTR_expired); personType.Attributes.Add(CS_ATTR_expiryAttribute); personType.Attributes.Add(CS_ATTR_ExpiryDate); //// create the schema object Schema schema = Schema.Create(); //// add the object type to the schema schema.Types.Add(personType); return schema; } /// <summary> /// This is the first function what will be called during the import run-profile. /// This function takes care about the connection to data source in our case to the sql database. /// </summary> /// <param name="configParameters">This collection contains the configured configuration data.</param> /// <param name="types"> /// This parameter contains the configured schema: selected object type - in our case person and the /// selected attributes. /// </param> /// <param name="importRunStep"> /// This contains the run step, what contains also the information, /// if the running run profile delta import or full import.</param> /// <returns></returns> public OpenImportConnectionResults OpenImportConnection( KeyedCollection<string, ConfigParameter> configParameters, Schema types, OpenImportConnectionRunStep importRunStep) { WriteLog("Open Connection"); //// reading the data from the config parameter string myServer = configParameters[SQL_SERVER_KEY].Value; string myDB = configParameters[SQL_SERVER_DB_KEY].Value; //// set the selected attribute for the expiry attribute string expiryMVAttr = configParameters[EXPIRY_ATTRIBUTE_KEY].Value; //// set the global schema variable currentSchema = types; //// set up the SQL connection string string myConnection = $"Server = '{myServer}';Initial Catalog='{myDB}';Integrated Security=True"; WriteLog($"connection string:{myConnection}"); //// set up the SQL connection conn = new SqlConnection(myConnection); sqlCmd = new SqlCommand(); sqlCmd.CommandType = CommandType.Text; /// set up the SQL command, that is used to retrieve the data from the SQL table string cmdText = $@" Use ADsync; SELECT [object_id] ,[object_type] ,[sourceAnchor] ,[displayName] ,[userPrincipalName] ,[extensionAttribute10] ,CAST( [{expiryMVAttr}] as date) as expiryDate ,CAST(GETDATE() AS DATE) as currentDate ,'TRUE' As expired ,[last_modification_date] FROM[ADSync].[dbo].[mms_metaverse] WHERE object_type = 'person' AND {expiryMVAttr} is not null AND(CAST( [{expiryMVAttr}] as date) < CAST(GETDATE() AS DATE) ) "; //// check if the select run profile is delta import and if there is a watermark (CustomData) if (importRunStep.ImportType == OperationType.Delta && importRunStep.CustomData.Length > 0) { //// retrieve the watermark and append to the SQL query string waterMark = Convert.ToString(importRunStep.CustomData); if (!string.IsNullOrWhiteSpace(waterMark)) { string deltaSuffix = $" AND (CAST( [{expiryMVAttr}] as date) >= '{waterMark}'"; cmdText += deltaSuffix; } } WriteLog($"Command text: {cmdText}"); //// set up the SQL command sqlCmd.CommandText = cmdText; sqlCmd.Connection = conn; /// try to open the SQL connection try { conn.Open(); } catch (InvalidOperationException iopEx) { throw new FailedConnectionException("Can't open connection to SQL Server!", iopEx); } catch (SqlException sqlEx) { throw new FailedConnectionException("Can't open connection to SQL Server!", sqlEx); } catch (System.Configuration.ConfigurationException scceEx) { throw new FailedConnectionException("Can't open connection to SQL Server!", scceEx); } return new OpenImportConnectionResults(); } /// <summary> /// this function is called after the connection is opened to retrieve the data /// </summary> /// <param name="importRunStep"></param> /// <returns></returns> public GetImportEntriesResults GetImportEntries(GetImportEntriesRunStep importRunStep) { GetImportEntriesResults importReturnInfo = new GetImportEntriesResults(); List<CSEntryChange> csentries = new List<CSEntryChange>(); //// set up the SQL Data Adapter and fill the data set SqlDataAdapter adapter = new SqlDataAdapter(sqlCmd); DataSet da = new DataSet(); adapter.Fill(da, TABLE_NAME); //// create a mapping table between column name and column id Dictionary<string, int> columnMapping = new Dictionary<string, int>(); foreach (DataColumn column in da.Tables[TABLE_NAME].Columns) { columnMapping.Add(column.ColumnName, column.Ordinal); } string currentWaterMark = string.Empty; //// retrieve the configured schema for the object type person KeyedCollection<string, SchemaAttribute> selectedAttributes = currentSchema.Types[OBJECT_TYPE_PERSON].Attributes; //// check if there are any results if (null != da.Tables && 0 < da.Tables.Count) { WriteLog($"Nr of result: {da.Tables.Count} has been retrieved."); //// iterate through the result set for (int indexOfResult = 0; indexOfResult <= da.Tables[TABLE_NAME].Rows.Count - 1; indexOfResult++) { //WriteLog($"Current: {indexOfResult} of result: {da.Tables.Count}"); //// create the connector space object, that will be passed as return value to the sync engine CSEntryChange cse = CSEntryChange.Create(); cse.ObjectModificationType = ObjectModificationType.Add; cse.ObjectType = OBJECT_TYPE_PERSON; //// iterate through the columns foreach (string columnName in columnMapping.Keys) { //WriteLog($"Current column: {columnName} "); //// check if the current attribute is selected if (selectedAttributes.Contains(columnName)) { //WriteLog($"Current column is selected: {columnName} "); //// retrieve the value of the current and selected attribute string attributeValue = RetrieveStringValueFromTable(ref da, ref indexOfResult, ref columnMapping, columnName); if (!string.IsNullOrWhiteSpace(attributeValue)) { //WriteLog($"Current value is: {attributeValue} "); //// the attribute value is not empty => add to the object cse.AttributeChanges.Add(AttributeChange.CreateAttributeAdd(columnName, attributeValue)); } } } /// add the connector space object to the result list csentries.Add(cse); } } else { //// the result list is empty WriteLog($"No result has been retrieved."); } //// set up the return info object the pass as return object importReturnInfo.CustomData = currentWaterMark; importReturnInfo = new GetImportEntriesResults(); importReturnInfo.MoreToImport = false; importReturnInfo.CSEntries = csentries; return importReturnInfo; } /// <summary> /// auxiliary function to read the object value from the given table /// </summary> /// <param name="da">DataSet that contains the retrieved data from SQL database</param> /// <param name="rowIndex">index of the current row in the data table</param> /// <param name="columnMapping">Dictionary that contains the mapping between column name and column index</param> /// <param name="attribute">contains the name of the current attribute</param> /// <returns></returns> private string RetrieveStringValueFromTable(ref DataSet da, ref int rowIndex, ref Dictionary<string, int> columnMapping, string attribute) { return Convert.ToString(da.Tables[TABLE_NAME].Rows[rowIndex].ItemArray.GetValue(columnMapping[attribute])).Trim(); } /// <summary> /// after all data are retrieved, the connection needs to be closed /// </summary> /// <param name="importRunStepInfo"></param> /// <returns></returns> public CloseImportConnectionResults CloseImportConnection(CloseImportConnectionRunStep importRunStepInfo) { if (conn.State == ConnectionState.Open) { try { conn.Close(); } catch (SqlException sqlEx ) { WriteLog($"Error while closing the SQL Connection. \n {sqlEx.Message}"); } } return new CloseImportConnectionResults(); } /// <summary> /// This function returns the max possible import page size /// </summary> public int ImportMaxPageSize { get { return m_importMaxPageSize; } } /// <summary> /// This function returns the default import page size /// </summary> public int ImportDefaultPageSize { get { return m_importDefaultPageSize; } } /// <summary> /// This function writes message to the Windows Application Event log /// </summary> /// <param name="message">This is the message string, what will be written into the Application Event Log</param> internal void WriteLog(string message) { //EventLog.CreateEventSource("ADSync", "Application"); EventLog objEvent = new EventLog(); objEvent.Source = "ADSync"; StringBuilder eventText = new StringBuilder(); eventText.Append($"ExpireAccountsConnector: {message}"); objEvent.WriteEntry(eventText.ToString(), EventLogEntryType.Information); } } } |
And if you wish to download the precompiled connector you can do this here:
the next step is to create a connector: https://happyts.tech/create-a-solution-to-synchronize-account-expiry-step-3-add-the-new-connector-to-aad-connect/