Role Extender

Posted on February 3, 2012 By

Application security is one of those inevitable things that every application you develop must handle. Thankfully, in ASP.NET, Microsoft has made security a pretty easy thing to implement. Most of the applications that I have created use Role based security (Read more here: Managing Authorization Using Roles) which can be easily configured in the web.config to restrict pages to certain roles.

However, sometimes there are cases where you want to have specific controls on a certain page hidden unless you are a member of a certain role. For example, you may want to have a delete button only visible to an Administrator. The typical way of handling this is to do a User.IsInRole check somewhere in the code behind, typically Page_Load. Seeing this type code repeated in many pages across several project gave me the idea of creating a decorator control. This way I have a fully functional, reusable component that can be used on any ASP.NET project.

The decorator would have a property to identify which control I wanted to show/hide and a list of roles that could see the control.

So I started with this:

	public class RoleExtender: WebControl
	{
		private String myTargetControlID;
		public String TargetControlID 
		{
			get
			{
				return myTargetControlID;
			}
			set
			{
				myTargetControlID = value;
			}
		}

		private String myRoles;
		public String Roles 
		{
			get
			{
				return myRoles;
			}
			set
			{
				myRoles = value;
			}
		}
	}

Then I overloaded some events, namely Render and OnPreRender. Render so the control could be displayed during design time in the designer. OnPreRender would be where I find the control in the page heirarchy and show/hide the control based on the roles.

	public class RoleExtender: WebControl
	{
		private String myTargetControlID;
		public String TargetControlID 
		{
			get
			{
				return myTargetControlID;
			}
			set
			{
				myTargetControlID = value;
			}
		}

		private String myRoles;
		public String Roles 
		{
			get
			{
				return myRoles;
			}
			set
			{
				myRoles = value;
			}
		}

		protected override void Render(HtmlTextWriter writer)
		{
			if (DesignMode)
			{
				writer.Write("RoleExtender");
			}
			base.Render(writer);
		}

		protected override void OnPreRender(EventArgs e)
		{
			base.OnPreRender(e);

			WebControl c = FindControl(TargetControlID) as WebControl;
			if (c == null) 
				c = this.Parent.FindControl(TargetControlID) as WebControl;
			if (c != null)
			{
				c.Style.Remove("display");
				c.Style.Add("display", "none");
				foreach (String role in Roles.Split(','))
				{
					if (Page.User.IsInRole(role.Trim()))
					{
						c.Style.Remove("display");
						c.Style.Add("display", "inline");
					}
				}
				if (c.Style["display"] == "none")
				{
					foreach (BaseValidator bv in Page.Validators)
					{
						if (bv.ControlToValidate == TargetControlID)
						{
							bv.Enabled = false;
						}
					}
				}
			}
			else
				throw new Exception("Control: " + TargetControlID + " not found!");
		}
	}

The OnPreRender method first attempts to find the Target control in the line:

	WebControl c = FindControl(TargetControlID) as WebControl;

In practice, I found I had instances where it would not find the control, so if the control is not found, I search the parent in the next line:

	c = this.Parent.FindControl(TargetControlID) as WebControl;

Once I have a valid target control, I then start setting the style display properties. I initialize the target control to be hidden (display:none). Then I split the roles paramter from a CSV to an array of strings using the String.Split method. Then for each role specified, I see if the user is in that role. If so, I set the target controls to be visible (display:inline);

Finally, if the target control is hidden, I then disable any validators that may be setup on that control.

That worked, but I had some instances where I wanted to hide the control if the user was in a Role, otherwise I wanted it to be visible. So I added another property to the control:

	public Boolean myHideControl = true;

	public Boolean HideControl 
	{
		get
		{
			return myHideControl;
		}
		set
		{
			myHideControl = value;
		}
	}

Then I updated the OnPreRender method to the following:

	protected override void OnPreRender(EventArgs e)
	{
		base.OnPreRender(e);

		WebControl c = FindControl(TargetControlID) as WebControl;
		if (c == null) 
			c = this.Parent.FindControl(TargetControlID) as WebControl;
		if (c != null)
		{
			c.Style.Remove("display");
			c.Style.Add("display", (HideControl) ? "none" : "inline" );
			foreach (String role in Roles.Split(','))
			{
				if (Page.User.IsInRole(role.Trim()))
				{
					c.Style.Remove("display");
					c.Style.Add("display", (HideControl) ? "inline" : "none");
				}
			}
			if (c.Style["display"] == "none")
			{
				foreach (BaseValidator bv in Page.Validators)
				{
					if (bv.ControlToValidate == TargetControlID)
					{
						bv.Enabled = false;
					}
				}
			}
		}
		else
			throw new Exception("Control: " + TargetControlID + " not found!");
	}

The difference are the lines where I set the display style attribute to either none or inline based on the value of the HideControl boolean. When this is true, the decorator will hide the target control unless the user is in one of the specified roles. When it is false, the decorator will show the target control unless the user is in on of the specified roles.

Then I came across a case where I wanted to hide the target control if the user was a member of two groups rather than just a single group. So again, I added another parameter to the control to help handle this case:

	public Boolean myAnd = false;
	public Boolean And
	{
		get
		{
			return myAnd;
		}
		set
		{
			myAnd = value;
		}
	}

Then I once again updated the OnPreRender method to handle this case:

	protected override void OnPreRender(EventArgs e)
	{
		base.OnPreRender(e);

		WebControl c = FindControl(TargetControlID) as WebControl;
		if (c == null) 
			c = this.Parent.FindControl(TargetControlID) as WebControl;
		if (c != null)
		{
			c.Style.Remove("display");
			c.Style.Add("display", (HideControl) ? "none" : "inline" );
			if (!And)
			{
				foreach (String role in Roles.Split(','))
				{
					if (Page.User.IsInRole(role.Trim()))
					{
						c.Style.Remove("display");
						c.Style.Add("display", (HideControl) ? "inline" : "none");
					}
				}
			}
			else
			{
				Boolean inRole = true;
				foreach (String role in Roles.Split(','))
				{
					inRole &= Page.User.IsInRole(role.Trim());
				}
				if (inRole)
				{
					c.Style.Remove("display");
					c.Style.Add("display", (HideControl) ? "inline" : "none");
				}
			}
			if (c.Style["display"] == "none")
			{
				foreach (BaseValidator bv in Page.Validators)
				{
					if (bv.ControlToValidate == TargetControlID)
					{
						bv.Enabled = false;
					}
				}
			}
		}
		else
			throw new Exception("Control: " + TargetControlID + " not found!");
	}

Now if the And attribute is set to True, I check each role that was specified and ensure that the user is a member of each one. If the user is, I hide or show the control based on the HideControl attribute.

The full and final class is below:


	[DefaultProperty("TargetControlID")]
	public class RoleExtender: WebControl
	{
		private String myTargetControlID;

		[Bindable(true),
		Category("Behavior"),
		Localizable(true),
		DesignerSerializationVisibility(DesignerSerializationVisibility.Visible),
		PersistenceMode(PersistenceMode.Attribute)]
		public String TargetControlID 
		{
			get
			{
				return myTargetControlID;
			}
			set
			{
				myTargetControlID = value;
			}
		}

		private String myRoles;

		[Bindable(true)]
		[Category("Behavior")]
		[Localizable(true)]
		public String Roles 
		{
			get
			{
				return myRoles;
			}
			set
			{
				myRoles = value;
			}
		}

		public Boolean myHideControl = true;

		[Bindable(true)]
		[Category("Behavior")]
		[Localizable(true)]
		public Boolean HideControl 
		{
			get
			{
				return myHideControl;
			}
			set
			{
				myHideControl = value;
			}
		}

		public Boolean myAnd = false;
		[Bindable(true)]
		[Category("Behavior")]
		[Localizable(true)]
		public Boolean And
		{
			get
			{
				return myAnd;
			}
			set
			{
				myAnd = value;
			}
		}

		protected override void Render(HtmlTextWriter writer)
		{
			if (DesignMode)
			{
				writer.Write("RoleExtender");
			}
			base.Render(writer);
		}

		protected override void OnPreRender(EventArgs e)
		{
			base.OnPreRender(e);

			WebControl c = FindControl(TargetControlID) as WebControl;
			if (c == null) 
				c = this.Parent.FindControl(TargetControlID) as WebControl;
			if (c != null)
			{
				c.Style.Remove("display");
				c.Style.Add("display", (HideControl) ? "none" : "inline" );
				if (!And)
				{
					foreach (String role in Roles.Split(','))
					{
						if (Page.User.IsInRole(role.Trim()))
						{
							c.Style.Remove("display");
							c.Style.Add("display", (HideControl) ? "inline" : "none");
						}
					}
				}
				else
				{
					Boolean inRole = true;
					foreach (String role in Roles.Split(','))
					{
						inRole &= Page.User.IsInRole(role.Trim());
					}
					if (inRole)
					{
						c.Style.Remove("display");
						c.Style.Add("display", (HideControl) ? "inline" : "none");
					}
				}
				if (c.Style["display"] == "none")
				{
					foreach (BaseValidator bv in Page.Validators)
					{
						if (bv.ControlToValidate == TargetControlID)
						{
							bv.Enabled = false;
						}
					}
				}
			}
			else
				throw new Exception("Control: " + TargetControlID + " not found!");
		}

	}

Extenders     , , , , , , ,