Friday, May 05, 2006

Self Service Updates in Active Directory

Sometimes AD Administrators would like to allow end users to update and maintain their own information in the directory.  Of course, this is not without risk – more adventurous users will change their title to “Chief Code Monkey” or something perhaps even less professional.  I was an ‘Executive Vice-President’ myself for some period of time and it made for some interesting phone conversations with co-workers that did not know me personally (it turns out people are pretty fearful of talking directly to EVPs).

However, by default, users have permission over a property set in the schema that allows them to update attributes on their own user object.  If these permissions have not been updated from the default, the user will generally have free reign to update their own personal information.  Which attributes, you ask?  Earlier I demonstrated a way to determine what attributes are available on a given class.  This involved finding the object we wanted in the directory and programmatically inspecting the schema.  It turns out there is another way to achieve the same thing, and with a twist – allow us to see which attributes we can update ourselves.

Active Directory (and ADAM) contain a couple attributes called ‘allowedAttributes’ and ‘allowedAttributesEffective’ that tell us all of the attributes on a given object and all of the attributes on a given object that we are allowed to update, respectively.  The first one, ‘allowedAttributes’ produces the exact output as inspecting the OptionalProperties and MandatoryProperties together (without distinction, however).  It gets more interesting with the second one, because it opens the possibility that we can easily generate a dynamic UI to allow the user to update any attributes where their permission allows.

Here is one such example:

<%@ Assembly Name="System.DirectoryServices, Version=2.0.0000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" %>

<%@ Import Namespace="System.DirectoryServices" %>

<%@ Import Namespace="System.Text" %>

<html>

<head>

 

    <script language="c#" runat="server">

 

        static string adsPath = "LDAP://dc=yourdomain,dc=com";

 

        private void Page_Load(object sender, System.EventArgs e)

        {

            if (!Page.IsPostBack)

            {

                SearchResult sr = FindCurrentUser(new string[] { "allowedAttributesEffective" });

 

                if (sr == null)

                {

                    msg.Text = "User not found...";

                    return;

                }

 

                int count = sr.Properties["allowedAttributesEffective"].Count;

 

                if (count > 0)

                {

                    int i = 0;

                    string[] effectiveAttributes = new string[count];

 

                    foreach (string attrib in sr.Properties["allowedAttributesEffective"])

                    {

                        effectiveAttributes[i++] = attrib;

                    }

 

                    sr = FindCurrentUser(effectiveAttributes);

 

                    foreach (string key in effectiveAttributes)

                    {

                        string val = String.Empty;

 

                        if (sr.Properties.Contains(key))

                        {

                            val = sr.Properties[key][0].ToString();

                        }

 

                        GenerateControls(key, val, parent);

                    }

                }

            }

            else

            {

                UpdateControls();

            }

        }

 

        private SearchResult FindCurrentUser(string[] attribsToLoad)

        {

            //parse the current user's logon name as search key

            string sFilter = String.Format(

                "(&(objectClass=user)(objectCategory=person)(sAMAccountName={0}))",

                User.Identity.Name.Split(new char[] { '\\' })[1]

                );

 

            DirectoryEntry searchRoot = new DirectoryEntry(

                adsPath,

                null,

                null,

                AuthenticationTypes.Secure

                );

 

            using (searchRoot)

            {

                DirectorySearcher ds = new DirectorySearcher(

                    searchRoot,

                    sFilter,

                    attribsToLoad,

                    SearchScope.Subtree

                    );

 

                ds.SizeLimit = 1;

 

                return ds.FindOne();

            }

        }

 

        private void GenerateControls(string attrib, string val, Control parent)

        {

            parent.Controls.Add(new LiteralControl("<div>"));

 

            TextBox t = new TextBox();

            t.ID = "c_" + attrib;

            t.Text = val;

            t.CssClass = "txt";

 

            Label l = new Label();

            l.Text = attrib;

            l.AssociatedControlID = t.ID;

            l.CssClass = "lbl";

 

            parent.Controls.Add(l);

            parent.Controls.Add(t);

            parent.Controls.Add(new LiteralControl("</div>"));

        }

 

        private void UpdateControls()

        {

            SearchResult sr = FindCurrentUser(new string[] { "cn" });

 

            if (sr != null)

            {

                using (DirectoryEntry user = sr.GetDirectoryEntry())

                {

                    foreach (string key in Request.Form.AllKeys)

                    {

                        if (key.StartsWith("c_"))

                        {

                            string attrib = key.Split(new char[] { '_' })[1];

                            string val = Request.Form[key];

 

                            if (!String.IsNullOrEmpty(val))

                            {

                                Response.Output.Write("Updating {0} to {1}<br>", attrib, val);

                                user.Properties[attrib].Value = val;

                            }

                        }

                    }

                    user.CommitChanges();

                }

            }

 

            btnSubmit.Visible = false;

            Response.Output.Write("<br><br><a href=\"{0}\">&lt;&nbsp;Back</a>", Request.Url);

        }

 

    </script>

 

    <style>

 

    .lbl

    {

        margin-left: 25px;

        clear: left;

        width: 250px;

    }

 

    .txt

    {

        width: 250px;

    }

</style>

</head>

<body>

    <form id="main" runat="server">

        Data for user:

        <%=User.Identity.Name%>

        <br>

        <br>

        <asp:Label ID="msg" runat="server" />

        <asp:Panel ID="parent" runat="server" />

        <asp:Button ID="btnSubmit" runat="server" Text="Update" />

    </form>

</body>

</html>

Pretty easy, eh?  Sure, this one was whacked together in about 20 minutes, but you could create something similar that takes care of the single- vs. multi-value attribute treatment and make it a whole lot prettier with a little more effort.

This should be fairly obvious – but it bears mentioning:  This requires you to use Integrated Windows Authentication with impersonation and your IIS server must be set for delegation.  The whole point of this exercise was to allow the user to update their own information using their own credentials.  Using a service account will show you what attributes the service account has permission to update on the object.

(thanks Paul for the css – yeah, I am teh suck on UI)