MarimerLLC / csla

A home for your business logic in any .NET application.
https://cslanet.com
MIT License
1.26k stars 403 forks source link

Blazor example should use CSLA authz rules #1377

Closed rockfordlhotka closed 7 months ago

rockfordlhotka commented 5 years ago

@dazinator updated the BlazorExample sample to include authentication and some authorization.

I believe the authorization relies on Blazor UI support directly against the user's principal, and doesn't leverage authorization rules from the business layer.

Ideally the authorization would rely on per-type rules implemented in the PersonEdit class to determine whether the current user is authorized to view/edit a person.

dazinator commented 5 years ago

Just thinking this through and here is my take on this.

In the sample at the moment, the page (actually the client side route) is the thing that's authorised. Conceptually a blazor page may or may not contain any Csla objects. Or it could contain multiple (for example displaying different forms or lists on different tabs). Therefore I think the current page level authorisation check that's in place in the sample, by not being bound to any particular Csla root object is as it should be.

So then the question is, how do we render say an EditableRoot on a page, and have that view automatically check for CanEdit permissions etc and display display appropriate bot authorised message based on the EditableRoot authorisation rules.

Well.. I'm not sure of the best approach.. yet. But I'm thinking roughly:

  1. Extend the native authorisation system so that we can authorise against business objects (types, instances). We might be able to do this by registering a custom Csla authorisation policy / provider on startup.
  2. Provide a blazor / razor component that can added to a page, wraps a Csla view model, and uses the native authorization service to authenticate the user agains that Csla business object type / instance of said view model. This component would have parameters for content to display if the user is not authorised to View / Edit / Delete etc.

I see there is an EditForm already developed in the sample. If this was wrapped in such a component above then the whole form would not be displayed if the use didnt have edit permission. However what about field level authorisation- would the edit form need to be extended to do authorisation checks on each field? It would it be down to the developer to provided different display templates for fields that fail authorisation? I'm not clear on this more thoughts needed

rockfordlhotka commented 5 years ago

I understand what you are saying, but there's a philosophical core to CSLA, which is that all rules are centralized in the business layer.

I'm thinking forward to the ProjectTracker Blazor UI I'll build, and in ProjectTracker at no point are there rules outside the business layer. So, for example, whether a user is allowed to navigate to a form/page/screen that edits a domain object, it is a rule from that domain type that is used to tell the UI if the user has permission.

The idea is that when such a rule changes (and authz are rules), a dev shouldn't have to hunt through the business layer and the entire UI to see if a role/claim/permission was met. They go to the business layer, change it, and know that the change automatically impacts all types of app that use the business layer.

In other UI technologies I've always created some UI helpers - usually extending existing platform functionality. For example, in MVC there are authorization helpers that can be used in place of the standard ones - the difference being these helpers rely on the business layer for the rules.

It isn't enough to let the user navigate somewhere and tell them they can't see it - that's cruel! 😄 It is necessary to disable or hide the navigate element so they know they can't go there.

dazinator commented 5 years ago

I understand, but im not sure fully.. yet! 😁 perhaps allow me to divulge my thinking a bit more to see if we are actually agreeing!

Not all pages in my blazor SPA may be CSLA driven. For example if I want to add simple content page to my site - and authorise access to it based on the logged in users roles (e.g "members" can access support docs page) - I shouldn't be forced to create a CSLA business layer for that addition to my site. My philosophy then is that my site should work like a normal blazor website first - but then allow me to add Csla driven UI's (i.e bound to Csla objects) where I need to.

I believe the blazor "page" level authorisation is not something csla need concern itself with. It's the actual point where I want to include a Csla driven portion of UI on a page that I care about Csla authorisation checks happening (I.e at the blazor component level) and the ability to control what to render to a non authorised user. Like you say - it's cruel to allow users to navigate to a page and then not see anything. Applying an authorise attribute to an entire page for the blazor router to check on navigation does not help you with that problem of not rendering the link in the first place. Likewise if you have the thing to not render the link in the first place then you can also use that same thing to add on the target page to not display that pages actual content if the user isnt authorised- so again that takes the blazor router and page level authorise attribute out of the equation.

I create blazor pages to reflect the url navigation space I want for my site - not as a 1 to 1 mapping exercise for Csla use cases. To elaborate on this point a little - it's my view that I should be able to build the equivalent of the entire Csla project tracker application, in one single blazor page using blazor components to achieve that- if that's how I decide I want my sites url space. At the page level I'd not apply any authorisation. Or i'd split it into two pages, with the /login page not having any authorisation and the main page requiring authenticated user. The UI I render on that main page will want to take into account Csla authorisation rules so I cant see menu items for things I shouldn't have permission to access (probably a good first problem to solve) but when I do click on a menu item - it wouldn't navigate the user off to any other page it would stay on the same oage and display the relevent Csla form in a modal. Note the url has not changed, the page has not changed, the blazor router has not been involved, but still I need that Csla ui in the modal to now honour the Csla auth rules when l launch the modal). Page level authorisation is not useful here.

Saying all of that, as a thought experiment suppose you did have a page like /EditUser that you did want to authorise based on Csla auth rule. If you navigate to that page the blazor router will perform the check and display its NotAuthorised template content when auth failed. Anyone can copy and paste in the URL to visit that page. In terms of not displaying a link to the page in the first place, based on some Csla auth rule check, my preference is to get the in built in AuthorizeView component to work with Csla business object type auth rules. Now this works but suppose we now remove the page level authorize attribute - the blazor router now does not do any authorization check on navigating to that page (which is what I am suggesting) but once on that /EditUser page there is the AuthorizeView that conditionally displays the csla ui component based on the auth check. You end up with a similar effect but its now more flexible - because now other components / parts of that target page can still be rendered to the user if their auth requirements are met.

dazinator commented 5 years ago

Given the above if I was to pursue this line of thinking I'd do this:

  1. Have Csla register a custom authorization policy name provider with the authorisation system on startup. This would allow the native authorization system to tap into Csla business object auth rules at a type level (not instance level) using policy names. For example you could use the standard AuthoriseView with a policy name like this:
<AuthorizeView Policy="csla-user-edit">
    <p>You can only see this if you satisfy the policy.</p>
</AuthorizeView>

They policy name would use some form of convention - containing enough info for the service to resolve the Csla business object type, and rule that needs to be checked, and build the authorisation policy to be consumed. In the case above its going to build and return an Authorization policy that checks that the current principal can edit the User business object type.

I think that's it? This component is pretty flexible. With this one native AuthorizeView component, it can be used to conditionally display links and menu items based on Csla type auth rules. Likewise on pages and within modals (for example, around the EditForm in the current sample) it can display alternatice content if the user isnt authorised.

rockfordlhotka commented 5 years ago

I think we're on the same page - we've both been authorized 😄

I'm not trying to force the idea that every page uses CSLA authorization. But it needs to be an option, because if someone does build a business app - such as porting from WinForms to Blazor - it needs to be possible to leverage rules from the business layer for page, navigation, display/hiding controls, and enabling/disabling controls. Stuff like that.

I think creating a CSLA authorization policy (or policies?) that use per-type rules is absolutely the right step.

dazinator commented 1 year ago

@rockfordlhotka any progress on this one?

Just dumping some thoughts here on implementation:

See implementation thoughts I noticed in one of your posts code like this: ``` @if (vm.GetPropertyInfo(nameof(vm.Model.Name)).CanRead) { /// stuff here } ``` I guess one goal would be able to replace this with something like this? ``` /// stuff here ``` However the authorization system in .NET also offers another level of auth called "Resource-based" authorization. Resource based authorization is designed to perform authz checks for a particular resource -i.e an instance of some object. Resource based authz does not naturally support declarative style as shown above. Instead you need to use `imperative` style auth check which means using the IAuthoirzationService and asking it to authorize supplying a policy name AND the particular object instance which is the resource being authorized against. More here: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-6.0#use-imperative-authorization So there are two levels, and I think this may align with CSLA's concepts of type level auth rules, vs per instance level auth rules? It has been ages since I worked with CSLA so please correct me - can CSLA have instance level auth rules - i.e user can not edit Customer: X but can edit Customer Y? So back to the `declarative` style authorization and the `AuthorizeView` shown above - all that we have to make this work is the policy name. We can implement a custom `IAuthorizationPolicyProvider` that when given a policy name, parses information from the name and builds the required AuthorizationPolicy dynamically - this approach is mentioned in the docs here: So looking at this here: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-6.0#parameterized-authorize-attribute-example > Instead of registering all the different age-based policies that the application will need in AuthorizationOptions, you can generate the policies dynamically with a custom IAuthorizationPolicyProvider. To make using the policies easier, you can annotate actions with custom authorization attribute like [MinimumAgeAuthorize(20)]. I imagine then that the policy name would need to contain enough information to point to the csla business type, and also perhaps property, and also the "kind" of access check i.e "CanRead" vs "CanExecute" etc etc. With this in place you can then create custom `Authorize` attributes to make usage easier - i.e [CslaAuthorize(typeof(MyBusinessObject), any other params] rather than [Authorize("super-long-convention-based-policy-name-here"] The custom attribute just takes params and sets the policy name based on those so the user doesnt have to formulate it themselves. Here is their example of building a policy on the fly - where the actual minimum age the user has to be is passed in the policy name: ``` internal class MinimumAgePolicyProvider : IAuthorizationPolicyProvider { const string POLICY_PREFIX = "MinimumAge"; // Policies are looked up by string name, so expect 'parameters' (like age) // to be embedded in the policy names. This is abstracted away from developers // by the more strongly-typed attributes derived from AuthorizeAttribute // (like [MinimumAgeAuthorize()] in this sample) public Task GetPolicyAsync(string policyName) { if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase) && int.TryParse(policyName.Substring(POLICY_PREFIX.Length), out var age)) { var policy = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme); policy.AddRequirements(new MinimumAgeRequirement(age)); return Task.FromResult(policy.Build()); } return Task.FromResult(null); } } ``` The csla implementation of the above would have to parse the policy name and use that to include some custom `AuthorizationRequirements` into the policy. It is then possible to implment an `IAuthorizationHandler` which can intercept those rerquirements and actually do the checks. See: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-6.0#authorization-handlers However none of this so far would actually take an object instance to honour instance level auth rules assuming there is such a thing in csla.. So --> moving on to "Resource based" authz = i.e where we need to run auth checks for a particular csla business object `instance` not `type`. It looks to be the case we'd need to implement another `IAuthorizationHandler` that also takes the object instance: `https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-6.0#use-imperative-authorization` Here is there example:: ``` public class DocumentAuthorizationHandler : AuthorizationHandler { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SameAuthorRequirement requirement, Document resource) { if (context.User.Identity?.Name == resource.Author) { context.Succeed(requirement); } return Task.CompletedTask; } } public class SameAuthorRequirement : IAuthorizationRequirement { } ``` Conclusion: Looks like two levels of authz to deal with:- - for CSLA type level checks, should be able to fulfill this with Decarative (AuthorizeView) approach, and an IAuthorizationPolicyProvider that can build policies on the fly, parsing CSLA custom `IAuthorizationRequirement ` from the policy name string, and including them into the returned policy, and later an CSLA `IAuthorizationHandler` implementation that checks these requirements by performing the type auth checks for each requirement encountered. - for CSLA object instance level checks, should be able to fulfill this with a custom CSLA helper component that wraps IAuthorizationService, and behaves like `AuthorizeView` in that it renders different content based on the result of the auth check. It would take some parameters such as the business object instance to check, as well as information about the type of authz check to perform. It imperatively performs authz check by calling the `IAuthorizationService` with the correct policy name, which as per the declarative style mentioned above results in CSLA building and returning an auth policy with custom requirements - later another CSLA `IAuthorizationHandler ` implementation checks those same requirements but against the business object instance supplied this time. Back in the helper component if the auth check fails it can render the unauthorized content, otherwise render the authorized content - given this component would need to behave like `AuthorizeView` it may be possible to compose it or derive it from `AuthorizeView`.
rockfordlhotka commented 7 months ago

This will be demonstrated in ProjectTracker, and I don't plan to change the simpler example.

github-actions[bot] commented 1 month ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.