Given FIM's strengths it seems only natural to leverage the notification capabilities to inform your users of impending password expiration. However, in order to do that you will need several items to start:

Now, these days I try to avoid purpose-built code and tend to build reusable modules, so I'm providing two code fragments at the end of this wiki for converting File Time attributes in AD to the precise ISO 8601 format that the FIM Web Service needs as well as a general method of extracting a bit from the userAccountControl bitmask and turning it into a Boolean attribute.

I'm not going to cover how to wire up FIM Sync here to contribute these values as there are enough examples now on how to get data into the FIM MA. I'm also not going to cover here how to wire up a basic Set Transition policy or use the Notification Workflow, but these topics could be covered if there is demand. Once you've wired in the attributes to the FIM Service, you'll need to work on your Set Transitions. After the Set Transitions are modeled, then you can build your policy and workflows to suit.

Set Transition Patterns for Password Notification

There are numerous ways to approach this, but I broke this down into three general patterns and one base pattern. For the sake of argument, let's assume we're modeling three basic notification events: 14 days, 7 days and 1 day. Also, in my examples I'm using ADPasswordLastSet as the FIM Service attribute I've contributed pwdLastSet into and ADPasswordDoesNotExpire as the Boolean attribute I've contributed from the extracted ADS_UF_DONT_EXPIRE_PASSWD. 

The Base Pattern

These two clauses should form the foundation of all of your sets, and they will be AND'd together with one of the general patterns.



In the first clause you are filtering out anyone who is not an active person, as identified by membership in another set. While this makes it handy for building sets that are dependent upon it, care must be taken that you are not violating any of the set nesting rules. You can also substitute this clause for a direct evaluation (EmployeeStatus = 'A'), for example. The bottom line here is to filter out inactive people; if you are sending password expiration notices to terminated people then you have issues with your Deprovisioning logic.

The second clause filters out anyone who has a password that does not expire as we want to avoid nagging them when their password isn't actually expiring despite the fact that it technically falls into the date ranges we care about.

These two clauses form the base of your rule and the following patterns will dictate the remainder of the query. You will pick the pattern below that fits your scenario best and add it to the list of AND'd clauses in your Set definition.

The All-inclusive Pattern

The first pattern is where you will likely find yourself after simple grouping into Sets:



To determine each milestone we use the following equation:

Milestone = Max Password Age – (Notification Event)

In this pattern we can correctly identify people as they reach each milestone; however, people never transition out of the preceding set. In some cases you simply may not care but I generally like to avoid this type of situation so I don't have people in sets they don't need to be in. There is one situation where this pattern is an advantage, and that is if you really want the ability to resend all of the outstanding notifications by triggering a Run On Policy Update (ROPU). In this case, the above pattern would trigger your 14 day and 7 day notifications for a person 3 days from expiration; if this is what you are after then this is the pattern for you. Also, keep in mind that if you wire up a policy to a ROPU enabled Action Workflow, you will send all of the notifications immediately.

A Regret

The most regrettable aspect of this pattern, and all subsequent patterns, is that we are hardcoding the Max Password Age and have no means to discover this at execution time. This means that we have to know a lot of about the infrastructure and model several variations of this approach if you are using Fine Grained Password Policy in Windows Server 2008. On to our next pattern...

The Short Staggered Pattern



This pattern (assuming a 90 day max password age) keeps the person object in each set for only one day – long enough to trigger the notification and then they are removed. So, in the above example, days 13 to 8 and days 6 to 2 the person is not in any of the expiration sets. How long they remain in each set is determined by the difference between the two clauses. If you wanted them to remain in the set for two days then you'd widen the gap by extending the Transition In clause by two days instead of one. Let's look at this in equation form:

Transition In = Max Password Age – (Notification Event – Membership Period)

Transition Out = Max Password Age – (Notification Event)

So, in our first example using the 14 day Notification Event, a 90 day Max Password Age, and a 1 day period we'd get:

77 = 90 – (14 – 1)

76 = 90 – 14

The notification behavior in this pattern would be to notify as objects entered each set.

The Skip Pattern

For the object conservationists, we have this pattern, where we leverage the extended period approach to skip every other set – this is because we can use both the rising and the falling edge of the transition using a Transition-In policy on the rising edge and a Transition-Out policy on the falling edge:



In this pattern, the person is in each set for the entire period (6 days in the first example, transitioning on day 7), allowing you to use Run On Policy Update anytime during that period. It also allows you to reduce the number of Set objects you are constructing (you didn't build the 7 to 2 day Set) if you don't mind using Transition-Out policies for the next transition. Your policies would look like so:

  • Notify at 14 days from Expiration – Transition-In (14 to 8 Days Set)
  • Notify at 7 days from Expiration – Transition-Out (14 to 8 Days Set)

You trigger the second policy because you  transitioned out after the 7th day…just remember that when you are in the 7 to 2 day range you cannot use Run On Policy Update to trigger any notifications until you reach the next Set transition.

Code Fragments

ConvertFileTimeToISO8601

if (FlowRuleName.StartsWith("ConvertFileTimeToISO8601:"))
{
    // 1/6/10 bturner
    //
    // Reusable code to convert file time into string format
    // FlowRuleName will be passed as "ConvertFileTimeToISO8601:sourceAttribute,destinationAttribute"
    // Source value should look like 9223372036854775807
    // Should return time formatted as such: 2009-11-06T07:00:00.000
    //
    string strAttributeName, strSourceAttribute, strDestinationAttribute;
    string[] arrAttribs;
    // Replace the beginning of the flowrulename with nothing to find the attribute to be deleted
    strAttributeName = FlowRuleName.Replace("ConvertFileTimeToISO8601:", "");
    arrAttribs = strAttributeName.Split(',');
    strSourceAttribute = arrAttribs[0];
    strDestinationAttribute = arrAttribs[1];
    //none,accountExpires,number,9223372036854775807,9223372036854775807
    if (csentry[strSourceAttribute].IntegerValue == 0 || csentry[strSourceAttribute].IntegerValue == 9223372036854775807)
    {
        // This is a special condition, do not contribute and delete any current value
        mventry[strDestinationAttribute].Delete();
    }
    else
    {
        DateTime dtFileTime = DateTime.FromFileTime(csentry[strSourceAttribute].IntegerValue);
        // Convert to UTC, format string using custom format similiar to round trip "o" format
        // NOTE: SQL's precision for fractional time makes storage and confirmation of anything more than two digits problematic
        //      It's better to simply enforce .000 for fractional time here since it's not absolutely critical
        mventry[strDestinationAttribute].Value = dtFileTime.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.000'");
    }
}

GetUACBit

if (FlowRuleName.StartsWith("GetUACBit:"))
{
    // 12/7/10 - bturner
    //
    // I A F 
    // [csentry                   -> mventry                              ]
    // [user                      -> person                               ]
    // [userAccountControl        -> destinationAttribute (Boolean)       ] 
    //
    // Check the userAccountControlStatus for the enable/disable bit (bit 1)
    // FlowRuleName will be passed as "GetUACBit:destinationAttribute,intADS_USER_FLAG"
    // Where intADS_USER_FLAG is the integer form of the bit you are testing the mask for
    //
    // For a list of userAccountControl bits and their decimal equivalents:
    //
    // Example: 
    //      GetUACBit:ADPwdDoesNotExpire,65536
    //
    string strAttributeName, strDestinationAttribute;
    long intADS_USER_FLAG;
    string[] arrAttribs;
    // Replace the beginning of the flowrulename with nothing to find the attribute to be deleted
    strAttributeName = FlowRuleName.Replace("GetUACBit:", "");
    arrAttribs = strAttributeName.Split(',');
    strDestinationAttribute = arrAttribs[0];
    intADS_USER_FLAG = Convert.ToInt64(arrAttribs[1]);
    mventry[strDestinationAttribute].BooleanValue = ((csentry["userAccountControl"].IntegerValue & intADS_USER_FLAG) == intADS_USER_FLAG);
         
}