Skip to content
This repository has been archived by the owner on Apr 18, 2020. It is now read-only.
/ OrgAuth Public archive

Organization Role-Based Hybrid Authorization research using ASP.NET Core and Angular

Notifications You must be signed in to change notification settings

JaimeStill/OrgAuth

Repository files navigation

Organization-based Role Authorization

Build and Run

Back to Top

Clone this repository:

git clone https://github.com/JaimeStill/OrgAuth

From the root OrgAuth folder, build the application:

dotnet build

Change directory into the dbseeder directory, and seed the database:

dotnet run -- -c "Server=(localdb)\ProjectsV13;Database=OrgAuth-dev;Trusted_Connection=True;"

Change directory into the Authorization.Web directory, and run the application:

dotnet run

The application should now be hosted at http://localhost:5000.

org-selector

Overview

Back to Top

The intent of this repository is to demonstrate how to perform the following:

  • Present an application from the viewpoint of an organization in an environment where many organizations may operate within the same application instance
  • Provide role-based authorization on a per-organization basis
  • Provide a means for data access to be layered based on the access a user is given within the organization

For API controllers that deal with data specific to an organization, the intent was to be able to route the org at the controller level so that access to those resources can be restricted based on permissions within that org.

In the built-in Role-Based Authorization strategy for .NET Core, attributes are read before Model Binding occurs. This means that the authorization strategy will be executed before any routed variables can be extracted from the HTTP request.

The following sections describe the infrastructure and practices established to not just enable this sort of authorization strategy, but to make the same infrastructure compatible with Angular Route authorization.

Config

Back to Top

This app is intended to allow you to see how a theoretical app would be affected by different authorization conditions. Because of this, the authorization configuration is isolated from the rest of the application so that if you restrict access to the current user, you are not locked out of the authorization configuration.

The app launches at the default /config/* route, and has the following sub-routes:

  • /users - Manage app User accounts. Users are based on an Active Directory account, or in the case of development, the mock user provider defined in Authorization.Identity.Mock.
  • /user-roles - Manage user role assignments
  • /orgs - Manage Org entities
  • org-users - Manage User assignments to an Org
  • org-user-roles - Manage Role assignments for a User in an Org

config

Authorization Infrastructure

Back to Top

This section outlines all of the infrastructure that has been written to facilitate Org-based Role Authorization. Each subsequent section deals with a particular facet of the Authorization, with each section that follows after relying on the previous infrastructure.

Entity Framework

Back to Top

The entities that enable Org-Based Role Authorization are illustrated in the following diagram:

org-auth-entities

Collection navigation properties that are not relevant to this diagram have been removed for brevity

These entities are defined in Authorization.Data/Entities, and they are mapped to TypeScript interfaces in the Angular models/api folder.

A User can be associated with many instances of Org and Role, via OrgUser and UserRole respectively.

However, simply having a Role and belonging to an Org is not enough.

An assigned UserRole must then be associated with an OrgUser assignment, specifying the User has a designated Role within an Org.

Auth Library

Back to Top

The following infrastructure is defined in the Authorization.Auth library

AuthContext.cs

Because authorization is org-based, the current authorization context needs to be tracked to determine the current Org scope, the current User, and any Role association the User has via OrgUserRole associations.

AuthExtensions.cs

When using Attribute-based authorization in ASP.NET Core, the policy is checked before Model Binding is able to occur, so the Org cannot be dynamically set on a per-request basis. Because of this, action methods that should be restricted based on organization should support an Authorize method that wraps the method to execute if authorization completes successfully. The following are the capabilites that the AuthExtensions.cs class provides:

  • Retrieve the AuthContext given the current User and a specified Org
  • Retrieve the AuthContext given the current User and the Org specified by User.DefaultOrg
  • Validate, given the current User, whether User.IsAdmin is true
  • Validate, given the current User and an Org, whether any OrgUserRole is assigned
  • Validate, given the current User and an Org, whether any OrgUserRole is assigned based on any of the specified Role.Name values
  • Provide Authorize methods that support any of the different validation contexts

Usage of the Authorize methods is demonstrated in the API Authorization section below

In addition to the infrastructure in this library, AuthContext is mapped to an AuthContext interface in the Angular models folder.

Auth Controller

Back to Top

AuthController.cs

In order to retrieve a given AuthContext and to provide client-side route authorization, AuthController.cs is created to expose all but the Authorize methods from AuthExtensions.cs above.

The methods in this controller are exposed to Angular via a global AuthContextService service in the Angular services folder

Checking GET Endpoints

Back to Top

After seeding the database and running the app, modify the authorization context (roles, org assignments, and roles within those org assignments). When this is done, you can check the API endpoints in the browser:

SignalR

Back to Top

When any of the authorization values associated with the current user are modified, it's imperative that the AuthContext is updated to reflect those changes. For instance, if an administrator removes a User from an Org, any associated OrgUserRole entries are removed, and they no longer have access to those Org resources.

If the user affected by this change is in an active session, those changes should immediately take place transparently. Because of this, SignalR has been used to provide real-time, multicast communication between user sessions where Authorization permissions are concerned. This section will highlight not only the SignalR infrastructure, but the points where it is used to trigger these updates.

signalr

SocketHub.cs

Defines a triggerAuth method that signals a refreshAuth function to a user with userSocketName if they have an open session to the application.

Startup.cs

services.AddSignalR is registered in Startup.ConfigureServices and in Startup.Configure, the SocketHub is registered to the socket route /core-socket.

socket.service.ts

Manages the lifecycle of the SocketHub connection to /core-socket, provides a trigger for when refreshAuth is executed, and exposes the triggerAuth function defined in SocketHub.

layout.view.ts

In ngOnInit, the refreshAuth$ stream from SocketService is subscribed to. When it resolves with a true value, the AuthContext is refreshed based on the updated values. The current orgs that the user belongs to is also updated.

org.service.ts

When the http.post call in the saveOrgUserRoles function returns successfully, SocketService.triggerAuth is executed, indicating that authorizations for a user have been updated.

role.service.ts

When the http.post call in the saveUserRoles function returns successfully, SocketService.triggerAuth is executed, indicating that authorizations for a user have been updated.

user.service.ts

When the http.post call in the toggleUserAdmin function returns successfully, SocketService.triggerAuth is executed, indicating that authorizations for a user have been updated.

Caveat Framework

Back to Top

Before showing how Authorization works, it's important to understand the kind of resources that would be protected at the Org level. The following diagram shows how the Caveat framework is structured:

caveat-framework

Collection navigation properties that are not relevant to this diagram have been removed for brevity

An Org can have many Items associated with it.

A User can be associated with a Brief via the UserBrief join entity.

A Caveat is associated with a Brief and can have sub-classes that link it to a specific entity. In this case, ItemCaveat is associated with an Item.

In order to retrieve a collection of Item records that belong to an Org, as well as the associated ItemCaveat records, a User must have:

  • An OrgUser association with the Org
  • At least one role tied to the Org via an OrgUserRole
  • The ItemCaveat records retrieved are based on the Brief records a User is associated with given their UserBrief records

The above entities are defined in the Authorization.Data/Entities library

The business logic for the above entities are defined in the Authorization.Data/Extensions library

The API endpoints for the above entities are defined in the Authorization.Web/Controllers library

The Angular models and services for the above entities are defined in the models/api and services/api Angular folders, respectively.

caveats

API Authorization

Back to Top

API Authorization is purely driven by the Authorize methods defined in the AuthExtensions.cs class. There are three different scenarios for Authorize that determine how access is permitted. Each scenario has two overloads, based on whether or not the exec delegate function returns a value (Task or Task<T>).

api-auth

Every Authorize method receives a Func<AppDbContext, Task> exec or Func<AppDbContext, Task<T> exec argument, which is a delegate function for the Task to execute if authorization is successful.

User is an Administrator:

public static async Task<T> Authorize<T>(this AppDbContext db, IUserProvider provider, Func<AppDbContext, Task<T>> exec)
{
    if (await db.ValidateAdmin(provider.CurrentUser.Guid.Value))
    {
        return await exec(db);
    }
    else
    {
        throw new AppException($"{provider.CurrentUser.SamAccountName} is not an administrator", ExceptionType.Authorization);
    }
}

public static async Task Authorize(this AppDbContext db, IUserProvider provider, Func<AppDbContext, Task> exec)
{
    if (await db.ValidateAdmin(provider.CurrentUser.Guid.Value))
    {
        await exec(db);
    }
    else
    {
        throw new AppException($"{provider.CurrentUser.SamAccountName} is not an administrator", ExceptionType.Authorization);
    }
}

User has any role in an Org:

public async Task<T> Authorize<T>(this AppDbContext db, IUserProvider provider, string org, Func<AppDbContext, Task<T>> exec)
{
    if (await db.ValidateAnyRole(org, provider.CurrentUser.Guid.Value))
    {
        return await exec(db);
    }
    else
    {
        throw new AppException($"{provider.CurrentUser.SamAccountName} is not authorized to access this resource", ExceptionType.Authorization);
    }
}

public static async Task Authorize(this AppDbContext db, IUserProvider provider, string org, Func<AppDbContext, Task> exec)
{
    if (await db.ValidateAnyRole(org, provider.CurrentUser.Guid.Value))
    {
        await exec(db);
    }
    else
    {
        throw new AppException($"{provider.CurrentUser.SamAccountName} is not authorized to access this resource", ExceptionType.Authorization);
    }
}

User has one of a set of roles in an Org:

public static async Task<T> Authorize<T>(this AppDbContext db, IUserProvider provider, string org, Func<AppDbContext, Task<T>> exec, params string[] roles)
{
    if (roles.Count() < 1)
        throw new AppException("A role must be provided for Org authorization", ExceptionType.Validation);

    if (await db.ValidateRole(org, provider.CurrentUser.Guid.Value, roles))
    {
        return await exec(db);
    }
    else
    {
        throw new AppException($"{provider.CurrentUser.SamAccountName} is not authorized to access this resource", ExceptionType.Authorization);
    }
}

public static async Task Authorize(this AppDbContext db, IUserProvider provider, string org, Func<AppDbContext, Task> exec, params string[] roles)
{
    if (roles.Count() < 1)
        throw new AppException("A role must be provided for Org authorization", ExceptionType.Validation);

    if (await db.ValidateRole(org, provider.CurrentUser.Guid.Value, roles))
    {
        await exec(db);
    }
    else
    {
        throw new AppException($"{provider.CurrentUser.SamAccountName} is not authorized to access this resource", ExceptionType.Authorization);
    }
}

Example usage of the methods can be found in two places:

You'll notice in the ItemController examples that an org argument is received, but not specified in the route definition for the method. This is because the root route signature for ItemController is [Route("api/[controller]/{org}")].

Any method where data is retrieved in ItemController require that a user has any role within an Org:

[HttpGet("[action]")]
public async Task<List<Item>> GetItems([FromRoute]string org) =>
    await db.Authorize(provider, org, async db => await db.GetItems(org));

Any method where data is manipulated in ItemController requires that a user has the Tech role within an Org:

[HttpPost("[action]")]
public async Task AddItem([FromRoute]string org, [FromBody]Item item) =>
    await db.Authorize(provider, org, async db => await db.AddItem(item), "Tech");

Any method where data is manipulated in BriefController requires that a user is an administrator:

[HttpPost("[action]")]
public async Task AddBrief([FromBody]Brief brief) =>
    await db.Authorize(provider, async db => await db.AddBrief(brief));

Angular Authorization

Back to Top

In Angular, Route Guards are used to provide logic which determines whether or not a route can be accessed. The AuthContextService defines all of the functions necessary for performing authorization within a route guard.

ng-auth

In this demo application, two guards are defined: AdminGuard and OrgGuard.

Guards are defined in the guards Angular folder

admin.guard.ts

import { Injectable } from '@angular/core';

import {
  CanActivate,
  Router
} from '@angular/router';

import { AuthContextService } from '../services';

@Injectable()
export class AdminGuard implements CanActivate {
  constructor(
    private authContext: AuthContextService,
    private router: Router
  ) { }

  canActivate(): Promise<boolean> {
    return this.checkLogin();
  }

  checkLogin = (): Promise<boolean> =>
    new Promise(async (resolve) => {
      const res = await this.authContext.validateAdmin();
      !res && this.denyAccess(`You must be an app administrator to access this route`);
      resolve(res);
    });

  denyAccess = (message: string) => this.router.navigate(['/denied', message])
}

org.guard.ts

import { Injectable } from '@angular/core';

import {
  CanActivate,
  Router
} from '@angular/router';

import { AuthContextService } from '../services';

@Injectable()
export class OrgGuard implements CanActivate {
  constructor(
    private authContext: AuthContextService,
    private router: Router
  ) { }

  canActivate(): Promise<boolean> {
    return this.checkLogin();
  }

  checkLogin = (): Promise<boolean> =>
    new Promise(async (resolve) => {
      let context = this.authContext.readAuthContext();
      if (!context || !context.org) {
        context = await this.authContext.getDefaultContext();
      }

      const res = await this.authContext.validateAnyRole(context.org.name);
      !res && this.denyAccess(`You must have a role in ${context.org.name} to access this route`);
      resolve(res);
    });

  denyAccess = (message: string) => this.router.navigate(['/denied', message]);
}

In order to lock down routes using the guards, they must be passed into the Route.canActivate array in the route definition:

export const Routes: Route[] = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AdminGuard],
    children: AdminRoutes
  },
  { path: 'config', component: ConfigComponent, children: ConfigRoutes },
  { path: 'denied', component: DeniedComponent },
  { path: 'denied/:message', component: DeniedComponent },
  { path: 'items', component: ItemsComponent, canActivate: [OrgGuard] },
  { path: '', redirectTo: 'config', pathMatch: 'full' },
  { path: '**', redirectTo: 'config', pathMatch: 'full' }
];

Angular Layout Restriction

Back to Top

Because the User and AuthContext instances are streamed into the application interface, components can be conditionally rendered based on certain criteria.

For instance, in layout.view.html, the links in the <sidepanel> are conditionally rendered as follows:

<sidepanel>
    <panel-link link="/items"
                label="Org Items"
                icon="storefront"
                [state]="state"
                *ngIf="auth.org && auth.roles.length > 0"></panel-link>
    <panel-link link="/admin"
                label="App Settings"
                icon="settings"
                [state]="state"
                *ngIf="auth.user.isAdmin"></panel-link>
</sidepanel>

Another example of this is in items.component.html, where if the current user has the Tech role for the current Org context, they can manipulate the collection of items in the view. This is accomplished in ngOnInit() of items.component.ts where the AuthContextService.auth$ stream is used to check for the presence of the Tech role in order to set an authorized variable on the component. This variable can also be passed along to child components in order to determine whether their action buttons are visible.

About

Organization Role-Based Hybrid Authorization research using ASP.NET Core and Angular

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published