MarimerLLC / cslaforum

Discussion forum for CSLA .NET
https://cslanet.com
Other
31 stars 6 forks source link

Advice for users / roles / permissions #377

Open michaelcsikos opened 7 years ago

michaelcsikos commented 7 years ago

Hello Rocky, Jonny, et al

In a WinForms project, I've implemented a user login with the ApplicationContextManager, CslaIdentityBase<T> and CslaPrincipal. It's working well.

We want to add roles and permissions now, and I'd appreciate some advice before I code myself into a corner.

I have read the relevant section in book 2 of the Using CSLA 4 e-book series, and Rocky's Permission-based authorization vs role-based authorization blog.

The Roles will be fairly typical, but they will be defined at runtime, except for a hardcoded Administrator role. Each user will have a collection of roles.

Permissions will be defined per type, e.g. the Legal role may be allowed create, get, edit, and delete all projects, by default. In the static AddObjectAuthorizationRules() method, is there any problem with retrieving the string[] roles from another static command which loads them from the database?

We are hoping to be able to override (grant or deny) the per-type Permissions per instance. For example, Project 123 may grant roles as:

Legal

According to the per-type permissions, the Legal role is allowed to do anything, but for this project they have been denied permission to edit and delete.

Accounting

By default, the Accounting role may have no permissions granted for projects, but for this project they have get and edit permissions.

Is there any problem effectively overriding the default permissions granted per type?

If a delete is done with a factory method using DataPortal.Delete() is it only the static per-type authorization rules that are checked?

Thanks for your wisdom and this wonderful framework. —Michael

jonnybee commented 7 years ago

The roles:

are only checked on the static DataPortal.XYZ or the Save method by CSLA.

are executed on a per-instance AuthzRules (CanRead../CanWrite../CanExecute...) and by default the result is is cached.

Assuming that you transform the authorization config into "permissions" and load these into the PrincipalObject to be checked by IsInRole - then yes, you can get it to work.

When a Delete is done by DataPortal.Delete only the static DeleteObject rule is checked. When a Save is done, depending on the status of the object the CreateObject/EditObject or DeleteObject is checked.

You should not load roles dynamically in AddObjectAuthorizationRules(). The roles to check should be a constant in your code. You should however transform the permissions into roles in the user principal on login.

Remember: AddObjectAuthorizationRules is only called once-per-type in the application.

IE: If your rules are to be dynamic in that they must also check the database for another configuration on a "per data object instance" - then this must be done in the Execute method of the AuthorizationRule. And you must also remember that there can only be one-1 AuthorizationRule per Type/(instance)/Action.

michaelcsikos commented 7 years ago

Thanks for your detailed reply, Jonny.

What is the recommended way to apply GetObject authorisation for particular instances? e.g. Project 123 requires role Top-secret clearance, but regular projects don't.

jonnybee commented 7 years ago

Tricky business. Depending on your requirements - you could implement this in a GetObject rule where you Get the ID as parameter or you could implement this in your DataPortal_Fetch.

There could even be requirements for a search that the user should only see the records that he/she has permission to get and that should also be handled in the Fetch logic.

tfreitasleal commented 7 years ago

The search returns a list that should show only objects you are allowed to read. This a pretty common scenario and can be handled by the database query itself.

ghost commented 7 years ago

I'm going to piggyback off of this question here since I am trying to do something similar. I'm curious to know if there are any pitfalls in what I am doing and how I am doing it. This might also be helpful to @michaelcsikos if he has not come up with a solution yet.

Here's a couple things up front:

  1. Using custom identity/principal
  2. Transforming "permissions" aka organizations in the custom identity
  3. One or more organizations are assigned to a user
  4. An assigned organization can be either "Access Granted" or "Access Denied" based on the IsAccessGranted field in my user organization table.
  5. Roles are also used (Developer, Admin, etc).

I am currently writing a custom "IsAuthorized" rule which subclasses AuthorizationRule. This is added to my business object in the AddBusinessRules override as a per-method rule. This rule is also tightly coupled to my business object since it takes advantage of context.Target. The goal of authorization rule is as follows:

Below is a generic version of my code that works but might not be implemented correctly:

Business object

[Serializable()]
public class EditableObject : BusinessBase<EditableObject>
{
    //code omitted...

    protected override void AddBusinessRules()
    {
        base.AddBusinessRules();

        //AUTHORIZATION RULE ADDED HERE
        BusinessRules.AddRule(new Project.Library.Rules.Authorization.IsAuthorized(Csla.Rules.AuthorizationActions.ExecuteMethod, EditableMethod));
    }

    protected static void AddObjectAuthorizationRules()
    {
        //Per-Type authorization like CreateObject, EditObject, etc. set up here with roles.
    }

    public static readonly MethodInfo EditableMethod = RegisterMethod(typeof(EditableObject), "GetEditableObject");
    /// <summary>
    /// Retrieves a business object by its Id
    /// </summary>
    /// <param name="id">The business object's Id</param>
    /// <returns></returns>
    public static EditableObject GetEditableObject(int id)
    {
        return DataPortal.Fetch<EditableObject>(id);
    }

    private EditableObject() { /* require use of factory methods */ }

    private void DataPortal_Fetch(int id)
    {
        using (var ctx = ContextManager<ProjectDataContext>.GetManager(Database.Project))
        {
            var data = (from c in ctx.DataContext.Editable_SelectById(id)
                        select c).Single();

            using (BypassPropertyChecks)
            {
                //load properties
            }

            //AUTHORIZATION RULE CHECKED HERE AFTER PROPERTIES ARE LOADED 
            //SINCE WE NEED THE DATA FIRST
            CanExecuteMethod(EditableMethod);
        }
    }

    //code omitted...
}

Authorization Rule

public class IsAuthorized : AuthorizationRule
{
    public IsAuthorized(AuthorizationActions action, Csla.Core.IMemberInfo element) :
        base(action, element)
    {
        CacheResult = false;
    }

    protected override void Execute(AuthorizationContext context)
    {
        var target = context.Target as EditableObject;
        var EditableString = string.Empty;
        if (target != null)
        {
            EditableString = target.OrgString;
        }

        var _user = Csla.ApplicationContext.User.Identity as CustomIdentity;

        var _userOk = Csla.ApplicationContext.User.IsInRole("Developer");
        if (!_userOk)
        {
            //If user is Admin AND the EditableString does not exist in 
            //their DeniedOrgs list, then they're ok; otherwise block access
            if (Csla.ApplicationContext.User.IsInRole("Admin"))
            {
                _userOk = !_user.DeniedOrgs.Contains(EditableString) ? true : false;
            }
            else
            {
                //If the EditableObject OrgString is in the user's AccessGranted list, then 
                //they're ok; otherwise block access
                //This logic is for users that are not Developer or Admin
                _userOk = _user.Orgs.Contains(EditableString);
            }
        }

        //"At the very least, every authorization rule must set the HasPermission 
        //property of the AuthorizationContext parameter to the result of the rule."
        context.HasPermission = _userOk;
    }
}