Jeff Garoutte

c# .net and anything else that happens across my desk

Recent posts

Tags

Categories

Navigation

Pages

    Archive

    Blogroll

    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

    Extending the ASP.Net Security model to use rights : Part Four - RightPermission

    The RightPermission works in the background.  Out of sight, out of mind and easily forgotten.  But it performs the "hard" work of securing the code.  The RightPermission clips in at just over 300 lines of code.  If you have not already read Part one - IPrincipal, Part two - IHttpModule and Part three - Attributes go back and take a look; We'll wait for you.

    First, I am going to give you all the code for the RightPermission and I will cover each method separately.

     

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Security;
    using System.Security.Principal;
    using System.Threading;
    using System.Security.Permissions;
    
    namespace ObjectHelpDesk.Security
    {
        public class RightPermission : IPermission, ICloneable, IEquatable<RightPermission>
        {
            private String _userName;
            private String _rightName;
            private Boolean _authenticated;
    
            public Boolean Authenticated
            {
                get { return _authenticated; }
                set { _authenticated = value; }
            }
    
            public String RightName
            {
                get { return _rightName; }
                set { _rightName = value; }
            }
    
            public String UserName
            {
                get { return _userName; }
                set { _userName = value; }
            }
    
            private RightPermission()
                : this(false, String.Empty, String.Empty)
            {
            }
    
            public RightPermission(Boolean authenticated, String userName, String rightName)
            {
                _authenticated = authenticated;
                _userName = userName;
                _rightName = rightName;
            }
    
            #region IEquatable<RightPermission> Members
    
            public bool Equals(RightPermission other)
            {
                if (this.Authenticated != other.Authenticated) return false;
                if (this.UserName != other.UserName) return false;
                if (this.RightName != other.RightName) return false;
                return true;
            }
    
            #endregion
    
            private Boolean IsUnrestircted()
            {
                if (_authenticated) return false;
                if (!String.IsNullOrEmpty(_userName)) return false;
                if (!String.IsNullOrEmpty(_rightName)) return false;
                return true;
            }
    
            private void DenyAccess()
            {
                DenyAccess("You lack the rights to do this");
            }
            private void DenyAccess(String message)
            {
                throw new SecurityException(message);
            }
            #region IPermission Members
    
            public IPermission Copy()
            {
                return this.Clone();
            }
    
            public void Demand()
            {
                if (IsUnrestircted()) return;
                IPrincipal user = Thread.CurrentPrincipal;
                if ((_authenticated) && (!user.Identity.IsAuthenticated)) DenyAccess("You must be logged in to do this.");
                if ((!String.IsNullOrEmpty(_userName)) && (_userName != user.Identity.Name)) DenyAccess("Your account lacks the rights to do this.");
                if (!String.IsNullOrEmpty(_rightName))
                {
                    if (!(user is RightPrincipal)) DenyAccess();
                    if (!((RightPrincipal)user).HasRight(_rightName)) DenyAccess();
                }
            }
    
            public IPermission Intersect(IPermission target)
            {
                RightPermission result = null;
                Boolean hasIntersection = true;
                if (target != null)
                {
                    if (!(target is RightPermission)) throw new ArgumentException("Target was not a RightPermission");
    
                    RightPermission p = (RightPermission)target;
    
                    if (this.Equals(p))
                        result = Clone();
                    else
                    {
                        result = new RightPermission();
    
                        result.Authenticated = (_authenticated || p.Authenticated);
                        if (_userName == p.UserName) result.UserName = _userName;
                        else if (String.IsNullOrEmpty(_userName)) result.UserName = p.UserName;
                        else if (String.IsNullOrEmpty(p.UserName)) result.UserName = _userName;
                        else hasIntersection = false;
    
    
                        if (_rightName == p.RightName) result.RightName = _rightName;
                        else if (String.IsNullOrEmpty(_rightName)) result.RightName = p.RightName;
                        else if (String.IsNullOrEmpty(p.RightName)) result.RightName = _rightName;
                        else hasIntersection = false;
                    }
                }
                if (!hasIntersection) result = null;
                return result;
    
            }
    
            private Boolean IsSubsetOf(RightPermission target)
            {
                Boolean result = false;
                if (Equals(target))
                    result = true;
                else if ((!_authenticated) && (target.Authenticated))
                    result = false;
                else if ((String.IsNullOrEmpty(target.UserName)) || (target.UserName == _userName))
                    result = ((String.IsNullOrEmpty(target.RightName)) || (target.RightName == _rightName));
                else
                    result = false;
                return result;
            }
    
            public Boolean IsSubsetOf(PrincipalPermission target)
            {
                Boolean result = false;
                
    
                if (target == null) return result;
                if (target.IsUnrestricted())
                {
                    result = true;
                }
                else
                {
    
                    SecurityElement element = target.ToXml();
                    Boolean authencation = CheckSubsetAuthencation(element);
                    Boolean userId = CheckSubsetUserId(element);
                    Boolean roleId = CheckSubsetRole(element);
                    
                    foreach (SecurityElement item in element.Children)
                    {
                        
                        if (authencation) authencation = CheckSubsetAuthencation(item);
                        if (userId) userId = CheckSubsetUserId(item);
                        if (roleId) roleId = CheckSubsetRole(item);
                    }
                    result = authencation && userId && roleId;
                
                }
    
                return result;
            }
    
            private bool CheckSubsetRole(SecurityElement item)
            {
                Boolean result = true;
                String value = String.Empty;
                value = item.Attribute("Role");
                if (!String.IsNullOrEmpty(value))
                {
                    result = RightManager.GetRightsByRoleName(value).Count(r => r.RightName == value) > 0;
                }
                else
                {
                    result = false;
                }
                return result;
            }
    
            private bool CheckSubsetUserId(SecurityElement item)
            {
                Boolean result = true;
                String value = String.Empty;
                value = item.Attribute("ID");
                if (value !=null)
                {
                    result = (String.IsNullOrEmpty(value) || value == _userName);
                }
                else
                {
                    result = false;
                }
                return result;
            }
    
            private bool CheckSubsetAuthencation(SecurityElement item)
            {
                Boolean result = true;
                String value = String.Empty;
                value = item.Attribute("Authenticated");
                if (!String.IsNullOrEmpty(value))
                {
                    result = String.Compare(value, "true", StringComparison.OrdinalIgnoreCase) == 0;
                }
                else
                {
                    result = false;
                }
                return result;
            }
    
            public bool IsSubsetOf(IPermission target)
            {
                Boolean result = false;
                if (target == null) return result;
    
                if (target is RightPermission)
                {
                    result = IsSubsetOf((RightPermission)target);
                }
                else if (target is PrincipalPermission)
                {
                    result = IsSubsetOf((PrincipalPermission)target);
                }
    
                return result;
            }
    
            public IPermission Union(IPermission target)
            {
                IPermission result = null;
                if ((target == null) || (target is RightPermission && Equals((RightPermission)target)))
                    result = Clone();
                else
                    throw new NotSupportedException();
    
                return result;
            }
    
            #endregion
    
            #region ISecurityEncodable Members
    
            public override string ToString()
            {
                return ToXml().ToString();
            }
            public void FromXml(SecurityElement e)
            {
                switch ((String)e.Attributes["Version"])
                {
                    case "1":
                    default:
                        this.RightName = (String)e.Attributes["Role"];
                        this.UserName = (String)e.Attributes["User"];
                        this.Authenticated = ("Y" == (String)e.Attributes["Authenticated"]);
                        break;
                }
            }
    
            public SecurityElement ToXml()
            {
                SecurityElement element = new SecurityElement("IPermission");
                String typename = "ObjectHelpDesk.Security.RightPermission";
                element.AddAttribute("class", typename + ", " + this.GetType().Module.Assembly.FullName.Replace('"', '\''));
                element.AddAttribute("Version", "1");
                element.AddAttribute("Role", _rightName);
                element.AddAttribute("User", _userName);
                element.AddAttribute("Authenticated", _authenticated ? "Y" : "N");
    
                return element;
            }
    
            #endregion
    
            #region ICloneable Members
    
            public RightPermission Clone()
            {
                return (RightPermission)((ICloneable)this).Clone();
            }
            object ICloneable.Clone()
            {
                return this.MemberwiseClone();
            }
    
            #endregion
        }
    }

     

    If you are are wondering about the class declaration public class RightPermission : IPermission, ICloneable, IEquatable<RightPermission>  and what IPermission, ICloneable, IEquatable<RightPermission> means a friend of mine has a nice post about Interfaces here.  In case you do not want to go read that right now; here is a quick blurb on Interfaces - IPermission, ICloneable, IEquatable are all Interfaces.  Normally Interfaces are prefix with a capital "i".  An Interface can define method and/or property signatures but not implementations.  Ok, that is a gross over simplification of an Interface but it will do for our purposes.

    I am working under the assumption that you understand generics (IEquatable<RightPermission> is a generic). If not, do not worry; You do not not really need to know about generics right now. 

    So lets start with the ICloneable code, if you've read Ira's article on ICloneable this may look familiar...

     
            #region ICloneable Members
    
            public RightPermission Clone()
            {
                return (RightPermission)((ICloneable)this).Clone();
            }
            object ICloneable.Clone()
            {
                return this.MemberwiseClone();
            }
    
            #endregion

     

    This code simply returns a new copy of the RightPermission. 

    If you looked at the code you may have noticed that ISecurtyEncodable snuck in someplace even though we did not include it in our class declaration.  Well we did, ISecurtyEncodable comes along with IPermission.  It includes to methods FromXml and ToXml.  I overrode the default ToString() method and placed it into the ISecurtyEncodable region.  Take note, if you change the namespace or class name you will want to edit the ToXml() method (psst, I know, yes.  But if it was just about the code I could have posted a zip file and moved on).

     
            #region ISecurityEncodable Members
    
            public override string ToString()
            {
                return ToXml().ToString();
            }
            public void FromXml(SecurityElement e)
            {
                switch ((String)e.Attributes["Version"])
                {
                    case "1":
                    default:
                        this.RightName = (String)e.Attributes["Role"];
                        this.UserName = (String)e.Attributes["User"];
                        this.Authenticated = ("Y" == (String)e.Attributes["Authenticated"]);
                        break;
                }
            }
    
            public SecurityElement ToXml()
            {
                SecurityElement element = new SecurityElement("IPermission");
                String typename = "ObjectHelpDesk.Security.RightPermission";
                element.AddAttribute("class", typename + ", " + this.GetType().Module.Assembly.FullName.Replace('"', '\''));
                element.AddAttribute("Version", "1");
                element.AddAttribute("Role", _rightName);
                element.AddAttribute("User", _userName);
                element.AddAttribute("Authenticated", _authenticated ? "Y" : "N");
    
                return element;
            }
    
            #endregion

     

    The next region I'm going to talk about is IEquatable<RightPermission>.  All this does is checks to see if the passed in RightPermission has the same values as the current instance.  We make use of this method in a few spots but again there is nothing fancy here outside of the IEquatable generic interface.

     
            #region IEquatable<RightPermission> Members
    
            public bool Equals(RightPermission other)
            {
                if (this.Authenticated != other.Authenticated) return false;
                if (this.UserName != other.UserName) return false;
                if (this.RightName != other.RightName) return false;
                return true;
            }
    
            #endregion

     

    There are some properties and constructors specific to the RightPermission.  I, despite the new public Property {get; set;} features of .net still use fields most of the time (Let's leave it at a bad day with INotifyPropertyChanged and move on).   First we have the three public properties, a private constructor and a public constructor.  There are two places where the constructors are used, the RightAttribute and within the RightPermission itself.  The method Unrestricted() checks to see if any of the properties are set that would cause a request to fail.  You can have a permission that does not restrict access.  Who knew?  Finally we have DenyAccess() and an overload that takes a string.  This method raises a security exception and stops the code execution.

     
            private String _userName;
            private String _rightName;
            private Boolean _authenticated;
    
            public Boolean Authenticated
            {
                get { return _authenticated; }
                set { _authenticated = value; }
            }
    
            public String RightName
            {
                get { return _rightName; }
                set { _rightName = value; }
            }
    
            public String UserName
            {
                get { return _userName; }
                set { _userName = value; }
            }
    
            private RightPermission()
                : this(false, String.Empty, String.Empty)
            {
            }
    
            public RightPermission(Boolean authenticated, String userName, String rightName)
            {
                _authenticated = authenticated;
                _userName = userName;
                _rightName = rightName;
            }
    
            private Boolean IsUnrestircted()
            {
                if (_authenticated) return false;
                if (!String.IsNullOrEmpty(_userName)) return false;
                if (!String.IsNullOrEmpty(_rightName)) return false;
                return true;
            }
    
            private void DenyAccess()
            {
                DenyAccess("You lack the rights to do this");
            }
            private void DenyAccess(String message)
            {
                throw new SecurityException(message);
            }

     

    The rest of the code is part of IPermission or an overload of an IPermission method.

    First is the Demand() method..

     
            public void Demand()
            {
                if (IsUnrestircted()) return;
                IPrincipal user = Thread.CurrentPrincipal;
                if ((_authenticated) && (!user.Identity.IsAuthenticated)) DenyAccess("You must be logged in to do this.");
                if ((!String.IsNullOrEmpty(_userName)) && (_userName != user.Identity.Name)) DenyAccess("Your account lacks the rights to do this.");
                if (!String.IsNullOrEmpty(_rightName))
                {
                    if (!(user is RightPrincipal)) DenyAccess();
                    if (!((RightPrincipal)user).HasRight(_rightName)) DenyAccess();
                }
            }

     

    Demand is what checks the current user for the needed right to access the code.  Right away we check the permission to see if it restricts access.  There is no point in checking it if every request would pass.  Next we get the current user from the Current thread.  If you recall in Part two - IHttpModule I said it was important to set the Principal of the thread and this is why.  If you are working on a large project and you dive into an assembly you may not have the HttpConext to pull the Principal from.  In addition, if you spawn a "worker" thread it inherits the parent threads principal.  That means the RightAttribute can be applied in all of the code, not just the web site code and it can be used in a win forms application.

    The Copy() method is easy enough, it simply calls clone.

     
            public IPermission Copy()
            {
                return this.Clone();
            }

     

    The Intersect method returns a new permission that is the "intersection" of this permission and the passed in permission.  By default if the passed in permission is not null and is not the same type of permission Intersect throws an ArgumentException.  If there is no intersection we return null.  What we do is build the new permission from the most restrictive values from the two permissions.

     
            public IPermission Intersect(IPermission target)
            {
                RightPermission result = null;
                Boolean hasIntersection = true;
                if (target != null)
                {
                    if (!(target is RightPermission)) throw new ArgumentException("Target was not a RightPermission");
    
                    RightPermission p = (RightPermission)target;
    
                    if (this.Equals(p))
                        result = Clone();
                    else
                    {
                        result = new RightPermission();
    
                        result.Authenticated = (_authenticated || p.Authenticated);
                        if (_userName == p.UserName) result.UserName = _userName;
                        else if (String.IsNullOrEmpty(_userName)) result.UserName = p.UserName;
                        else if (String.IsNullOrEmpty(p.UserName)) result.UserName = _userName;
                        else hasIntersection = false;
    
    
                        if (_rightName == p.RightName) result.RightName = _rightName;
                        else if (String.IsNullOrEmpty(_rightName)) result.RightName = p.RightName;
                        else if (String.IsNullOrEmpty(p.RightName)) result.RightName = _rightName;
                        else hasIntersection = false;
                    }
                }
                if (!hasIntersection) result = null;
                return result;
    
            }

     

    The Union(...) method also requires that the parameter target is the same type of permission as the current permission (so it has to be a RightPermission).  If the target is null or the same as the current permission we return a clone of the current permission.  Strictly speaking, if you have two permissions x and y calling x.Union(y) should return the same thing as y.Union(x).  I am throwing a NotSupportedException if it is not a simple clone.

     
            public IPermission Union(IPermission target)
            {
                IPermission result = null;
                if ((target == null) || (target is RightPermission && Equals((RightPermission)target)))
                    result = Clone();
                else
                    throw new NotSupportedException();
    
                return result;
            }

     

    Last but not least is the IsSubSetOf(...) method.  This looks to see if the permission is contained within the target permission.  I used a few overloads with this to keep each subset check small and self contained.  If the target is null there is no way the current permission is a subset so return false and bail.

     
            public bool IsSubsetOf(IPermission target)
            {
                Boolean result = false;
                if (target == null) return result;
    
                if (target is RightPermission)
                {
                    result = IsSubsetOf((RightPermission)target);
                }
                else if (target is PrincipalPermission)
                {
                    result = IsSubsetOf((PrincipalPermission)target);
                }
    
                return result;
            }

     

    Normally you can throw an ArgumentException if the target permission type is not the same type as the current permission; however, since a right can be assigned to a role it makes sense, to me, that a RightPermission could very well a subset of a default PrincipalPermission.  So the IsSubsetOf(RightPermission) compares the current permission against the target to see if it is a subset.

     
            private Boolean IsSubsetOf(RightPermission target)
            {
                Boolean result = false;
                if (Equals(target))
                    result = true;
                else if ((!_authenticated) && (target.Authenticated))
                    result = false;
                else if ((String.IsNullOrEmpty(target.UserName)) || (target.UserName == _userName))
                    result = ((String.IsNullOrEmpty(target.RightName)) || (target.RightName == _rightName));
                else
                    result = false;
                return result;
            }

     

    IsSubsetOf(PrincipalPermission)  does the same thing except it looks into the the RightPermission and checks if the Role has the right assigned to it.

     
            public Boolean IsSubsetOf(PrincipalPermission target)
            {
                Boolean result = false;
                
    
                if (target == null) return result;
                if (target.IsUnrestricted())
                {
                    result = true;
                }
                else
                {
    
                    SecurityElement element = target.ToXml();
                    Boolean authencation = CheckSubsetAuthencation(element);
                    Boolean userId = CheckSubsetUserId(element);
                    Boolean roleId = CheckSubsetRole(element);
                    
                    foreach (SecurityElement item in element.Children)
                    {
                        
                        if (authencation) authencation = CheckSubsetAuthencation(item);
                        if (userId) userId = CheckSubsetUserId(item);
                        if (roleId) roleId = CheckSubsetRole(item);
                    }
                    result = authencation && userId && roleId;
                
                }
    
                return result;
            }

     

    Check SubsetUserId and CheckSubsetAuthencation are fairly straight forward.  In Part three - Attributes we added another method to the RightManager.  That is used in the CheckSubsetRole(SecurityElement item). The important line of code is result = RightManager.GetRightsByRoleName(value).Count(r => r.RightName == value) > 0;

    RightManager.GetRightsByRoleName(value) returns back a list of rights.  We use a Linq count function to see how many rights in the list have the RightName we are looking for.  While unlikely, it is possible you might have 2 rights with the same name (hey, plan for the unexpected and since we did not build a full RightManager in this article it is possible)So we check to see if the count is greater than 0.

     

            private bool CheckSubsetRole(SecurityElement item)
            {
                Boolean result = true;
                String value = String.Empty;
                value = item.Attribute("Role");
                if (!String.IsNullOrEmpty(value))
                {
                    result = RightManager.GetRightsByRoleName(value).Count(r => r.RightName == value) > 0;
                }
                else
                {
                    result = false;
                }
                return result;
            }

     

    If you setup the page with the test buttons back in Part three - Attributes you can now go click those buttons.  Now you can assign rights to your class, properties and methods that will raise a SecurityException if the current user does not have the right.

    How flexible is this system?  We really once it is in place the only bits that need to be altered are that nagging bit of code in IHttpModule so that the user is always a RightPrincipal and the RightManager.

    Ok, so here's a minor rewrite of the context_PostAuthenticateRequest for the IHttpModule

     

            public void context_PostAuthenticateRequest(object sender, EventArgs e)
            {
                HttpApplication context = (HttpApplication)sender;
                if (context.User is RightPrincipal) return;
                List<Right> rights = null;
                if ((context.User != null) && (context.User.Identity.IsAuthenticated))
                    rights = RightManager.GetRightsByUserName(context.User.Identity.Name);
                else
                    rights = new List<Right>();
    
                RightPrincipal newPrincipal = new RightPrincipal(context.User, rights);
                HttpContext.Current.User = newPrincipal;
                System.Threading.Thread.CurrentPrincipal = newPrincipal;
            }

     

    The RightManager should be fully implemented; using a provider model to access the data store to update, create, delete rights as well as assigning them to roles.  In my full RightManager the GetRightsByUserName looks up all the roles assigned to the user and returns all the rights for those roles.  It is done in the data store to keep the workload off the web server and in the database. You could also assign rights directly to users if you wanted too.  The beauty of separating the system that manages rights from the system that secures code by rights is you can change how rights are stored, work or managed and the code security still functions. 

    If you want to learn more about IPermission check out the MSDN site

    kick it on DotNetKicks.com

    Posted: May 28 2008, 02:56 by jeff | Comments (1) RSS comment feed |
    • Currently 0/5 Stars.
    • 1
    • 2
    • 3
    • 4
    • 5
    Filed under: Security

    Related posts

    Comments

    zuppaman be said:

    zuppamanJust wanted to say thx for this great article.
    I was just what i needed.

    # June 27 2008, 09:19

    Add comment


    (Will show your Gravatar icon)  

      Country flag

    [b][/b] - [i][/i] - [u][/u]- [quote][/quote]