Security is important. Most developers don't like security.
It requires a lot of thought. It requires study. Most developers would
rather just "write code", and leave security to "somebody
else". Unfortunately, if you are a developer and your job is to produce
an application, then guess who that "somebody else" usually
is? It's YOU. Even behemoth Microsoft got the message loud and clear.
They've made security the single most important thing, above everything
else. Wanna know why .NET Server, which was supposed to be released back
in March, is delayed until the third quarter? Security! They weren't satisfied,
and so they went back to the drawing board, so to speak. And its for real,
too. The Microsoft insiders with whom I am privileged to speak with from
time to time are totally focused on security.
I'm focused on security, only partly because I'm interested
in it, but mostly because I earn my living developing software for the
banking and financial industry, and often regulators such as FDIC
require that certain security measures be incorporated into applications.
Certainly clients such as banks and insurance companies do. And, you can't
just tell them "it's secure" -- they want proof.
There are a few fallacies about security that I think
are important for both developers and our managers to be aware of. One
is that usually because of lack of knowledge, projects often get
hit with what I refer to as "Security Overkill". The result
can often be a slower application that is also more expensive to install
and maintain. It is certainly possible to have "too much security",
especially when the payoff for it is not commensurate.
Another fallacy is that often a huge effort is put into
defending applications from attackers that aren't supposed to be able
to gain access, when in fact the statistics show that most successful
attacks are made by insiders such as disgruntled employees who already
had access!
As COM / Windows DNA developers, one of the features
we've learned to use is COM+ role-based security. The typical idea is
that you might have a database component that's called by users from an
ASP web application, and the database component runs under a different
ROLE than the code that calls it. Typically this is a Database User role.
In COM+, this is extremely easy to do. Just bring up the COM+ Explorer,
add a Role and user(s), and check "Enforce Access Checks", and
under "This application will run under the following account",
you enter the account. Presto! Instant role-based security. Er, sort -
of. . . You see, an experienced programmer (read that: "hacker")
can easily iterate the COM+ catalog programmatically and discover all
your settings!
In .NET, a whole new world of security is opened up for
us. But I want to make clear the distinctions that can be made here. Microsoft's
"marketing" dictates that COM+ is available in .NET through
the ServicedComponent class, and that COM+ is all "optimized"
(and other glowing descriptive terms) to work with .NET, etc. And that
you should use COM+ for all your role-based and Identity based security
needs. Well, let me share with you a quote from Tim McCarthy and Paul
Sherriff's recent article in MSDN about COM+(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/comservnet.asp):
"Tip If you do not need your .NET code to work
with COM+ services, that is, you are only going to be working in the .NET
Framework, do not use System.EnterpriseServices, because there is a
performance penalty. "
"If you are building a new application from scratch,
it is highly recommended that you build it entirely in the .NET Framework,
as it will be much more efficient."
The italics and bold type are of course,
mine. Note that they didn't just say there's a performance
penalty. They said apps built entirely in the .NET Framework
"will be much more efficient".
Hey, I was all ready to start running some tests with
serviced components, but after reading what the gurus
have to say, why should I bother? I always suspected it,
anyway. The fact is, almost everybody I've talked to (including
published authors whose books I've reviewed) say that
the only reason they were ever using COM+ was so they
wouldn't have to restart IIS when debugging COM components!
I don't know anyone who uses component - enlisted transaction
services because its almost always possible to have the
transactional business logic code 100 percent in SQL Server
stored procedures where it willlikely be more efficient
anyway.. Now please understand, I'm not "trashing"
COM+ and specifically not COM+ in the .NET platform -
they've done a great job of fully integrating it with
.NET and making it very easy to use. I'm merely suggesting
that my experience has shown that it may not always be
the best way to go, especially when your needs are as
simple as a role-based security context and there is no
need for transactions. For blazing speed, I'll show you
an alternative that's not too difficult to implement.
Regarding object pooling and Just-In-Time
Activation- well, the jury is out. Of course, we could
never do that with VB 6.0 anyway 'cause it's "STA
all the way". In .NET, all your objects are free-threaded
by default, so I wonder if it would really be worth the
trouble deriving from ServicedComponent in .NET? According
to Tim Ewald in his excellent MSDN article, "COM+
Integration: How .NET Enterprise Services Can Help You
Build Distributed Applications", using JIT
activation should be avoided in NET:
"At this point you may be wondering
about JIT activation and deactivating an object at the
end of every method call. Doesn't that resolve these resource
management issues? The general answer is no. Deactivating
an object at the end of every call forces an object to
release any resource it holds in data members, but it
also forces a client that wants to use the same object
more than once to reacquire an object for each method
call. You can achieve the benefit of this model without
the overhead simply by not storing any resources in data
members. If you do choose to use JIT activation and deactivate
your object at the end of every call, be aware that
a client's call to Dispose will actually force a reactivation
of your object simply to deactivate and finalize it again.
In general, the shift to the CLR doesn't change the rules
about using JIT activation. You should avoid it
unless it is required by some other runtime services
(such as the declarative transaction service) because
it is never more efficient, and is often less efficient
than simply managing precious resources carefully yourself."
The good news is that you can now employ declarative
and imperative role and / or identity - based security directly in
your .NET Code, and avoid the potentail performance drag of COM+ as
indicated by quotes from the authors above. The security model in .NET
is extensive, robust, and extensible. NET Security is a very big subject;
and it's too bad - because I haven't yet seen any good comprehensive books
out about it yet. Maybe the people at Wrox or aPress or O'Reilly will
do one? I figure its good for at least a 1,000 - pager - I'd sure buy
a copy!
Managed code can discover the identity or the role of
a principal through a Principal
object, which contains a reference to an Identity
object. .NET Framework identity objects represent users, while roles
represent memberships and security contexts. In the .NET Framework, the
principal object encapsulates both an identity object and a role. .NET
Framework applications grant rights to the principal based on its identity
or, more commonly, its role membership.
Identity Objects
The identity object encapsulates information about the
user or entity being validated. Identity objects contain a name and an
authentication type. The name can either be a user's name or the name
of a Windows account, while the authentication type can be either a supported
logon protocol, such as Kerberos V5, or a custom value. The .NET Framework
defines a GenericIdentity
object that can be used for most custom logon scenarios and a more
specialized WindowsIdentity
object that can be used when you want your application to rely on
Windows authentication. Additionally, you can define your own identity
class that encapsulates custom user information.
The IIdentity
interface defines properties for accessing a name and an authentication
type, such as Kerberos V5 or NTLM. All Identity classes implement
the IIdentity interface. If the Identity object is a WindowsIdentity
object, the identity is assumed to represent a Windows NT security token.
Principal Objects
The principal object represents the security context
under which code is running. Applications that implement role-based security
grant rights based on the role associated with a principal object. Similar
to identity objects, the .NET Framework provides a GenericPrincipal
object and a WindowsPrincipal
object. You can also define your own custom principal classes.
The IPrincipal
interface defines a property for accessing an associated Identity
object as well as a method for determining whether the user identified
by the Principal object is a member of a given role. All Principal
classes implement the IPrincipal interface as well as any additional
properties and methods that are necessary. For example, the common language
runtime provides the WindowsPrincipal class, which implements additional
functionality for mapping Windows NT or Windows 2000 group membership
to roles.
A Principal object is bound to a call context
(CallContext)
object within an application domain (AppDomain).
A default call context is always created with each new AppDomain,
so there is always a call context available to accept the Principal
object. When a new thread is created, a CallContext object is also
created for the thread. The Principal object reference is automatically
copied from the creating thread to the new thread's CallContext.
If the runtime cannot determine which Principal object belongs
to the creator of the thread, it follows the default policy for Principal
and Identity object creation.
Once you have defined identity and principal objects,
you can perform security checks against them in one of the following ways:
- Using imperative security checks.
- Using declarative security checks.
- Directly accessing the Principal object.
Managed code can use imperative or declarative
security checks to determine whether a particular principal object is
a member of a known role, has a known identity, or represents a known
identity acting in a role. Additionally, you can access the values of
the principal object directly and perform checks without a PrincipalPermission
object. In this case, you simply read the values of the current thread's
principal or use the IsInRole method perform authorization.
A link demand causes a security check during just-in-time
compilation and only checks the immediate caller of your code. Linking
occurs when your code is bound to a type reference, including function
pointer references and method calls. If the caller does not have sufficient
permission to link to your code, the link is not allowed and a runtime
exception is thrown when the code is loaded and run. Link demands can
be overridden in classes that inherit from your code.
Here are some code examples:
[C#]
[CustomPermissionAttribute(SecurityAction.LinkDemand)]
public static string ReadData()
{
//Access a custom resource.
}
[Visual Basic]
<CustomPermissionAttribute(SecurityAction.LinkDemand)> Public Shared
Function ReadData() As String
'Access a custom resource.
End Function
PrincipalPermissionAttribute:
[Visual Basic]
<PrincipalPermissionAttribute(SecurityAction.Demand, _
Name := "Bob", Role := "Supervisor")> Public Class
SampleClass
[C#]
[PrincipalPermissionAttribute(SecurityAction.Demand, Name="Bob",
Role="Supervisor")]
Public Shared Sub _
<PrincipalPermissionAttribute(SecurityAction.Demand, Name := "MyUser",
Role := "User")> _
PrivateInfo()
'Print secret data.
Console.WriteLine(ControlChars.CrLf + "You have access to the private
data!")
End Sub
When the security check is performed, both the specified
identity and role must match for the check to succeed. However, when you
create the PrincipalPermission object, you can pass a null identity string
to indicate that the identity of the principal can be anything. Similarly,
passing a null role string indicates that the principal can be a member
of any role (or no roles at all). For declarative security, the same result
can be achieved by omitting either property. For example, the following
code uses the PrincipalPermissionAttribute to declaratively indicate that
a principal can have any name, but must have the role of teller.
Enum: SecurityAction
The following table describes the time that each of
the security actions takes place and the targets that each supports.
Declaration of security action |
Time of action |
Targets supported |
LinkDemand |
Just-in-time compilation |
Class, Method |
InheritanceDemand |
Load time |
Class, Method |
Demand |
Run time |
Class, Method |
Assert |
Run time |
Class, Method |
Deny |
Run time |
Class, Method |
PermitOnly |
Run time |
Class, Method |
RequestMinimum |
Grant time |
Assembly |
RequestOptional |
Grant time |
Assembly |
RequestRefuse |
Grant time |
Assembly |
Members
Member name |
Description |
Assert |
The calling code can access the resource
identified by the current permission object, even if callers higher
in the stack have not been granted permission to access the resource
(see Assert). |
Demand |
All callers higher in the call stack
are required to have been granted the permission specified by the
current permission object (see Security
Demands). |
Deny |
The ability to access the resource specified
by the current permission object is denied to callers, even if they
have been granted permission to access it (see Deny). |
InheritanceDemand |
The derived class inheriting the class
or overriding a method is required to have been granted the specified
permission. |
LinkDemand |
The immediate caller is required to
have been granted the specified permission. |
PermitOnly |
Only the resources specified by this
permission object can be accessed, even if the code has been granted
permission to access other resources (see PermitOnly). |
RequestMinimum |
The request for the minimum permissions
required for code to run. This action can only be used within the
scope of the assembly. |
RequestOptional |
The request for additional permissions
that are optional (not required to run). This action can only be
used within the scope of the assembly. |
RequestRefuse |
The request that permissions that might
be misused will not be granted to the calling code. This action
can only be used within the scope of the assembly. |
Now that we've endured a highly abbreviated introduction
to the native .NET Security Principal and Identity objects, lets get down
to some code that puts them into practice. Remember above where I indicated
most "role - based" COM+ security simply involves running a
component under some separate privileged Windows Account in a specific
group (role)?
Let's use some code that does a P/Invoke call into the
Win32 API LogonUser to "switch" the identity of the currently
executing thread which will allow it to call into .NET managed code that
will use either declarative or imperative Identity role checking to prevent
unauthorized callers from executing privileged code. When we are done
with our "database" call under the new privileged user account,
we'll call RevertToSelf() to switch back to the regular identity under
which the code was originally running before the database call. Best of
all, this call runs in approximately 2 milliseconds on my PIII 667 at
home. I'd be willing to BET that's faster than role - based security under
COM+, and I doubt there is any question that deployment and maintenance
are much simpler! Not only will we be able to switch the identity and
prevent any user / role that's not authorized from running our "database"
code, we will also be able to check the identity that is asking
to perform the required impersonation - and refuse to do so if
the correct calling user does not match!
First, we need a handy Class Library to take care of
our call to the APIs:
Imports
System
Imports System.Runtime.InteropServices
Imports System.Security.Principal
Imports System.Security.Permissions
Imports PAB
Public Class Impersonation
<DllImport("C:\\WINNT\\System32\\advapi32.dll")> _
Public Shared Function LogonUser(ByVal lpszUsername As String, ByVal lpszDomain
As String, ByVal lpszPassword As String, _
ByVal dwLogonType As Integer, ByVal dwLogonProvider As Integer, ByRef
phToken As Integer) As Boolean
End Function
<DllImport("C:\\WINNT\\System32\\Kernel32.dll")> _
Public Shared Function GetLastError() As Integer
End Function
Public Function ImpersonateNewUser(ByVal strMachineName As String,
ByVal strCurrentUserName As String, ByVal strNewUserName As String, ByVal
strNewPassword As String) As Integer
'
if a DOMAIN account, strMachineName should be the DOMAIN
'The Windows NT user token.
Dim token1 As Integer
'Get the user token for the specified user, machine, and password using
the unmanaged LogonUser method.
'The parameters for LogonUser are the user name, computer name, password,
'Logon type (LOGON32_LOGON_NETWORK_CLEARTEXT), Logon provider (LOGON32_PROVIDER_DEFAULT),
'and user token.
Dim loggedOn As Boolean = LogonUser(strNewUserName, strMachineName, strNewPassword,
3, 0, token1)
Debug.WriteLine("LogonUser called")
Debug.WriteLine("LogonUser Success? " + loggedOn.ToString())
Debug.WriteLine("NT Token Value: " + token1.ToString())
'First lets get the credentials of the actual user that's already in context:
Debug.WriteLine("Before impersonation:")
Dim mWI1 As WindowsIdentity = WindowsIdentity.GetCurrent()
Debug.WriteLine(mWI1.Name)
Debug.WriteLine(mWI1.Token)
If mWI1.Name <> strCurrentUserName Then Return 1
' Now set our IntPtr (token) to the new user whose token we want to impersonate:
Dim token2 As IntPtr = New IntPtr(token1)
Debug.WriteLine("New identity created:")
' now instantiate the new WindowsIdentity object:
Dim mWI2 As WindowsIdentity = New WindowsIdentity(token2)
Debug.WriteLine(mWI2.Name)
Debug.WriteLine(mWI2.Token)
'Now go ahead and Impersonate the new user.
Dim mWIC As WindowsImpersonationContext = mWI2.Impersonate()
Dim sCurrentImp As String = mWI2.Token.ToString()
System.AppDomain.CurrentDomain.SetData(sCurrentImp, mWIC)
Debug.WriteLine("After impersonation:")
Return mWI2.Token.ToInt32()
End Function
Public Function Revert(ByVal sToken As String) As Integer
Dim mWI3 As WindowsImpersonationContext = System.AppDomain.CurrentDomain.GetData(sToken)
mWI3.Undo()
Debug.WriteLine("After impersonation is reverted:")
Dim mWIR As WindowsIdentity
mWIR = WindowsIdentity.GetCurrent()
Debug.WriteLine(mWIR.Name)
Debug.WriteLine(mWIR.Token)
Dim retval As Integer = mWIR.Token.ToInt32()
Return retval
mWIR = Nothing
End Function
End Class
This class has one major function:
to accept a machine name, an "authorized user" name to check
against, and a new username and password to impersonate assuming the passed-in
username is correct. In the Test Harness that accompanies the app, you
will set the authorized Machine\UserName or DOMAIN\UserName for it to
check against in the line, "If
mWI1.Name <> strCurrentUserName Then Return 1". Since
this function returns a Windows Identity token, we'll just send back a
"1" to show that the call failed on the authorized user check
so we can handle it in the calling code. Note also that for convenience,
I am storing the ImpersonationContext object in the AppDomain cache by
the token integer (converted to a string) so that we can gain access to
this at any time:
Dim
sCurrentImp As String = mWI2.Token.ToString()
System.AppDomain.CurrentDomain.SetData(sCurrentImp, mWIC)
IMPORTANT:
If the user account you want to impersonate is a DOMAIN account (e.g.,
not native to the local machine the code is running on) then you'll need
to pass in the DOMAIN name instead of the MACHINE name, and the Domain
account as "UserName" instead of "DOMAIN\UserName".
Now we need a class library that will act as a surrogate
or sample of our "Database Component". To keep things simple,
I've set one up with only one function, "hello", which if successfully
accessed, will just return the name of the Identity under which it was
called:
Imports
System.Security.Principal
Imports System.Security.Permissions
Imports System.Threading
Public Class Testme
<PrincipalPermissionAttribute(SecurityAction.Demand, Role:="PETER\DBUser")>
_
Public Function Hello(ByVal strText As String)
Dim mWI1 As WindowsIdentity = WindowsIdentity.GetCurrent()
Debug.Write(mWI1.Name)
Return strText + " was run under permitted user:" & mWI1.Name
& vbCrLf
End Function
Public Sub New()
'Get the current principal and put it into a principal object.
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal)
Dim MyPrincipal As WindowsPrincipal = CType(Thread.CurrentPrincipal, WindowsPrincipal)
' NOTE: uncomment following lines and Comment out the
' <PrincipalPermissionAttribute(SecurityAction.Demand, Name:="PETER\tester",
Role:="PETER\DBUser")> _
' before the "Hello" function to do checks imperatively
'If Not (MyPrincipal.IsInRole("PETER\DBUser")) Then
' Throw New System.Security.SecurityException("Unauthorized User")
'End If
'If Not (MyPrincipal.Identity.Name.Equals("PETER\tester")) Then
' Throw New System.Security.SecurityException("Unauthorized User")
'End If
End Sub
End Class
Notice that I've put in two ways to handle our testing:
1) Declarative, using the <PrincipalPermissionAttribute(SecurityAction.Demand,
Role:="PETER\DBUser")> attribute
to decorate the Hello method, and
2) In the constructor (which code is commented out) doing it imperatively
with code:
'If
Not (MyPrincipal.IsInRole("PETER\DBUser")) Then
' Throw New System.Security.SecurityException("Unauthorized User")
'End If
'If Not (MyPrincipal.Identity.Name.Equals("PETER\tester")) Then
' Throw New System.Security.SecurityException("Unauthorized User")
'End If
Note also that we are setting the Principal Policy of
the Current AppDomain to Windows Principal in the constructor.
Finally, we need a "Test Harness" to check
all this stuff out (I've left out the IDE -Generated form designer code
for brevity):
Imports
System.Security.Principal
Imports System.Security.Permissions
Imports System.Threading
Public Class Form1
Inherits System.Windows.Forms.Form
Dim retval2 As Integer
Dim retval As Integer
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)
Handles Button1.Click
Dim mWI2 As WindowsIdentity = WindowsIdentity.GetCurrent()
' show original identity:
TextBox1.Text += "Original Identity: " & mWI2.Name &
vbCrLf
' Impersonate the required user:
Dim ip As PAB.Impersonation = New PAB.Impersonation()
retval=ip.ImpersonateNewUser(System.Environment.MachineName, TextBox2.Text,
TextBox3.Text, TextBox4.Text)
If retval = 1 Then
MessageBox.Show("UH-OH, Wrong user running app!")
Exit Sub
End If
'Show stored impersonation Token
TextBox1.Text += "Impersonation Token: " & retval.ToString()
' Make the privileged call...
Dim tD As Testme = New Testme()
TextBox1.Text += vbCrLf & tD.Hello("Security")
tD = Nothing
' Revert to original user
Dim retval2 As Integer = ip.Revert(retval.ToString())
TextBox1.Text += vbCrLf & "Reverted: " & retval2.ToString()
& vbCrLf
' Show current identity
TextBox1.Text += "Final Identity: " & mWI2.Name
ip = Nothing
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)
Handles Button2.Click
TextBox1.Clear()
Dim mWI1 As WindowsIdentity = WindowsIdentity.GetCurrent()
TextBox1.Text += "Current Identity: " & mWI1.Name
Dim tD As Testme = New Testme()
TextBox1.Text += vbCrLf & tD.Hello("Security")
tD = Nothing
mWI1 = WindowsIdentity.GetCurrent()
TextBox1.Text += "Identity: " & mWI1.Name
End Sub
End Class
What we are doing here when you run the test harness
form is:
1) If you first click Button2 (No Security) the code
attempts to instantiate the TestMe class and call its Hello method. This
method call will fail, because the security checks will return a Security
Exception:
2) If you fill in an Authorized user, a DBUser and password,
and click Button1 ("Security") then the code calls the impersonation
API:
ip.ImpersonateNewUser(System.Environment.MachineName,
TextBox2.Text, TextBox3.Text, TextBox4.Text)
Since the call to the Hello Method
is now being run under the required user / role identity, the call will
succeed:
Of course, the user information you see above is native
to the machine I ran this code on; you'll need to revise the checks and
users to correspond to your specific machine. You can see above that the
Hello method was indeed run under the required "PETER\tester"
identity / role, and that the final Identity after calling the Revert
Method is the original user once again.
Role - based security allows developers to provide access
permissions to specified users / groups at a very fine-grained level.
This article only touches on one area; this is a big subject and a full
understanding of code access security in the .NET Framework requires significant
study. However the power and exceptional speed of being able to control
access to code through these various techniques is well worth the effort.
As has been seen, COM+ is superflous, overkill, and completely unnecessary
to perform these actions!
Download
the code that accompanies this article
| Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform. |
|