Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Asp Net 2.0 Security Membership And Role Management

.pdf
Скачиваний:
51
Добавлен:
17.08.2013
Размер:
12.33 Mб
Скачать

Chapter 12

Using the ProviderUserKey Property

The ActiveDirectoryMembershipUser class conveniently returns the user’s SID in the ProviderUserKey property. If you have other code that manipulates users via their SID, you can use this property — both by reading it for use elsewhere as well as for looking up an

ActiveDirectoryMembershipUser instance by SID.

The following code outputs the string representation of a user’s SID:

using System.Security.Principal;

...

//code to retrieve a MembershipUser in the mu variable

...

SecurityIdentifier sid = (SecurityIdentifier)mu.ProviderUserKey; Response.Write(“The user’s SID is: “ + sid.ToString());

The output from this looks like:

The user’s SID is: S-1-5-21-2424360418-2194369526-2737752971-1115

This format is the Security Descriptor Definition Language (SDDL) representation of the objectSID attribute on a user object in the directory. You can use the SDDL representation to create your own instance of a SecurityIdentifier.

//Load a user instance using the SID

string sddlSID = sid.ToString(); //gets the SDDL form SecurityIdentifier pkey = new SecurityIdentifier(sddlSID);

ActiveDirectoryMembershipUser admu =

(ActiveDirectoryMembershipUser)Membership.Provider.GetUser(pkey, false);

Response.Write(“The username is: “ + admu.UserName + “<br/>”); Response.Write(“The user’s SID is: “ +

((SecurityIdentifier)admu.ProviderUserKey).ToString());

This code takes the SecurityIdentifier instance that was returned from the previous sample code and converts it into the string SDDL syntax. It then constructs a new instance of a SecurityIdentifier passing the SDDL representation to the constructor. The resultant SecurityIdentifier is then passed to ActiveDirectoryMembershipProvider as the key for looking up a user in the directory. When you run this code you see that with the SDDL version of the SID, you can successfully get back to the original user object:

The username is: testusernestedinpopa@corsair.com

The user’s SID is: S-1-5-21-2424360418-2194369526-2737752971-1115

Working with Active Director y

Out of the box, there is a reasonably high likelihood that you can get the provider to start working with an AD domain. Because the first hurdle you will face is the question of connectivity to the directory, getting the correct connection string is important. Luckily, if you know what your options are it is also

482

ActiveDirectoryMembershipProvider

pretty easy to setup. For starters, you can configure a sample application with the provider that attempts to retrieve a user object from the Users container that is found on all domains. Because

ActiveDirectoryMembershipProvider is not configured in either machine.config or the root web.config files, you will need to explicitly configure it in web.config.

<membership defaultProvider=”appprovider”> <providers>

<clear/>

<add name=”appprovider” type=”System.Web.Security.ActiveDirectoryMembershipProvider, ...” connectionStringName=”DirectoryConnection” />

</providers>

</membership>

Because none of the other provider-specific configuration options are used, the provider will connect to the directory using the underlying process credentials. This is an important point because it means that, by default, when running on IIS6 the provider will connect to your directory as NETWORK SERVICE (that is, the machine account from the perspective of the directory server). For now, let’s use a connection string that looks like:

<connectionStrings>

<add name=”DirectoryConnection” connectionString=”LDAP://corsdc2.corsair.com”/> </connectionStrings>

This style of connection string tells the provider to explicitly connect to a specific directory server. Note, though, that there is no other information in the connection string, which means that the provider will automatically attempt to bind to the Users container. To see whether this configuration works, a simple test page writes out some of the properties of a user that already exists in the directory:

MembershipUser mu = Membership.GetUser(“demouser@corsair.com”); Response.Write(“Email address is: “ + mu.Email + “<br/>”); Response.Write(“Creation date is: “ + mu.CreationDate.ToString() + “<br/>”);

When I ran this sample app against a directory server, the following information was returned:

Email address is: someemailaddress@corsair.com

Creation date is: 3/6/2005 1:12:57 PM

This isn’t exactly earth-shattering information, but if you think about it, with only some standard configuration entries and some boilerplate Membership code, you are now accessing a user object in a directory. No need for kung-fu coding with classes in the System.DirectoryServices namespace let alone mucking around with the older ADSI programming APIs.

You can make things more interesting by first trying different variations of the connection string. One variation simply points the application at the domain, as opposed to a domain controller.

<add name=”DirectoryConnection” connectionString=”LDAP://corsair.com”/>

Notice how the connection string no longer points at a specific server. Now the provider is simply leveraging the default connectivity behavior supported by AD where you can just supply the DNS name associated with the domain and the underlying network stack performs the magic of looking up special directory service entries in DNS to route the request to an actual domain controller.

483

Chapter 12

Although this type of connection string is interesting to know about, and it can be useful in a development environment just to get things up and running, in an extranet environment you need to be careful with this type of connection string. Because you aren’t guaranteed a connection to any specific directory controller, you can end up in cases where an operation against a user object occurs against one domain controller, and then at a later point in time the provider connects to a different controller that has not yet received the replicated changes. This behavior is not a bad thing; you just need to be aware of whether your application can tolerate this. The nice thing about a serverless connection string is that your application isn’t tied to the uptime of any specific directory server. Instead, the provider will connect to whatever is available, and if a DC goes down then the provider will simply be routed to a different server.

Another connection string variation (and probably the most common one you will use) includes the container name.

<add name=”DirectoryConnection” connectionString=”LDAP://corsdc2.corsair.com/CN=Users,DC=corsair,DC=com”/>

With this connection string, the provider will bind to the container specified after the server name. In this case, the connection string is binding to the Users container. If you have ever used ADSI or System

.DirectoryServices, this should be a familiar syntax to you for binding to the Users container.

If you use the provider in an extranet environment where different user populations are segmented into different organizational units (that is, OUs), then you would use a connection string like the following:

<add name=”DirectoryConnection” connectionString=”LDAP://corsdc2.corsair.com/OU=UserPopulation_A,DC=corsair,DC=com” />

Now instead of referencing a built in container, the connection string references an OU that was created in the domain. In this case, the OU is a peer of the Users container. However, you can just as easily bind to OUs that are nested any number of levels deep.

<add name=”DirectoryConnection” connectionString=”LDAP://corsdc2.corsair.com/OU=SomeNestedOU,OU=UserPopulation_A,DC

=corsair,DC=com” />

For nested containers, you just build up the second part of the connection string with the walk-up path from the nested OU to the top of the container hierarchy.

UPNs and SAM Account Names

In the previous examples, the provider was implicitly binding to the directory and looking for user objects based on the user principal name. In my test directory, I always created a UPN for each new user, so the provider can find user objects and bind to them. For older directory infrastructures, though, user principal names may not be in wide use, or they may not even be used at all. The provider supports binding to user objects using the sAMAccountName attribute instead. However, you need to explicitly configure this behavior. The configuration for the provider using a SAM account name looks like:

<add name=”appprovider” type=”System.Web.Security.ActiveDirectoryMembershipProvider, ...” attributeMapUsername=”sAMAccountName” connectionStringName=”DirectoryConnection” />

484

ActiveDirectoryMembershipProvider

With this configuration, the provider expects that any usernames passed to its methods will be just the username portion of the NT4-style DOMAIN\USERNAME format. For example, the following code retrieves the user object for CORSAIR\demouser:

MembershipUser mu = Membership.GetUser(“demouser”);

Notice how the username parameter doesn’t include the domain identifier. This is important because if you attempt to pass full NT4-style usernames to the provider, the calls will never return anything (that is, if you pass DOMAIN\USERNAME the provider is literally looking for a user object whose SAM account name is DOMAIN\USERNAME ). Because the provider already knows the domain within which it is operating, it does not need the domain portion of the username. Remember that the provider is effectively acting like a database provider — except that the “database” is really an LDAP server. When the provider looks for objects using a SAM account name, it is performing an LDAP search where the sAMAccountName attribute on the directory’s user object equals a specific value. As a result, you only need to supply the username.

If you happen to set up ActiveDirectoryMembershipProvider, and you are unable to retrieve any existing users, keep in mind the attributeMapUsername attribute. It is likely that if the connection string works and you are getting back nulls from methods like GetUser that your directory users have been configured only with SAM account names — and not UPNs. Switching attributeMapUsername over to sAMAccountName is probably the most common configuration step that developers need to make to get the provider working with their directory.

However, if you have been creating user accounts in the directory using the ActiveDirectoryMembershipProvider with its default setting of UPN-style usernames, you may run into a different problem. When you create users in the Active Directory Users and Computers MMC, the UI conveniently auto-selects a domain suffix for your UPN. In fact, the UI remembers previous UPN suffixes that have been used with the tool, and it displays a drop-down list where you can choose any one of them. However, if you create users directly with the provider, you may find yourself creating users with just a username and no suffix (for example, “demouser98” as opposed to “demouser98@corsair.com”). This kind of a UPN will sort of work with Active Directory, but you will find that if you also write code with System.DirectoryServices there are cases where a UPN without an @ will fail. As a result, you should always ensure that UPNs have an @ sign and some kind of domain suffix in them. For Internet-facing sites, it makes sense to create user accounts with some kind of domain suffix — with the user’s email address being the most likely candidate.

This raises the question of whether you should eventually switch your user population over to UPNs. Although as far back as Windows 2000, the guidance was to create users with UPNs, the reality is that many folks still rely on the older NT4-style usernames, especially if their current domain infrastructure was the result of an NT4 domain upgrade. I certainly wouldn’t recommend reworking your user population to use UPNs just because ActiveDirectoryMembershipProvider defaults to UPNs. (That’s why the username mapping is configurable!) However, it does seem to be a recurring theme that UPNs are architecturally preferable. For e-commerce sites or extranet sites that rely on Active Directory, UPNs do make more sense because, typically, you don’t want external users to be aware of AD domain names. Technically, external sites that do this are leaking a little bit of their security architecture to the public by requiring a domain name. Also UPNs frequently mirror a person’s email address, so they can be a more natural username for your website users to grasp.

485

Chapter 12

Container Nesting

You already saw a simple example where nested OUs were used in a connection string. However, container nesting raises some interesting issues when working with the provider. If you have different sets of users in different OUs, and you want some provider operations to span all of these sets of users, how do you go about configuring the provider? Remember that data modification operations can occur only in the container specified by the connection string, whereas search-oriented operations are rooted at the container specified by the connection string.

Using the sample directory structure, so far there are users are laid out as follows:

Cn=Users

demouser

OU=UserPopulation_A

testuserpopA

OU=SomeNestedOU

Testusernestedinpopa

If you use the following connection string:

<add name=”DirectoryConnection” connectionString=”LDAP://corsdc2.corsair.com”/>

then all search operations are rooted at what is called the default naming context for the domain. What this means is that all containers and OUs are considered children of the default naming context, so this type of connection string allows searches to be performed across all available containers. Because the provider performs its search operations using subtree searches, the following code searches across all containers, as well as down through the container hierarchy to its lowest nested level:

MembershipUserCollection muc = Membership.GetAllUsers(); foreach (MembershipUser mu in muc)

Response.Write(“Username: “ + mu.UserName + “<br />”);

The result from running this code is:

Username: appimpersonation@corsair.com

Username: demoadmin@corsair.com

Username: demouser@corsair.com

Username: fradmin@corsair.com

Username: testusernestedinpopa@corsair.com

Username: testuserpopa@corsair.com

Username: uncidentity@corsair.com

The bolded identities are the three accounts used earlier in the chapter. The demouser account as well as all of the other unbolded user accounts are located in the CN=Users container (some of the accounts should be a bit familiar from back in Chapters 1 and 2!). The other two testuser* accounts are from

OU=UserPopulation_A and OU=SomeNestedOU.

486

ActiveDirectoryMembershipProvider

Similarly, if you perform get operations such as:

MembershipUser mu = Membership.GetUser(“testusernestedinpopa@corsair.com”);

the code will return a valid user object because even though the user account is nested two OUs deep, the Get* methods on the provider start their search at the default naming context (because the connection string from earlier doesn’t specify a container) and then work their way down. If you explicitly specify a container hierarchy in your connection string, then get and search methods will be rooted at the container you specify and then searches will work their way down through any remaining container hierarchy.

However, if you attempt to create a new user or delete an existing user, then these operations only occur in the container specified on the connection string. In the case of the sample connection string that doesn’t explicitly specify a container, this means that user creation and deletion only occur in the CN=Users container. There are other provider methods that involve modifying information for a user,

including UpdateUser, ChangePassword, and so on. Although these methods are technically data-modi- fication operations, all of these methods first bind to a specific user in the directory (a get operation) prior to making a change. As a result, updates to existing users also have the behavior of being rooted at a specific point in the directory, and then searching for the user object down through the nested containers.

With this behavior, it is possible to come up with some interesting provider configurations. For example, if your site supports multiple sets of users, you could allocate each set of users to a different OU. You could then configure a separate provider instance for each different OU (and hence each provider instance would have its own unique connection string). These different providers could be used exclusively for create and delete operations. For the rest of your site, you could then configure one more provider pointed at the default naming context or at a root OU, depending on how you structured your containers. This last provider would be used for things like calling ValidateUser or for fetching a MembershipUser object to display information on a page. In this way, you would get the flexibility to create and delete users in different OUs, while still having the convenience of searching, retrieving, and modifying users across the OUs with a single provider.

Securing Containers

So far, the sample code has been running with the credentials of the IIS6 worker process. The reason that the samples have worked so far is that the NETWORK SERVICE account is implicitly considered part of the Authenticated Users group. If you look at the default security configuration in the directory, you will see that this group has rights to list objects in a container as well as having some read permissions on individual object. The concept of read permissions on objects though differs depending on the object in question.

In the case of the provider, the object type you care about are user objects. The default permissions that any authenticated user in a domain has on any other user object in the directory are read general information, read personal information, read web information, and read public information. General information, personal information, web information, and public information are just property sets that conveniently group together dozens of different directory attributes so that permissions can be granted to them without having to spam dozens or hundreds of ACLs on user objects. These default permissions are why the sample pages running as NETWORK SERVICE were able to find the user object in the first place and then read the various directory attributes in order to construct an instance of ActiveDirectoryMembershipUser.

487

Chapter 12

If you attempt to use the sample configuration shown earlier to update an existing user object or create a new user object, you will get a System.UnauthorizedAccessException. The exception bubbles up from the underlying System.DirectoryServices API and is triggered because, for obvious reasons, authenticated domain users don’t have the right to arbitrarily make data modifications to other objects or containers in the directory. This behavior is roughly equivalent to the exceptions you get when you haven’t granted login rights to SQL Server or execute permissions to the Membership stored procedures and you attempt to use the SqlMembershipProvider.

One obvious solution would be to just add rights in the directory granting NETWORK SERVICE the required rights. However, in general this is not the correct approach. Each machine in a domain has a corresponding machine account in the directory. Because the account is comparatively well known, granting broad rights to it is not something you should do. Additionally, if you are running in a web farm, each individual server has a different machine account in the directory that locally is known as NETWORK SERVICE. So if you granted broad rights to the machine account, you would have to repeat this task for each and every server running in your web farm.

A better approach would be to at least assign your application’s worker process a different domain identity and then grant this domain identity the necessary rights in the directory depending on what your code needs to do with the provider. With this approach, if you run multiple machines in a web farm, each web server can be configured with the same domain account for the worker process. For a lot of application scenarios, this is actually a reasonable approach. However, if you need to host multiple applications in a single worker process, with each application having a different set of privileges in the directory, or if you want to configure multiple providers in a single application with each provider having a different set of privileges, then you will need to use explicit provider credentials instead.

The ActiveDirectoryMembershipProvider exposes the connectionUsername and connectionPassword configuration attributes. With these attributes, you can explicitly set the domain credentials that the provider will use when connecting to the directory. Even though the default provider behavior is to revert to either the process credentials, or application impersonation credentials if application impersonation is being used, when explicit credentials are configured the provider always uses them in lieu of any other security identity.

The advantage of using explicit credentials in combination with application specific OUs (as opposed to just using the Users container) is that you have the ability to specify granular permissions for different sets of application users. With the provider configuration attributes you then have the flexibility to fine-tune individual providers to allow only certain operations through specific providers. Let’s see how this works by creating a new admin account to work with the UserPopulation_A container: userpopaadmin. You want this account to have the ability to create and delete user objects, as well as the ability to reset passwords and unlock users.

Remember that for a provider instance to be able to create users, it also needs the ability to delete users (in the event that the multistep user creation process failed) and to set passwords (because part of the process of creating the user is setting password). Note that the ability to set passwords for new accounts as well as reset existing passwords is shown as the Reset Password inside of the security dialogs boxes shown in the MMC.

488

ActiveDirectoryMembershipProvider

The Active Directory Users and Computers MMC has a wizard that steps you through delegating control over containers like the OUs used here. You can open up the MMC to display all of the containers that are currently available in a directory. In the test directory, I am running, right-clicking the UserPopulate_

A container and selecting Delegate Control opens the first step of the wizard as shown in Figure 12-1.

In the next wizard step you can select one or more user/group accounts that will all be granted a specific set of rights over the OU. In Figure 12-2, you can see that I have selected the userpopaadmin account.

Figure 12-1

489

Chapter 12

Figure 12-2

On the next step of the wizard, you can select multiple rights to grant to the accounts. Because you want the admin account to have the ability to create/delete users, reset passwords and unlock users, the first three sets of tasks are selected in the wizard. Figure 12-3 shows these selections.

490

ActiveDirectoryMembershipProvider

Figure 12-3

The final step of the wizard (not shown) just asks for confirmation of the selections. When you click the Finish button on the last wizard step, the security changes take effect. You can see the new set of security rights if you right-click the UserPopulation_A OU and then drill into the security settings for userpopaadmin. Figure 12-4 shows the two sets of rights highlighted in the Advanced Security Settings dialog box.

491