Recipe 14.1 Controlling Access to Types in aLocal Assembly
Problem
You 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.
Solution
Use 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.
Discussion
The 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 Also
See
Design Patterns by Erich Gamma et al. (Addison
Wesley).
|