I l @ ve RuBoard |
The following sections offer enterprise application development scenarios that implement the concepts we've discussed thus far in the chapter. Code SecurityWindows security is based on a user 's identity, and code access security is based on an assembly's identity. We'll delve into two important aspects of the Visual Basic .NET code security model: code access security and requesting code permissions. Code Access SecurityCode access security is all about control access to assemblies (or individual methods contained in assemblies). Every assembly loaded by the CLR is assigned evidence that describes its identity. This evidence can be the path or URL from which the assembly was loaded, or it can be a digital signature given to the code by its publisher. To control access to your code, you can simply demand that your caller have a specific identity. For example, to limit access to a shared component to only code from the same publisher, the publisher will sign all its code and then place a demand for that signature within its shared component. The identity permissions found under the System.Security.Permissions namespace are used for this purpose. Identity permissions are provided for the following types of assembly identity: strong name , Authenticode publisher certificate, URL of origin, site of origin, and Internet Explorer security zone. All identity permissions support three types of identity demands, which are described in Table 9-1. Table 9-1. Types of Identity Demands
Because LinkDemand and InheritanceDemand are performed when assemblies are loaded, they can be specified only declaratively using attributes at compile-time. The compiler places the declarative security in an assembly's manifest, where the CLR can read and act upon it when loading that assembly. A straight demand, on the other hand, occurs at run time and can be specified imperatively or declaratively . The following example demonstrates how to make a link demand on a method based on a strong-named method. The public key has been abbreviated for readability. <StrongNameIdentityPermission(SecurityAction.LinkDemand,_ PublicKey:= "002400000...")>_ PublicSharedSubProtectedMethod() 'dosomething EndSub The StrongNameIdentityPermission attribute supports the additional properties Name and Version . By specifying Name , Version , and PublicKey , a client can reliably demand an exact version of an assembly. If the client specifies only Name and PublicKey , the demand will succeed if the assembly name and signature match, regardless of the assembly version. If the client specifies only PublicKey , as in the code example above, the security system will look for only the required signature; this is useful when you want to limit access to a group of code signed by the same key. Attaching a strong name signature to your code involves two steps: creating the strong name key and compiling your assembly with that key. The first step is accomplished by using the SN (strong name) utility provided with the .NET Framework SDK. Below is the command-line syntax for creating a key pair and viewing the public key portion. (You must make an identity demand for code signed with the corresponding private key.) sn-kkeypair.dat sn-pkeypair.datpublickey.dat sn-tppublickey.dat More Info For more information about the strong name tool, see Chapter 8. The second step requires adding a declaration to the assembly to indicate the location of the file generated in step 1: <Assembly:AssemblyKeyFile("keypair.dat")> PublicClassMyClass 'somethinginteresting EndClass You can also delay-sign an assembly. Delayed signing reserves room for the signature in an assembly's manifest but does not actually sign the assembly. It is used when the author of the assembly does not have access to the private key that will be used to generate the signature. You can implement delayed signing by using the AssemblyDelaySign attribute class. More Info For more information about delayed signing, check out the Framework SDK documentation that accompanies Visual Studio .NET. Requesting Code PermissionsYou do not necessarily have control over what permissions are assigned to the code you write, so the CLR provides a mechanism for requesting the permissions code needs in order to run properly. If the code is not granted the required permissions, it will not run. And, because permission requests are stored in an assembly's manifest, the end user can run the SDK tool called PERMVIEW to determine what permissions have been requested by the assembly author and then take the appropriate steps to grant those permissions if she needs the code to run on her machine. Table 9-2 lists the three types of permission requests supported by the .NET runtime. Table 9-2. Permission Request Types
Permission requests can be made only in a declarative fashion and must always be at the assembly level. (The assembly is the unit to which permissions are granted by the security system.) The following code is a request stating that an assembly must have unrestricted access to the file system in order to function: <Assembly:FileIOPermission(SecurityAction.RequestMinimum,_ Unrestricted:=True)> PublicClassFileMover 'somethinginteresting EndClass You can make several requests of the same type, in which case the final permission set requested will be the aggregate of all requests of that type. In the following example, RequestMinimum is used twice with different permissions to state that the assembly must have the ability to use Reflection Emit and perform serialization in order for it to function. <Assembly:ReflectionPermission(SecurityAction.RequestMinimum,_ ReflectionEmit:=True)> <Assembly:SecurityPermission(SecurityAction.RequestMinimum,_ SerializationFormatter:=True)> PublicClassCodeGenerator 'somethinginteresting EndClass The same permission can also appear in requests of different types. For instance, the following example program uses an EnvironmentPermission in each of its three requests ( Minimum , Optional , and Refuse ). Using requests of different types is useful when a permission encompasses a number of operations and you want to ensure that your assembly has the ability to perform some of those operations while being prevented from performing others. Note Any permission you refuse using RequestRefuse will not be granted to your assembly even if you request that same permission using RequestMinimum . In addition to requesting individual permissions, you can also request entire sets of permissions in a compact fashion. The following example shows two requests: one stating that an assembly must have unrestricted access to the file system in order to function, and one stating that it will take any and all other permissions that the security system is willing to grant it. <Assembly:FileIOPermission(SecurityAction.RequestMinimum,_ Unrestricted:=True)> <Assembly:PermissionSet(SecurityAction.RequestOptional,_ Name:= "FullTrust")> PublicClassFileMover 'somethinginteresting EndClass The example shows how to request a permission set by name, but it is also possible to use a custom permission set representing the exact permissions you want. For more information on how to do this, search on PermissionSetAttribute in the .NET Framework SDK Reference. The PERMVIEW tool is useful for verifying that your permission requests are correct. You can run PERMVIEW on a compiled assembly to read the permission requests out of the assembly's manifest and display them, as shown here: C:\>permviewfilemover.exe Microsoft(R).NETFrameworkPermissionRequestViewer.Version1.0.XXXX.0 Copyright(C)MicrosoftCorp.1998-2001 minimalpermissionset: <PermissionSetclass="System.Security.PermissionSet" version="1"> <IPermissionclass="System.Security.Permissions.FileIOPermission" version="1" Unrestricted="true"/> </PermissionSet> optionalpermissionset: <PermissionSetclass="System.Security.PermissionSet" version="1" Unrestricted="true"/> refusedpermissionset: Notspecified User IdentityUser identity is a common means of controlling access to a business application or limiting the options available within that application. The System.Security.Principal namespace contains classes that help make such role-based security determinations. These classes are highly extensible. They allow host code to provide its own user identity and role information, or they allow it to expose the user account and group information provided by Windows. For more complete details regarding how to extend the role-based security system, consult the .NET Framework SDK Developer's Guide. If you simply need to check the user's Windows user name and group memberships from a client application, here is how. The WindowsIdentity class represents an authenticated Windows user, and the WindowsPrincipal class that encapsulates the WindowsIdentity contains information about the user's role memberships. These objects representing the current user are accessible using either a static property of the Thread object or a static method of the WindowsIdentity object. We'll look at examples of both shortly. Accessing the current principal from the Thread object is the standard approach, and it works for all types of principal objects. But because this method returns an IPrincipal , it must be cast as a WindowsPrincipal before it can be used as one. Notice that before the current principal is accessed, a call to SetPrincipalPolicy is made. This is noteworthy because without this call the principal returned would be a GenericPrincipal containing no user information. Because the call to SetPrincipalPolicy requires the ControlPrincipal Security Permission (one not normally given out to less-than -fully-trusted code), this prevents semitrusted code (such as that running off the Internet) from gaining access to a user's account name. 'GettheCurrentUser'sSecurityPolicy AppDomain.CurrentDomain.SetPrincipalPolicy(_ PrincipalPolicy.WindowsPrincipal) DimuserAsWindowsPrincipal=_ CType(System.Threading.Thread.CurrentPrincipal,WindowsPrincipal) DimidentAsWindowsIdentity=user.Identity Checking for a Windows identity is a very common case, so this identity is easily accessible by using the static GetCurrent method of the WindowsIdentity class, as shown in the following example. Please note, however, that this method requires the same level of permission as the one above. DimidentAsWindowsIdentity=WindowsIdentity.GetCurrent() DimuserAsNewWindowsPrincipal(ident) Once a WindowsPrincipal object is retrieved, a user's group membership can be determined using the IsInRole method. If the goal of checking role group membership is to deny access to an application (as opposed to customizing the user experience), an even simpler approach is to use the Principal Permission to demand the required role. Scripting SecurityThe .NET Framework SDK ships with some tools you can use to script user, machine, and enterprise security policies. Scripting Security Policy ChangesThe CLR ships with an advanced security policy system that allows for three policy levels: the enterprise policy, the machine policy, and the user policy. Each policy level consists of a tree of code groups. Each code group consists of a membership condition (which might be based on URL of origin or publisher certificate, for example) and an associated permission set. Code is granted the permission set associated with a code group if it meets the respective membership condition. By changing code groups in the user, machine, or enterprise policy, administrators can determine what permissions are granted to assemblies. If you need to script policy changes, you can use the Code Access Security Policy ( Caspol .exe) command-line tool to create batch files containing policy change commands. Note For all standard administrative tasks , it is highly recommended that you use the Common Language Runtime Configuration (Mscorcfg.msc) tool. For more information on the policy system, see the security documentation in the Frameworks SDK.
Scripting Against Named Code GroupsThe CLR's default policy gives each code group a unique name. If code groups have not been deleted or renamed , you can uniquely script changes against these code groups. The most common code group names for scripting are listed in Table 9-3. Table 9-3. Common Code Group Names for Scripting
To see a complete list of code groups and their names in all policy levels, you can use the following caspol command: caspol-all-listgroups To change a code group's permission set, include a command of the following form in your batch script: caspolPolicyLevel-chggroupNameofcodegroupPermissionSetName To add a new code group, include a command of the following form in your batch script: caspolPolicyLevel-addgroup<NameofParentcodegroup> MembershipConditionPermissionSetNameCodeGroupFlags To reset policy to the default state at a policy level, include a command of the following form in your batch script: caspolPolicyLevel-reset The following caspol batch script resets policy to the default on all policy levels and grants full trust to intranet applications. Because the granted permissions to code are calculated as the intersection between policy levels and (in default policy) both enterprise and user policy levels are set to full trust, you need to change only the machine policy level in order to guarantee that intranet applications receive full trust. In our example, the following script guarantees that intranet applications will run with full trust (while code from other places of origin will run with the permissions given it by default policy): caspol-all-reset caspol-machine-chggroupLocalIntranet_ZoneFullTrust The first line in the following script of caspol commands shows how to set policy so code from the Internet will not receive any permissions from machine policy. The second command shows how to add a code group for granting full trust to code signed by the publisher that signed Myexe.exe. Note that the new code group is hung off the root of the machine policy. caspol-machine-chggroupInternet_ZoneNothing caspol-machine-addgroupAll_Code-pub-fileMyexe.exeFullTrust Authentication and AuthorizationNext, we'll look at .NET and operating system interaction in the areas of authentication and authorization. Windows Identity in Server ApplicationsWhen you use ASP.NET Windows authentication, ASP.NET attaches a WindowsPrincipal object to the current request. This object is used for URL authorization. The application can also use it programmatically to determine whether a requesting identity is in a given role. IfUser.IsInRole("Administrators")Then DisplayPrivilegedContent() EndIf The WindowsPrincipal class determines the roles of the user's NT group membership. ASP.NET applications that want to determine their own roles can do so by handling the WindowsAuthentication_OnAuthenticate event in their Global.asax file and attaching their own class that implements System.Security.Principal.IPrincipal to the request, as shown in the following example: 'CreateaclassthatimplementsIPrincipal PublicClassMyPrincipal:ImplementsIPrincipal 'Implementapplication-definedrolemappings EndClass 'InaGlobal.asaxfile PublicSubWindowsAuthentication_OnAuthenticate(SourceAsObject,_ eAsWindowsAuthenticationEventArgs) 'Attachanewapplication-definedclassthatimplementsIPrincipalto 'therequest. 'NotethatsinceIIShasalreadyperformedauthentication,theprovided 'identityisused. e.User=NewMyPrincipal(e.Identity) EndSub Forms-Based AuthenticationForms-based authentication is an ASP.NET authentication service that enables applications to provide their own logon user interface and do their own credential verification. ASP.NET authenticates users, redirecting unauthenticated users to the logon page and performing all the necessary cookie management. This sort of authentication is used by many Web sites. An application must be configured to use forms-based authentication; you set <authentication> to Forms and deny access to anonymous users. The following example shows how this can be done in the Web.config file for the desired application: <configuration> <system.web> <authenticationmode="Forms"/> <authorization> <denyusers="?" /> </authorization> </system.web> </configuration> Administrators use forms-based authentication to configure the name of the cookie to use, the protection type, the URL to use for the logon page, the length of time the cookie will be in effect, and the path to use for the issued cookie. Table 9-4 shows the valid attributes for the <Forms> element, which is a subelement of the <authentication> element shown in the following example: <authenticationmode="Forms"> <formsname=".ASPXCOOKIEDEMO" loginUrl="login.aspx" protection= "all" timeout="30" path="/"> <!--protection="[AllNoneEncryptionValidation]" --> </forms> </authentication> Table 9-4. Security-Related Forms Attributes
After the application has been configured, you need to provide a logon page. The following example shows a simple logon page. When the sample is run, it requests the Default.aspx page. Unauthenticated requests are redirected to the logon page (Login.aspx), which presents a simple form that prompts for an e-mail address and a password. (Use Username and Password as the credentials.) After validating the credentials, the application calls the following: FormsAuthentication.RedirectFromLoginPage(UserEmail.Value,_ PersistCookie.Checked) This redirects the user back to the originally requested URL. Applications that do not want to perform the redirection can call FormsAuthentication.GetAuthCookie to retrieve the cookie value or FormsAuthentication.SetAuthCookie to attach a properly encrypted cookie to the outgoing response. These techniques can be useful for applications that provide a logon user interface embedded in the containing page or that want to have more control over where users are redirected. Authentication cookies can be temporary or permanent (persistent). Temporary cookies last only for the duration of the current browser session. When the browser is closed, the cookie is lost. Permanent cookies are saved by the browser and are sent back across browser sessions unless explicitly deleted by the user. The authentication cookie used by forms authentication consists of one version of the System.Web.Security.FormsAuthenticationTicket class. The information includes the username (but not the password), the version of forms authentication used, the date the cookie was issued, and a field for optional application-specific data. Application code can revoke or remove authentication cookies using the FormsAuthentication.SignOut method, which removes the authentication cookie regardless of whether it is temporary or permanent. It's also possible to supply forms-based authentication services with a list of valid credentials using configuration, as shown in the following example: <authentication> <credentialspasswordFormat="SHA1" > <username="Mary" password="GASDFSA9823598ASDBAD"/> <username="John" password="ZASDFADSFASD23483142"/> </credentials> </authentication> The application can then call FormsAuthentication.Authenticate , supplying the username and password, and ASP.NET will verify the credentials. Credentials can be stored in cleartext or as SHA1 or MD5 hashes, according to the value of the passwordFormat attribute. The possible values are listed in Table 9-5. Table 9-5. Values of the passwordFormat Attribute
Authorizing Users and RolesASP.NET is used to control client access to URL resources. It is configurable for the HTTP method used to make the request ( GET or POST ) and can be configured to allow or deny access to groups of users or roles. To illustrate ASP.NET in action, consider an example in which you want to grant access to the user John and to the Admins role. All other users are denied access. The following example shows part of the XML code that needs to be included in Web.config: <authorization> <allowusers="john@microsoft.com" /> <allowroles="Admins" /> <denyusers="*" /> </authorization> Permissible elements for authorization directives are allow or deny . Each allow or deny element must contain a users or a roles attribute. You can specify multiple users or roles in a single element by providing a comma-separated list. <allowusers="John,Mary" /> You can indicate the HTTP method by using the Verb attribute: <allowVERB="POST" users="John,Mary" /> <denyVERB="POST" users="*" /> <allowVERB="GET" users="*" /> This example lets Mary and John POST to the protected resources, while allowing everyone else only to use GET . Two special (reserved) usernames are similar to the Everyone and Anonymous accounts in Windows: *:Allusers ?:Anonymous(unauthenticated)users These special usernames are commonly used by applications that use forms-based authentication to deny access to unauthenticated users, as shown in the following example: <authorization> <denyusers="?" /> </authorization> URL authorization is computed hierarchically, and the rules used to determine access are as follows :
This means applications that are not interested in inheriting their configuration should explicitly configure all of the possibilities relevant to them. The default top-level Web.config file for a given computer allows access to all users. Unless an application is configured to the contrary (and assuming that a user is authenticated and passes the file authorization ACL check), access is granted. When roles are checked, URL authorization effectively marches down the list of configured roles and does something that looks like the following pseudocode: IfUser.IsInRole("ConfiguredRole")Then ApplyRule() EndIf What this means for your application is that you use your own class that implements System.Security.Principal.IPrincipal to provide your own role-mapping semantics (as explained earlier). |
I l @ ve RuBoard |