http://wordpress.org/extend/plugins/members-list/ would be it for per-site users, I think.
You’d have to extend that to all sites somehow.
Custom. very very custom work. 🙂
Go get blog 1 details, pull list of users. go get blog 2 details, pull list of users.
Cache results so it’s not looping thru every blog on every load of this page.
Here’s a function I wrote that works
function rolodex($blognumber,$content = null) {
extract(shortcode_atts(array( "blognumber" => 'blognumber' ), $blognumber));
$rolodex = '<a name="'.$content.'"></a><table class="full"><caption>'.$content.'</caption>
<thead><th> </th>
<th nowrap="nowrap">First Name</th>
<th>Last Name</th>
<th>Phone number</th>
<th>eMail</th>
<th>Emergency contact</th>
<th>Emergency phone</th>
</thead><tbody>';
$blogusers = get_users('blog_id='.$blognumber.'&orderby=nicename&role=author');
foreach ($blogusers as $user) {
$rolodex .= '<tr>';
$userdat = get_userdata($user->ID);
$rolodex .= '<td>'.get_avatar($user->ID,52).'</td>';
$rolodex .= '<td>'.$userdat->first_name .'</td>';
$rolodex .= '<td>'.$userdat->last_name.'</td>';
$rolodex .= '<td nowrap="nowrap">'.$userdat->phonenumber.'</td>';
$rolodex .= '<td><a href="mailto:'.$user->user_email.'">'.$user->user_email.'</a></td>';
$rolodex .= '<td>'.$userdat->{'911contact'}.'</td>';
$rolodex .= '<td>'.$userdat->{'911phonenumber'}.'</td>';
$rolodex .= '</tr>';
}
$rolodex .= '</tbody>';
$rolodex .= '</table>';
return $rolodex;
}
add_shortcode('rolodex', 'rolodex');
With this you go into a page and add [rolodex blognumber=”2″]First Blog[/rolodex] and it spits out a directory.