Author: Jorg Wegener
Editor: David Schultz
Reviewer(s): Teun Duynstee, Neil Piggot
Introduction
A directory service is a good alternative when you have to handle data in a more flexible way than a traditional relational database can offer. It is very difficult to store complex data structures in a relational database, which is more optimized for a fixed structure of a flat table. You often have to span data over multiple tables or have to extensively use null values to store different classes which were inherited from an abstract base class. You may also need self-referencing foreign keys, triggers or complex stored procedure to retrieve the required information.
Think about a single user in a relational database. It may contain some mandatory information, like the user name, the first and last name; and it may contain optional information, like a mailing address, and a phone or fax number. Normally, you would allow null values for those fields. But this solution is not very flexible, what happens if you need to support several phone or fax numbers for a single user? In such situations you have to create a new table, which would contain only this information. A directory service makes it a lot easier to store multiple values for a single field. Out of the box several attributes already support this feature.
A directory service was designed and optimized for storing and querying data which is managed in a hierarchical manner. If you have tried this before in a relational database, you probably needed several queries to retrieve all the required data. Imagine a midsize company, which is organized in several units, departments and teams. To represent this information in a relational database, you have to use self-referencing keys as shown in the table below:
| ID | DisplayName | ParentID |
|---|---|---|
| 1 | MyCompany | NULL |
| 2 | Software Development | 1 |
| 3 | Business Software | 2 |
| 4 | Project Team XY | 3 |
In this example, other tables have to maintain other related data - like user data. A directory service is less complicated and easier to maintain. You just have to add entries, which will act like directories on you local file system.
/MyCompany /Software Development /Business Software /Project Team XY
To address the object for the project team you have to use a special reference:
OU=Project Team XY,OU=Business Software,OU=Software Dvelopment,CN=MyCompany
This could be a valid reference for the project team and is the so called Distinguished Name for referencing objects. Any object in the directory can be addressed by using this syntax. You can split the path by each comma where each part is in the format key and value. So the directory can look at what path is used to retrieve the requested object.
The schema defines what kind of classes and attributes are available in the directory. It contains all the information about the structure and defines what attributes can be used and what kind of hierarchy can be created. So each entry in the directory has to be validated against this schema. Of course you can modify and extend the schema to fulfill your own needs. Because Microsoft doesn't allow the deletion of class definitions in the schema, you should be very careful about only adding new definitions when necessary. Normally it is more practical to use an existing schema rather than define new ones. By using well known standard schemas, it is also a lot easier to exchange data with other systems or to use the directory service for multiple applications.
If you decide to extend an existing schema, it should be pointed out that this can be done independently from other existing directories. This allows departments or applications to use a replicated copy of an existing directory service without the modifications to the schema. The central Active Directory is kept untouched in this scenario.
Active Directory is a well known implementation of a directory service. It offers a central database of information about the network environment and user details. Unfortunately Active Directory has a few disadvantages: Firstly, you need a Windows domain to run your own Active Directory and secondly, it contains a lot of overhead information about services and infrastructure you may not need.
To fulfill the needs of a really lightweight directory service for small client or web applications, Microsoft offers ADAM as an alternative. It could be called the smaller brother of Active Directory, because it is a fully compatible implementation, but without all of the overhead of a Windows domain.
Although ADAM is lighter, it has several advantages besides being a full implementation of Active Directory. It is possible for example to install ADAM several times on a single computer. Every installation is a so-called instance and is hosted in its own Windows service which can be started or stopped independently from each other. Each instance is listening on a separate TCP/IP-Port (which does not have to be the default port), so if you want to connect to your ADAM instance, you need to change the port number. Every instance works completely separate from each other.
Additionally any instance can contain several partitions, which can be compared with a partition on your local hard disk. These partitions exist independently from each other and are storing their data in their own logical storage. Note that the difference is made by ADAM itself - finally only one file per instance is used, which contains all of the partitions. Creating a new partition is less expansive than a new instance, because each instance is hosted in its own Windows service, it requires more memory and processor time than a partition. A new partition can easily be added to or removed from an existing instance.
A partition is the final storage for the information - and, if you want to store information, you need to create a partition for your own use. ADAM itself uses two partitions for its own management of the instance. The first partition is called configuration and contains all the settings and options for managing ADAM. The second partition schema, contains the definition of all objects and attributes which are supported in the instance. Every object must be validated against this schema which is responsible for all partitions in the instance. It is important to know this, because every change in the schema takes affect on all partitions.
Because ADAM doesn't create its own domain, you have to enter a virtual domain name. This domain name has no effect on existing domains or Active Directories. It is only used to make it easier for the naming conventions and cannot be used for maintaining a Windows domain.
There are many real world situations where a directory service is very useful. Most of them have simple user authentication in common, which is commonly referred to as Single Sign On (SSO). This functionality helps the user to handle the accounts for several different applications, because only one credential is needed to access them all. This approach can have a huge benefit for extranet or intranet solutions in a corporate network.
In this article I will demonstrate these benefits with a simple user management application. The following example will take advantage of the central directory service of ADAM. .NET 2.0 offers some new features and controls which will allow us to implement the solution with less code then under previous versions of .NET. Although I am using some of the new features, like the new MembershipProvider, it is also possible to achieve the same functionality with previous versions of .NET.
System Requirements
- Active Directory in Application Mode (ADAM)
- Windows Server 2003, Windows 2000 or Windows XP
- .NET Framework 1.x
- .NET Framework 2.0 for new Authentication Features in ASP.NET
Installing and Compiling the Sample Code
To run the sample project, you have to copy the download file to your local hard drive. Create a new temporary folder and extract the files to it. Then open the folder Administrative Tools in Control Panel and start the Internet Information Services Manager.
If you run the application on Windows XP, you have to add a new virtual directory. On Windows 2000 Server or Windows Server 2003 you also have the ability to add a new virtual webserver. In both cases specify your temporary folder as the target. Make sure you have enabled ASP.NET on your webserver, which is disabled on Windows Server 2003 by default.
Installing ADAM
The installation package of ADAM can be downloaded from the Microsoft website (see the Related Links section). The wizard is very easy to use and offers the opportunity to configure the application during the setup. This is the recommended way to configure it because it offers the easiest way to complete the configuration.
In the first step you can choose between creating a new unique instance or adding a replication of an existing instance of ADAM. The last option offers you the ability to easily replicate the data of another ADAM instance on your network. This is useful for a centrally maintained directory service which is replicated to several branches in the wide area network or even for offline users. In this case you want to create a new instance.
In the next steps of the wizard you can define the name and the port numbers for your instance. Be careful with the choice of the name because it is also used for the name of the Windows service. The wizard offers to create a new partition for the new instance. Although it is possible to later add as many partitions as you want, it is recommended to create an initial partition in this step.
The last steps of the wizard are relatively easy. You can choose the location of your data and you can also import some of the useful schemas. If you don't choose any schema, your ADAM instance will be completely empty and you will have to populate your own schema. It is easier to import some of the more commonly used schemas now during the setup. For the following examples I will need the schemas InetOrgPerson, User and UserProxy. If you later want to use Microsoft's Authorization Manager you will also need the schema AZMan. This will allow for the storage of a lot useful information about a user, like their name or their mailing address.
Discovering the Directory
During the installation of ADAM a program called ADSI Edit is installed (see Figure 1). It is a friendly tool for editing objects in the directory and can also be used for changing the schema and configuration.

Figure 1. The ADSI Tool
To get access to ADAM via your code is very similar to accessing Active Directory. Both directories offer their services over ADSI and the LDAP protocol (a standard that works behind the scenes underneath ADSI). You can use the common classes in the namespace System.DirectoryServices to get access to the directory. The port number may be different if you have multiple installations of ADAM on your machine.
The Sample Application
The following code snippet shows how easy it is to get access to the directory. For this article I am using the default port 389 and an example Distinguished Name which may be different then your local installation. It contains three major parts: the protocol used (LDAP), the name or address of the server with the port used (localhost and 389) and the domain name, which you have specified during the installation. In my case I have used the domain TheEdge.de, which has to be translated into the format DC=TheEdge,DC=de.
using System.DirectoryServices;
// ...
DirectoryEntry rootEntry;
rootEntry = new DirectoryEntry("LDAP://localhost:389/" +
"DC=TheEdge,DC=de");
The class DirectoryEntry is easy to use because it allows you to instantiate directly any well known object of the directory service. The object rootEntry now contains all of the relevant information about the first entry which will also be the parent for all other upcoming entries. By using a directory service you don't have to create a query like in a relational database, instead you can directly retrieve all the required information.
foreach (string propertyName in rootEntry.Properties.PropertyNames)
{
for(int i=0; i<rootEntry.Properties[propertyName].Count; i++)
{
Console.WriteLine(" {0,-20} {1}",
(i == 0 ? propertyName : ""),
rootEntry.Properties[propertyName][i].ToString());
}
}
Each entry in the directory can have different attributes, because there isn't a fixed structure like in a table from a relational database. What attributes for what objects are available depends on the classes that each entry refers. In the foreach statement above I am first requesting the names of all available properties and than writing inside the for statement the values to the console.
You can read these values by enumerating the collection of the specified attribute. The attributes which support multiple values are only specified in the current schema. So you have to handle and validate this part in your code, what attributes can have only one a single value, multiple values or even none (because it is optional). In either case you have to use the collection for the access.
Besides the attributes you can also retrieve a list of the containing objects. Because objects can contain other objects, you can easily build your own hierarchy by just adding a few objects to the directory.
foreach(DirectoryEntry childEntry in rootEntry.Children)
{
Console.WriteLine(childEntry.Name);
}
The code above will print all subentries of the root element. Surely a new installation of ADAM won't contain many interesting entries, but it is nice to see the given structure of the directory. A fresh installation of ADAM should give you the following result as shown in Figure 2, which shows how, with a few lines of code you can retrieve the full structure and sub objects of a specified entry.

Figure 2. Retrieving the structure of an entry
This example provides an excellent overview of the root entry. You can see what attributes the object contains and what values they have. On top of this list you can see the attribute objectClass which defines what schemas are used for the root object. In this case the schemas top, domain and domainDNS are specified. So the root object can take advantage of all attributes of each specified schema. This is quite different from objects in most other programming languages. Most of them don't allow objects to inherit from several classes. Maybe it is more comparable to interfaces rather then inheritance. There are also some other useful attributes, like the dates when the object was created and modified.
At the end you can see a listing of some objects which are subentries of the root entry. Like the current root entry, they are just entries like any entry in the directory. If you want to get more details about the object Roles, you just have to specify the distinguished name:
DirectoryEntry rolesEntry;
rolesEntry = new DirectoryEntry("LDAP://localhost:389/" +
"CN=Roles,DC=TheEdge,DC=de");
Adding a New User
In the sample application I want to use the directory for to manage users in a website. The directory will be responsible for maintaining the data of all registered users. Of course you can retrieve the data for the users by using the class DirectoryEntry, but for the daily business of editing, changing and deleting users it may not be very handy. For ease of maintenance, you'll be implementing a new user class which will offer a better way to get access or to modify the data in the directory. This will be a good foundation for how you can easily register and authenticate the users on the web site.
public class User {
private const string path = "LDAP://localhost:389/DC=TheEdge,DC=de";
// default constructor for creating new users
public User(string userName) {
this.Username = userName;
}
// private constructor for loading data
private User(DirectoryEntry directoryEntry) {
distinguishedName = directoryEntry.Path;
UserName = (string)directoryEntry.Properties["userPrincipalName"][0];
FirstName = (string)directoryEntry.Propertires["givenName"][0];
LastName = (string)directoryEntry.Properties["sn"][0];
MailAddress = (string)directoryEntry.Properties["mail"][0];
}
// internal string reminds us what user we are handling
private string distinguishedName;
public string UserName;
public string FirstName;
public string LastName;
public string MailAddress;
public void Save() {
if (string.IsNullOrEmpty(this. distinguishedName)) {
// insert new user to directory
} else {
// update existing user
}
}
public void SetPassword(string newPassword) {
// setting password for this user
}
public static User GetUserByMailAddress(string mailAddress) {
// load
}
}
This code snippet above provides an overview of the new User class, which will offer some of the most common functionalities for the user management. The class implements two constructors: a default public constructor, which can be used to create a new user account, and a private constructor, which is used by internal methods to load the data of the requesting user.
The class contains some properties with information about the current user, like the first name, the last name and their e-mail address. If you retrieve the data of an existing user, the class will set the Distinguished Name in the private field distinguishedName. So you are then able to refer to this user in the directory service very easily without using complex queries. If the field doesn't contain a valid value (null or empty) it indicates that you are currently handling a non existing user. The method Save() is already checking for this field, but also other ones can profit by this indicator later. In the sample application you are only using a few of all the available properties, but you can also simply extend the class with more properties as need be.
public void RegisterButton_OnClick(object sender, EventArgs e) {
User user = new User("joergw");
user.LastName = "Wegener";
user.FirstName = "Jörg";
user.MailAddress = "joergw@theedge.de ";
user.Save();
}
The registration of a new user should be as simple as possible. With a few lines of code you can create a new user in the directory and assign the most important properties. Of course the most important part of this code snippet is found in the Save() method.
public void Save() {
if (string.IsNullOrEmpty(this. distinguishedName)) {
// INSERT
rootEntry = new DirectoryEntry(path);
userEntry = rootEntry.Children.Add("CN=" + UserName, "user");
userEntry.Properties["userPrincipalName"].Add(UserName);
userEntry.Properties["displayName"].Add(FirstName + " " + LastName);
userEntry.Properties["sn"].Add(LastName);
userEntry.Properties["givenName"].Add(FirstName);
userEntry.Properties["mail"].Add(MailAddress);
userEntry.CommitChanges();
userEntry.RefreshCache();
distinguishedName = (string)userEntry.Properties["distinguishedName"][0];
} else {
// update existing user
}
}
The root entry is the first directory entry which you had seen earlier in this article. You simply put the new users in this root directory by using the method Add(). It requires the RDN attribute and the class of the object you want to create. RDN stands for Relative Distinguished Name and describes a local path - in this case the path under the root object.
The next lines just add some properties to the entry you have used in the class. In the end you commit the changes and ADAM will create a brand new entry in the directory. A lot of checks will be done in the background before the entry will be created. First of all, ADAM is looking for a unique identifier. This is the RDN attribute which you have specified at the beginning of the Add() method. It can be compared to the primary key in a relational database, but with the difference that a RDN only needs to be unique in each container. So if you try to create a second user with the same common name (CN) it will fail with an error. By contrast, some other attributes, like userPrincipalName use an index and as a result have to be unique within the whole directory.
The last two lines are just for the refreshing of your own User object. Because you just created the entry, you have to remember it by saving the new distinguished name.
Updating a User
The update is a lot easier and should look familiar. You just retrieve the needed entry of the user and modify the parameters. After the commit all changes should be saved back into the directory.
public void Save() {
if (string.IsNullOrEmpty(this. distinguishedName)) {
// INSERT
} else {
// UPDATE
userEntry = new DirectoryEntry(distinguishedName);
userEntry.Properties["userPrincipalName"][0] = UserName;
userEntry.Properties["displayName"][0] = FirstName + " " + LastName);
userEntry.Properties["sn"][0] = LastName;
userEntry.Properties["givenName"][0] = FirstName;
userEntry.Properties["mail"][0] = MailAddress;
userEntry.CommitChanges();
}
}
At this point you are able to create and to modify users in the directory. But without a password the user won't be able to be authenticated. The password can only be set after the creation of the initial entry. So the first thing you need to do is check for the existence of the entry.
The maintenance for the password is a complex area of ADAM. Normally you need a secure connection to your instance (like SSL), so no one who is listening on the wire can see the password. For internal usage or demonstration purposes it is not necessary to fulfill all of the security requirements, so it is better to turn off this specific policy. In order to do this, open the ADSI Edit application and connect to the configuration of your ADAM instance. Then navigate to the path CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration and right click on Directory Service and select Properties. Change the value of dSHeuristics to "0000000001001". After this you will be able to change passwords without a secure connection. However, in a production environment it is not recommended to do this.
Before you can start to set passwords, you have to retrieve the entry of the directory with an extended method. It is a bit more complex than just retrieving the entry for read. The entry offers a method called SetPassword() which can be invoked to set the password of the current user.
private const long ADS_OPTION_PASSWORD_METHOD = 7;
private const int ADS_PASSWORD_ENCODE_REQUIRE_SSL = 0;
private const int ADS_PASSWORD_ENCODE_CLEAR = 1;
public void SetPassword(string newPassword) {
AuthenticationTypes authTypes;
DirectoryEntry userEntry;
if (string.IsNullOrEmpty(this.distinguishedName))
throw new ApplicationException("user does not exists");
authTypes = AuthenticationTypes.Signing |
AuthenticationTypes.Sealing |
AuthenticationTypes.Secure;
userEntry = new DirectoryEntry(
this.distinguishedName, null, null, authTypes);
userEntry.RefreshCache();
userEntry.Invoke("SetOption", new object[]
{ ADS_OPTION_PASSWORD_METHOD, ADS_PASSWORD_ENCODE_CLEAR });
userEntry.Invoke("SetPassword", new object[] { newPassword });
}
Searching For a User
To retrieve users, the User class also contains several static methods which return a single user or several users by different criteria. Probably the most common case is to search for a user by referring to their e-mail address. This occurs several times in the sample application, for example when checking for the existence of a user or for the login.
User user = User.GetUserByMailAddress("joergw@theedge.de");
The implementation of this method is pretty simple as seen below:
public static User GetUserByUsername(string userPrincipalName) {
DirectorySearcher searcher = new DirectorySearcher();
searcher.SearchRoot = rootEntry;
SearchResult result = searcher.FindOne(
"(userPrincipalName=" + userPrincipalName + ")");
if (result == null) return null;
return new User(result.GetDirectoryEntry());
}
The GetUserByUserName() uses the object DirectorySearcher because it is a lot faster than iterating the entire hierarchical tree in the directory. It would not be wise to retrieve and to analyze every single entry, because a directory can possibly contain thousands or millions of entries and such a query would take too long. Instead you can use a single query, which will be processed by the directory server itself.
A directory service uses a different syntax for searching than does a relational database. But it is easy to learn. The foundation for a new query is always parentheses - you have to use them in every query, whether they contain one or more conditions. In this case you just compare the attribute mail to the parameter. Of course you should never inject an unchecked parameter into a query, but this is only for demonstration purposes. Beside the security risk you can potentially have other problems, because you can possibly get other objects than users. This happens because other classes use the attribute mailAddress which you would also retrieve. Because you are only interested in the users, you have to tell the directory service to look only for the specific type of object.
searcher.FindOne("(&(mail=" + mailAddress + ")(objectClass=user))")
Next let's develop a more complex query. In this case both conditions for mail and objectClass have to be true. This is defined by the & character in the beginning of the query. It is also possible to specify an or by using a single | (Pipe). The DirectorySearcher offers two types of searching: find all possible entries and just find one entry. The second method will only return the first entry of all the possible matches. Even if there are several other possible matches only the first one found will be returned.
One frequent concern is how fast the queries will run. This really depends on the attributes you are looking for. Some attributes use a global index, so queries against these attributes are incredibly fast. It is important to understand that these indexes are used globally for the whole partition. So it is possible to look for users in a very fast query, which may even spread all over the directory.
Registering a User
Now you need a simple registration form for the sample website in order to capture the users' information. Using this page any user can register to get access to the website as shown in Figure 3

Figure 3. A simple form offering the user to self register
By clicking the Create Account button, a new user will be created with the code below.
public void RegisterButton_Click(object sender, EventArgs e) {
Page.Validate();
if (!Page.IsValid())
return;
User user = new User();
user.FirstName = FirstName.Text;
user.LastName = LastName.Text;
user.MailAddress = MailAddress.Text;
user.Save();
Response.Redirect("/");
}
The implementation is pretty simple, you just create a new User object and set the proper attributes and call Save(). All the work is done in the User object you created previously.
Authenticating a User
Many websites offers their registered users more information and better features than anonymous user. A lot of exciting features are only possible with authenticated users, because personal settings and preferences are bound to a specific user. For all of these types of features you need to authenticate the visitor. ASP.NET 2.0 makes authentication a lot easier then in the past because you can use some standard controls and providers.
You need some additional entries in the web.config file. Many of the security settings can be defined as well as what user database is being used.
<connectionString>
<add
name="TheEdgeAdam"
connectionString="ldap://localhost:389/DC=TheEdge,DC=de" />
</connectionString>
The managing of connections has been made easier with the <connectionString> section. You just add the connection string to the local ADAM instance by referring to the name TheEdgeAdam. The suffix Adam helps later to distinguish from other names within the configuration file.
<system.web>
<authentication mode="Forms">
<forms
name=".ADAuthCookie"
timeout="10" />
</authentication>
<authorization>
<!—- optional: deny all anonymous users
<deny users="?" />
<allow users="*" />
->
</authorization>
<system.web>
Within the configuration you can also specify whether to deny or allow access for all anonymous users. In cases where personalization or customizing have been made it, it would often times still make sense for anonymous users an optional login, so they can still read a non customizable or a default version of the website.
<membership defaultProvider="TheEdgeMembershipProvider">
<providers>
<add
name="TheEdgeMembershipProvider"
type="System.Web.Security.ActiveDirectoryMembershipProvider"
connectionStringName="TheEdgeAdam"
attributeMapUsername="userPrincipalName"
connectionProtection="None"
connectionUsername="joergw"
connectionPassword="secure" />
</providers>
</membership>
The Membership Provider is new in ASP.NET 2.0 and manages the authentication for us. In this case you add an Active Directory Provider, which allows us to get access to ADAM. The attribute attributeMapUsername is special for managing the usernames. Here you can specify what type of attribute the provider should use for the username. You can use something like userPrincipalName or mail as an attribute, which identifies the user.
If you don't want to specify a username and password for the LDAP connection, ASP.NET tries to connect to ADAM over a secure connection (SSL). This requires a dedicated certificate, which was generated by a trusted Certificate Authority (like your local Domain Controller or Verisign). If you don't need a secure connection, because your web server and ADAM are on the same machine, you can also use the authentication by using username and password.
Now you just have to create a login page, ASP.NET 2.0 offers a useful Login control, so you don't need to code anything special.
<asp:Login runat="server"></asp:Login> <p> If you don't have an account, please <a href="Register.aspx">register</a> first. </p>

Figure 4. The login page of the new ASP.NET 2.0 Login control.0
The Login control as seen in Figure 4 uses the Membership Provider which you have specified in your configuration file and authorizes the users, which are maintained in the directory service specified.
Working with User Data
Of course you have to know which user is currently visiting the website so you can retrieve the correct personalized data. For this purpose you can use the User object and Identity which contain all the required information about the current user. You can also determine whether it is an authenticated user or not.
User user;
if (Page.User.Identity.IsAuthenticated) {
user = User.GetUserByUsername(Page.User.Identity.Name);
}
The Identity class is an abstract base class which contains only the data which fits the supported authentication mechanism. Because you are using the forms authentication you can cast the Identity object to a FormsIdentitiy class, which allows us to get more useful information about the user.
FormsIdentity id; FormsAuthenticationTicket ticket; id = (FormsIdentity)Page.User.Identity; ticket = id.Ticket; Username.Text = Server.HtmlEncode(Page.User.Identity.Name); TicketName.Text = ticket.Name; CookiePath.Text = ticket.CookiePath; TicketExpiration.Text = ticket.Expiration.ToString(); Expired.Text = ticket.Expired.ToString(); Persistent.Text = ticket.IsPersistent.ToString(); IssueDate.Text = ticket.IssueDate.ToString(); UserData.Text = ticket.UserData; Version.Text = ticket.Version.ToString();
The resultant ticket information contains a lot useful information about the current user. Figure 5 below is an example of the results.

Figure 5. User ticket information
Limitations
ADAM (and even Active Directory) doesn't support transactions. Only modifications on one single object are done in a single transaction. Another consideration is the fact that write operations can be slow in comparison to a relational database. So you shouldn't use a directory for volatile data, because it is well optimized for reading and querying data as apposed to writing data.
Conclusion
A directory has a lot of advantages in comparison to a relational database when dealing with data structures that are not fixed. The use of a single class or even the combination of multiple classes offering a flexible way to store objects with different attributes. A directory is managed in a hierarchical way which provides the ability to set every object in the directory into a different relation depending on its place within the hierarchy.
ADAM is a very lightweight directory services. It is fully compatible with Active Directory, but doesn't require the Windows Server System or its own Windows Domain. Therefore it doesn't contain all the information about the infrastructure or network, which is normally stored in the Active Directory. It is perfect for web applications, because ADAM can be installed multiple times on a single computer. This provides a web hosting company the ability to offer directory services to its clients.
Additionally the .NET Framework 2.0 includes several improvements for Active Directory and ADAM. The new provider model is designed to easily plumb the directory with a web application together. This simplifies the authorization and the user management dramatically.
