Skip to main content

Microsoft

Creating Microsoft FIM Management Agent: lessons learned

extensibilityMicrosoft FIM (Frontend Identity Manager) is a popular enterprise product which is allowing to automate user creation, provisioning and de-provisioning in Microsoft Active Directory. FIM has many out-of-the-box extension connectors which allows for connecting FIM to external systems (like external user catalogs), including a set of web services. When out-of-the-box connectors are not sufficient, it’s possible to implement a custom Management Agent (MA) for FIM using .NET framework.
I recently helped to connect FIM to external system which had MySQL user database. There is no out-of-the-box FIM connector for MySQL, we had to implement our own custom management agent. While working on this task I found out that the process of creating FIM MAs is very scarcely documented. This reference and that example is pretty my the only source of information about creating Extensible Connectivity Management Agents (ECMA). The problem though is that the reference doesn’t provide a complete documentation for creating ECMAs and the code example, while providing a complete code listing for management agent, doesn’t cover all everything. In fact, the code sample represents one specific, quite simple case of ECMA 2.2 agents, and there are a couple of places in this example which require additional explanation.
I don’t want to repeat Microsoft documentation, I just want to mention a few things which I learned from working on creating FIM MA and which are not obvious from provided code sample.
1. GetSchema and GetImportEntries should be consistent. GetSchema is returning a full set of columns which are available for FIM operator (i.e. the person who is configuring MA in FIM) to select to import and map to AD metaverse. In Microsoft’s example, GetSchema is returning a all columns from selected table, while GetImportEntries is only returning data for the selected few columns. This is going to work only when operator mapped all of these selected few records to metaverse and didn’t map anything else. If GetImportEntries will return a column which is not mapped in FIM, then FIM will complain and stop import process. Same is going to happen if GetImportEntries will not return a column which is mapped in FIM. So the provided code sample is not exactly correct. Yes, it’s going to work, but only on specific condition. I think the better practice would be to either return only these columns from GetSchema which are going to be mapped, or check what is mapped in GetImportEntries code and return data for these columns. The second option is obviously better, but first one is easier to implement.
2. Yes, it’s possible to implement paging. The provided code sample doesn’t illustrate the paging (I assume it was created to work with a small-scale dataset, but what if the external data source contains hundreds of thousands of records?), but ECMA 2.2 interfaces support that. In order to implement the paging, you can do the following:
a. In OpenImportConnection you can calculate a total number of records that you need to import and then store in in the member variable:

        public OpenImportConnectionResults OpenImportConnection(
                                       KeyedCollection<string, configparameter=""> configParameters,
                                       Schema types,
                                       OpenImportConnectionRunStep importRunStep)
        {
            try
            {
                myServer = configParameters["Server"].Value;
                myDB = configParameters["Database"].Value;
                myTable = configParameters["Table"].Value;
                myPort = configParameters["Port"].Value;
                myUser = configParameters["User"].Value;
                myPassword = configParameters["Password"].Value;
                myConnectionString = GetConnectionString(myServer, myDB, myPort, myUser, myPassword);
                m_conn = new MySqlConnection(myConnectionString);
                Logger.Write("OpenImportConnection", myServer, myDB, myTable, myPort, myUser, myPassword);
                m_pageSize = importRunStep.PageSize;
                m_importCount = 0;
                Logger.Write("OpenImportConnection", "PageSize", importRunStep.PageSize);
                m_cmd = new MySqlCommand();
                m_cmd.CommandType = CommandType.Text;
                string cmdText = "Select * from " + myTable;
                m_cmd.CommandText = cmdText;
                m_cmd.Connection = m_conn;
                Logger.Write("OpenImportConnection", "CommandText", cmdText);
                m_totalCount = GetRecordCount(myServer, myDB, myTable, myPort, myUser, myPassword);
                Logger.Write("OpenImportConnection", "TotalCount", m_totalCount);
            }
            catch(Exception ex)
            {
                Logger.Write("OpenImportConnection exception", ex.ToString());
            }
            return new OpenImportConnectionResults();
        }
        public int GetRecordCount(string server, string database, string table, string port, string user, string password)
        {
            using (var connection = new MySqlConnection(GetConnectionString(server, database, port, user, password)))
            {
                using (var command = new MySqlCommand())
                {
                    connection.Open();
                    command.CommandType = CommandType.Text;
                    command.CommandText = "Select count(*) from " + table;
                    command.Connection = connection;
                    var result = command.ExecuteScalar();
                    return Convert.ToInt32(result);
                }
            }
        }

Then, inside GetImportEntries you can count the records which you imported and then set GetImportEntriesResults.MoreToImport to true when there are still records available to import:

public GetImportEntriesResults GetImportEntries(GetImportEntriesRunStep importRunStep)
        {
            try
            {
                int skipRecords = m_importCount;
                int takeRecords = m_pageSize;
                if(skipRecords + takeRecords > m_totalCount)
                {
                    takeRecords = m_totalCount - skipRecords;
                }
                string cmdText = "Select * from " + myTable + " limit " + skipRecords.ToString() + "," + takeRecords.ToString();
                m_cmd.CommandText = cmdText;
                Logger.Write("GetImportEntries", "cmdText", cmdText);
                m_adapter = new MySqlDataAdapter(m_cmd);
                m_da = new DataSet();
                m_adapter.Fill(m_da, "Users");
                List csentries = new List();
                for (int i = 0; i <= m_da.Tables[myTable].Rows.Count - 1; i++)
                {
                    CSEntryChange csentry = CSEntryChange.Create();
                    // do you import here
                    csentries.Add(csentry);
                    m_importCount++;
                }
                Logger.Write("GetImportEntries", "imported", m_importCount);
                GetImportEntriesResults importReturnInfo;
                importReturnInfo = new GetImportEntriesResults();
                importReturnInfo.MoreToImport = m_importCount < m_totalCount;
                importReturnInfo.CSEntries = csentries;
                Logger.Write("GetImportEntries", "MoreToImport", importReturnInfo.MoreToImport);
                return importReturnInfo;
            }
            catch(Exception ex)
            {
                Logger.Write("GetImportEntries exception", ex.ToString());
            }
            return new GetImportEntriesResults();
        }

Also, you’ll need to provide your recommended page sizes to FIM:

        public int ImportMaxPageSize
        {
            get
            {
                return m_importMaxPageSize;
            }
        }
        public int ImportDefaultPageSize
        {
            get
            {
                return m_importDefaultPageSize;
            }
        }

3. Your ECMA 2.2. management agent is not completely unit-testable, unfortunately. CSEntryChange.Create() will fail when it’s called from outside of the FIM process due to dependency on FIM. So if you planning to create a unit test for your management agent (which is a good thing), then you’ll have to find a workaround for that, by either creating a debug-only code, or by using dependency injection.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Stan Tarnovskiy

Solutions Architect at Perficient

More from this Author

Categories
Follow Us