[ Team LiB ] |
Recipe 14.1 Controlling Access to Types in aLocal AssemblyProblemYou have an existing class that contains sensitive data and you do not want clients to have direct access to any objects of this class directly. Instead, you would rather have an intermediary object talk to the clients and allow access to sensitive data based on the client's credentials. What's more, you would also like to have specific queries and modifications to the sensitive data tracked, so that if an attacker manages to access the object, you will have a log of what the attacker was attempting to do. SolutionUse the proxy design pattern to allow clients to talk directly to a proxy object. This proxy object will act as gatekeeper to the class that contains the sensitive data. To keep malicious users from accessing the class itself, make it private, which will at least keep code without the ReflectionPermissionFlag.TypeInformation access (which is currently given only in fully trusted code scenarios like executing code interactively on a local machine ) from getting at it. The namespaces we will be using are: using System; using System.IO; using System.Security; using System.Security.Permissions; using System.Security.Principal; We start this design by creating an interface that will be common to both the proxy objects and the object that contains sensitive data: internal interface ICompanyData { string AdminUserName { get; set; } string AdminPwd { get; set; } string CEOPhoneNumExt { get; set; } void RefreshData( ); void SaveNewData( ); } The CompanyData class is the underlying object that is "expensive" to create: internal class CompanyData : ICompanyData { public CompanyData( ) { Console.WriteLine("[CONCRETE] CompanyData Created"); // Perform expensive initialization here } private string adminUserName = "admin"; private string adminPwd = "password"; private string ceoPhoneNumExt = "0000"; public string AdminUserName { get {return (adminUserName);} set {adminUserName = value;} } public string AdminPwd { get {return (adminPwd);} set {adminPwd = value;} } public string CEOPhoneNumExt { get {return (ceoPhoneNumExt);} set {ceoPhoneNumExt = value;} } public void RefreshData( ) { Console.WriteLine("[CONCRETE] Data Refreshed"); } public void SaveNewData( ) { Console.WriteLine("[CONCRETE] Data Saved"); } } The following is the code for the security proxy class, which checks the caller's permissions to determine whether the CompanyData object should be created and its methods or properties called: public class CompanyDataSecProxy : ICompanyData { public CompanyDataSecProxy( ) { Console.WriteLine("[SECPROXY] Created"); // Must set principal policy first AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy. WindowsPrincipal); } private ICompanyData coData = null; private PrincipalPermission admPerm = new PrincipalPermission(null, @"BUILTIN\Administrators", true); private PrincipalPermission guestPerm = new PrincipalPermission(null, @"BUILTIN\Guest", true); private PrincipalPermission powerPerm = new PrincipalPermission(null, @"BUILTIN\PowerUser", true); private PrincipalPermission userPerm = new PrincipalPermission(null, @"BUILTIN\User", true); public string AdminUserName { get { string userName = ""; try { admPerm.Demand( ); Startup( ); userName = coData.AdminUserName; } catch(SecurityException e) { Console.WriteLine("AdminUserName_get failed! {0}",e.ToString( )); } return (userName); } set { try { admPerm.Demand( ); Startup( ); coData.AdminUserName = value; } catch(SecurityException e) { Console.WriteLine("AdminUserName_set failed! {0}",e.ToString( )); } } } public string AdminPwd { get { string pwd = ""; try { admPerm.Demand( ); Startup( ); pwd = coData.AdminPwd; } catch(SecurityException e) { Console.WriteLine("AdminPwd_get Failed! {0}",e.ToString( )); } return (pwd); } set { try { admPerm.Demand( ); Startup( ); coData.AdminPwd = value; } catch(SecurityException e) { Console.WriteLine("AdminPwd_set Failed! {0}",e.ToString( )); } } } public string CEOPhoneNumExt { get { string ceoPhoneNum = ""; try { admPerm.Union(powerPerm).Demand( ); Startup( ); ceoPhoneNum = coData.CEOPhoneNumExt; } catch(SecurityException e) { Console.WriteLine("CEOPhoneNum_set Failed! {0}",e.ToString( )); } return (ceoPhoneNum); } set { try { admPerm.Demand( ); Startup( ); coData.CEOPhoneNumExt = value; } catch(SecurityException e) { Console.WriteLine("CEOPhoneNum_set Failed! {0}",e.ToString( )); } } } public void RefreshData( ) { try { admPerm.Union(powerPerm.Union(userPerm)).Demand( ); Startup( ); Console.WriteLine("[SECPROXY] Data Refreshed"); coData.RefreshData( ); } catch(SecurityException e) { Console.WriteLine("RefreshData Failed! {0}",e.ToString( )); } } public void SaveNewData( ) { try { admPerm.Union(powerPerm).Demand( ); Startup( ); Console.WriteLine("[SECPROXY] Data Saved"); coData.SaveNewData( ); } catch(SecurityException e) { Console.WriteLine("SaveNewData Failed! {0}",e.ToString( )); } } // DO NOT forget to use [#define DOTRACE] to control the tracing proxy private void Startup( ) { if (coData == null) { #if (DOTRACE) coData = new CompanyDataTraceProxy( ); #else coData = new CompanyData( ); #endif Console.WriteLine("[SECPROXY] Refresh Data"); coData.RefreshData( ); } } } When creating the PrincipalPermissions as part of the object construction, we are using string representations of the built in objects ("BUILTIN\Administrators") to set up the principal role. However, the names of these objects may be different depending on the locale the code runs under. It would be appropriate to use the WindowsAccountType.Administrator enumeration value to ease localization since this value is defined to represent the administrator role as well. We used text here to clarify what was being done and also to access the PowerUsers role, which is not available through the WindowsAccountType enumeration. If the call to the CompanyData object passes through the CompanyDataSecProxy, then the user has permissions to access the underlying data. Any access to this data may be logged to allow the administrator to check for any attempted hacking of the CompanyData object. The following code is the tracing proxy used to log access to the various method and property access points in the CompanyData object (note that the CompanyDataSecProxy contains the code to turn on or off this proxy object): public class CompanyDataTraceProxy : ICompanyData { public CompanyDataTraceProxy( ) { Console.WriteLine("[TRACEPROXY] Created"); string path = Path.GetTempPath( ) + @"\CompanyAccessTraceFile.txt"; fileStream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.None); traceWriter = new StreamWriter(fileStream); coData = new CompanyData( ); } private ICompanyData coData = null; private FileStream fileStream = null; private StreamWriter traceWriter = null; public string AdminPwd { get { traceWriter.WriteLine("AdminPwd read by user."); traceWriter.Flush( ); return (coData.AdminPwd); } set { traceWriter.WriteLine("AdminPwd written by user."); traceWriter.Flush( ); coData.AdminPwd = value; } } public string AdminUserName { get { traceWriter.WriteLine("AdminUserName read by user."); traceWriter.Flush( ); return (coData.AdminUserName); } set { traceWriter.WriteLine("AdminUserName written by user."); traceWriter.Flush( ); coData.AdminUserName = value; } } public string CEOPhoneNumExt { get { traceWriter.WriteLine("CEOPhoneNumExt read by user."); traceWriter.Flush( ); return (coData.CEOPhoneNumExt); } set { traceWriter.WriteLine("CEOPhoneNumExt written by user."); traceWriter.Flush( ); coData.CEOPhoneNumExt = value; } } public void RefreshData( ) { Console.WriteLine("[TRACEPROXY] Refresh Data"); coData.RefreshData( ); } public void SaveNewData( ) { Console.WriteLine("[TRACEPROXY] Save Data"); coData.SaveNewData( ); } } The proxy is used in the following manner: // Create the security proxy here CompanyDataSecProxy companyDataSecProxy = new CompanyDataSecProxy( ); // Read some data Console.WriteLine("CEOPhoneNumExt: " + companyDataSecProxy.CEOPhoneNumExt); // Write some data companyDataSecProxy.AdminPwd = "asdf"; companyDataSecProxy.AdminUserName = "asdf"; // Save and refresh this data companyDataSecProxy.SaveNewData( ); companyDataSecProxy.RefreshData( ); Note that as long as the CompanyData object were accessible, we could have also written this to access the object directly: // Instantiate the CompanyData object directly without a proxy CompanyData companyData = new CompanyData( ); // Read some data Console.WriteLine("CEOPhoneNumExt: " + companyData.CEOPhoneNumExt); // Write some data companyData.AdminPwd = "asdf"; companyData.AdminUserName = "asdf"; // Save and refresh this data companyData.SaveNewData( ); companyData.RefreshData( ); If these two blocks of code are run, the same fundamental actions occur: data is read, data is written, and data is updated/refreshed. This shows us that our proxy objects are set up correctly and function as they should. DiscussionThe proxy design pattern is useful for several tasks. The most notable, in COM and .NET Remoting, is for marshaling data across boundaries such as AppDomains or even across a network. To the client, a proxy looks and acts exactly the same as its underlying object; fundamentally, the proxy object is a wrapper around the underlying object. A proxy can test the security and/or identity permissions of the caller before the underlying object is created or accessed. Proxy objects can also be chained together to form several layers around an underlying object. Each proxy could be added or removed depending on the circumstances. For the proxy object to look and act the same as its underlying object, both should implement the same interface. The implementation in this recipe uses an ICompanyData interface on both the proxies (CompanyDataSecProxy and CompanyDataTraceProxy) and the underlying object (CompanyData). If more proxies are created, they too need to implement this interface. The CompanyData class represents an expensive object to create. In addition, this class contains a mixture of sensitive and nonsensitive data that require permission checks to be made before the data is accessed. For this recipe, the CompanyData class simply contains a group of properties to access company data and two methods for updating and refreshing this data. You can replace this class with one of your own and create a corresponding interface that both the class and its proxies implement. The CompanyDataSecProxy object is the object that a client must interact with. This object is responsible for determining whether the client has the correct privileges to access the method or property that it is calling. The get accessor of the AdminUserName property shows the structure of the code throughout most of this class: public string AdminUserName { get { string userName = ""; try { admPerm.Demand( ); Startup( ); userName = coData.AdminUserName; } catch(SecurityException e) { Console.WriteLine("AdminUserName_get Failed!: {0}",e.ToString( )); } return (userName); } set { try { admPerm.Demand( ); Startup( ); coData.AdminUserName = value; } catch(SecurityException e) { Console.WriteLine("AdminUserName_set Failed! {0}",e.ToString( )); } } } Initially, a single permission (AdmPerm) is demanded. If this demand fails, a SecurityException, which is handed by the catch clause, is thrown. (Other exceptions will be handed back to the caller.) If the Demand succeeds, the Startup method is called. It is in charge of instantiating either the next proxy object in the chain (CompanyDataTraceProxy) or the underlying CompanyData object. The choice depends on whether the DOTRACE preprocessor symbol has been defined. You may use a different technique, such as a registry key to turn tracing on or off, if you wish. Notice that if a security demand fails, the expensive object CompanyData is not created, saving our application time and resources. This proxy class uses the private field CoData to hold a reference to an ICompanyData type, which could either be a CompanyDataTraceProxy or the CompanyData object. This reference allows us to chain several proxies together. The CompanyDataTraceProxy simply logs any access to the CompanyData object's information to a text file. Since this proxy will not attempt to prevent a client from accessing the CompanyData object, the CompanyData object is created and explicitly called in each property and method of this object. See AlsoSee Design Patterns by Erich Gamma et al. (Addison Wesley). |
[ Team LiB ] |