Why?
Recently, I started seeing numerous requests regarding creation of custom permissions that do not inherit from CodeAccessPermission and thus do not perform stackwalk. There is nothing special about implementing such classes. In fact, it is easier then with CodeAccessPermission as a base. However, having a sample handy, I just decided to share it here along with my comments. So welcome
WorkingTimePermission
Trying to be at least somewhat close to real life, I implemented the permission object with the following Demand semantics:
If Demand is performed during business hours, it passes.
If it is done in other time, it throws the SecurityException.
As you see, it’s straightforward. However, it might save you a good chunk of time when you use it declaratively, e.g.:
[method:WorkingTimePermissionAttribute(SecurityAction.Demand)]
private static void AccessibleDuringBusinessHoursOnly()
{
//…
}
…instead of performing time checks explicitly on each protected function entrance.
The sample consists of 4 files: Permission DLL, Permission Attribute DLL, Client file that uses it and script that builds all together. My comments are in gray. Catches or important places are in dark red. Enjoy!
1. WorkingTimePermission.cs:
using System;
using System.Security;
using System.Security.Permissions;
[assembly:System.Reflection.AssemblyKeyFile("SomeKey.snk")]
[assembly:System.Security.AllowPartiallyTrustedCallersAttribute()]
namespace CustomPermissions
{
[Flags,Serializable]
public enum AccessType
{
Common = 0x00,
VIP = 0x01
}
public sealed class WorkingTimePermission : IPermission
{
private AccessType m_Access;
public WorkingTimePermission()
{
m_Access = AccessType.Common;
}
public WorkingTimePermission(AccessType access)
{
VerifyAccess(access);
m_Access = access;
}
/* Catch #1: although IPermission does not obligate you
to implement this kind of constructor, it must be specified
if you wish to have the declarative form of the permission,
because Security system will silently try to use it when decoding
the attribute, and fail if it's not found. */
public WorkingTimePermission(PermissionState State)
{
m_Access = AccessType.Common;
}
private void VerifyAccess(AccessType access)
{
if (0 != (((int) access) & ~1))
throw new ArgumentException("Wrong access type value: " + (int) access);
}
public IPermission Copy()
{
return new WorkingTimePermission(this.m_Access);
}
private void VerifyType(IPermission Target, bool IsNullOK)
{
if (null == Target)
{
if (true == IsNullOK) return;
else throw new ArgumentException("Target is not WorkingTimePermission [it is null]");
}
if (this.GetType() != Target.GetType())
throw new ArgumentException("Target is not WorkingTimePermission");
}
public AccessType GetAccess()
{
return m_Access;
}
public IPermission Intersect(IPermission Target)
{
if (null == Target) return null;
VerifyType(Target, false);
WorkingTimePermission P = (WorkingTimePermission) Target;
return new WorkingTimePermission(this.GetAccess() & P.GetAccess());
}
public IPermission Union(IPermission Target)
{
VerifyType(Target, true);
WorkingTimePermission P = (WorkingTimePermission) Target;
return new WorkingTimePermission(this.GetAccess() | P.GetAccess());
}
/* Catch #2: if you work with V1.0 or V1.1, this method will be silently
called by the compiler during build time if permission is used
declaratively. If any error occurs inside the body of this method, it is not
propagated to the above, issuing instead quite a bogus message like
“failed to create the permission for this attribute”. If it sounds familiar,
double your attention to this place: inject more debugging output, etc.*/
public bool IsSubsetOf(IPermission Target)
{
VerifyType(Target, true);
if (null == Target) return false;
WorkingTimePermission P = (WorkingTimePermission) Target;
return (this.GetAccess() <= P.GetAccess());
}
/* This is the core logics part of the permission. I’ve implemented it this way:
For VIP access, Demand always passes.
For normal access, it passes during “usual” business hours only.
Of course, you are more then welcome to implement whatever logics you see suitable.
Note please that all exception messages are hardcoded in English here.
This is not a good style in general, and done only for the sake of simplicity. */
public void Demand()
{
Console.WriteLine("WorkingTimePermission.Demand() called!");
if (AccessType.VIP == this.GetAccess()) return;
DateTime Curr = DateTime.Now;
DayOfWeek CurrDay = Curr.DayOfWeek;
if ((CurrDay == DayOfWeek.Saturday)||(CurrDay == DayOfWeek.Sunday))
throw new SecurityException("Request for WorkingTimePermission failed because current day of week is " + CurrDay + ".");
int Hour = Curr.Hour;
if ((Hour < 8)||(Hour > 17))
throw new SecurityException("Request for WorkingTimePermission failed because current time is " + Hour + " hours.");
}
.
public SecurityElement ToXml()
{
SecurityElement Ret = new SecurityElement("IPermission");
String name = typeof(WorkingTimePermission).AssemblyQualifiedName;
Ret.AddAttribute("class", name);
Ret.AddAttribute("version", "1.0");
Ret.AddAttribute("AccessType", this.GetAccess().ToString());
return Ret;
}
.
public void FromXml(SecurityElement e)
{
String name = e.Attribute("class");
if (name != typeof(WorkingTimePermission).AssemblyQualifiedName) throw new ArgumentException("Wrong SecurityElement");
String version = e.Attribute("version");
if (version != "1.0") throw new ArgumentException("Version " + version + " does not match current version of the permission");
String access = e.Attribute("AccessType");
if (null != access) m_Access = (AccessType) Enum.Parse(typeof(AccessType), access);
else m_Access = AccessType.Common;
}
}
}
2. WorkingTimePermissionAttribute.cs:
using System;
using System.Security;
using System.Security.Permissions;
[assembly:System.Reflection.AssemblyKeyFile("SomeKey.snk")]
[assembly:System.Security.AllowPartiallyTrustedCallersAttribute()]
namespace CustomPermissions
{
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false )]
/*Catch #3: use CodeAccessSecurityAttribute as a base class, don’t be tempted by SecurityAttribute. */
public sealed class WorkingTimePermissionAttribute : CodeAccessSecurityAttribute
{
private AccessType m_Access;
public WorkingTimePermissionAttribute(SecurityAction action): base(action)
{
m_Access = AccessType.Common;
}
public override IPermission CreatePermission()
{
return new WorkingTimePermission(m_Access);
}
/* Catch #4: Even if permission has some internal parameter named Data,
we don't need to implement constructor(DataType Data). Attributes think that
Data is a property of the permission, so let’s implement it as a property: */
public AccessType Access
{
get
{
return m_Access;
}
set
{
m_Access = value;
}
}
}
}
3. Client.cs
This is the sample of the code that uses WorkingTimePermission:
using System;
using System.Security;
using System.Security.Permissions;
using CustomPermissions;
public class BBB : AAA
{
public override void Ozz()
{
Console.WriteLine("BBB.Ozz() being called");
}
}
public class AAA
{
private static void Foo()
{
WorkingTimePermission P = new WorkingTimePermission();
P.Demand();
Console.WriteLine("AAA.Foo() called!");
}
[method:WorkingTimePermissionAttribute(SecurityAction.Demand)]
private static void Bar()
{
Console.WriteLine("AAA.Bar() called!");
}
[method:WorkingTimePermissionAttribute(SecurityAction.Demand, Access = AccessType.VIP)]
private static void BarVIP()
{
Console.WriteLine("AAA.BarVIP() called!");
}
[method:WorkingTimePermissionAttribute(SecurityAction.LinkDemand)]
private static void Rug()
{
Console.WriteLine("AAA.Rug() called!");
}
/* Catch #5: if you want to be able to see the actual SecurityException from the method
protected by JIT-time action like LinkDemand, wrap a target method into another one
and call the wrapper from inside a try...catch block. Without a wrapper, SecurityException
will be thrown during JIT-time, when try...catch does not exist yet, thus going uncaught.
So the sample below calls RugWrapper() instead of just Rug().*/
private static void RugWrapper()
{
Rug();
}
[method:WorkingTimePermissionAttribute(SecurityAction.InheritanceDemand)]
public virtual void Ozz()
{
Console.WriteLine("AAA.Ozz() being called");
}
private static void BBBOzzWrapper()
{
(new BBB()).Ozz();
}
public static void Main()
{
try
{
Foo();
}
catch (SecurityException e)
{
Console.WriteLine(e);
}
try
{
Bar();
}
catch (SecurityException e)
{
Console.WriteLine(e);
}
try
{
BarVIP();
}
catch (SecurityException e)
{
Console.WriteLine(e);
}
try
{
RugWrapper();
}
catch (SecurityException e)
{
Console.WriteLine(e);
}
try
{
BBBOzzWrapper();
}
catch (SecurityException e)
{
Console.WriteLine(e);
}
}
}
4. Build script. I assumed that all the tools like caspol.exe are on the path:
caspol -pp off -all –reset
del *.dll
del *.exe
gacutil /u WorkingTimePermission
gacutil /u WorkingTimePermissionAttribute
sn -k SomeKey.snk
csc /debug+ /t:library WorkingTimePermission.cs
REM: Now is the important detail: in order to work properly, every DLL that implements classes used by Security must be in the GAC and must be in the list of fully trusted assemblies, so let’s do it:
gacutil /i WorkingTimePermission.dll
caspol -af WorkingTimePermission.dll
csc /debug+ /t:library /r:WorkingTimePermission.dll WorkingTimePermissionAttribute.cs
gacutil /i WorkingTimePermissionAttribute.dll
caspol -af WorkingTimePermissionAttribute.dll
csc /debug+ /r:WorkingTimePermission.dll /r:WorkingTimePermissionAttribute.dll Client.cs
That’s it!
Other uses of permission classes?
Just as a quick side note… If you try to think of permission as of “deferred condition” object, you might find a number of interesting uses for custom classes like the above. Examples include:
CallDepthPermission: demand passes if the length of the callstack is less then some particular number [say, passed as a parameter].
OSLanguagePermission: you want your code to be able to run on a limited set of OS languages only? It’s very easy to implement!
NetworkStatusPermission: how about an attribute that allows function to be called only when the machine is off the network?
Have fun!
Important update for users of .NET Framework 2.0 ["Whidbey"]
Due to the number of Permission semantics changes, the above sample will not work as described in 2.0 if run in Full Trust environment. Partial trust behavior should remain unchanged though. More details are available at http://blogs.msdn.com/eugene_bobukh/archive/2005/05/06/415217.aspx .