Skip to content

Commit

Permalink
Merge pull request #251 from ucdavis/swe/AddEmulation
Browse files Browse the repository at this point in the history
Add user emulation and System role authorization
  • Loading branch information
srkirkland authored Jan 2, 2024
2 parents d0c68a4 + f033600 commit 6778870
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 8 deletions.
1 change: 1 addition & 0 deletions Finjector.Core/Domain/Role.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal static void OnModelCreating(ModelBuilder modelBuilder)

public class Codes
{
public const string System = "System";
public const string Admin = "Admin";
public const string Edit = "Edit";
public const string View = "View";
Expand Down
6 changes: 6 additions & 0 deletions Finjector.Core/Models/AccessCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Finjector.Core.Models;

public static class AccessCodes
{
public const string SystemAccess = "SystemAccess";
}
6 changes: 6 additions & 0 deletions Finjector.Core/Models/SystemOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Finjector.Core.Models;

public class SystemOptions
{
public string[] Users { get; set; } = Array.Empty<string>();
}
3 changes: 2 additions & 1 deletion Finjector.Web/ClientApp/src/setupProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const context = [
"/api", // anything to /api/* will be proxied
"/signin-oidc",
"/signout-oidc",
"/signout-callback-oidc"
"/signout-callback-oidc",
"/system"
];

const onError = (err, req, resp, target) => {
Expand Down
83 changes: 83 additions & 0 deletions Finjector.Web/Controllers/SystemController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Finjector.Core.Data;
using Finjector.Core.Models;
using Finjector.Core.Services;
using Finjector.Web.Handlers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Serilog;
using System.Security.Claims;

namespace Finjector.Web.Controllers;

[Authorize]
public class SystemController : Controller
{
private readonly AppDbContext _dbContext;
private readonly IIdentityService _identityService;
private readonly IUserService _userService;

public SystemController(AppDbContext dbContext, IIdentityService identityService, IUserService userService)
{
_dbContext = dbContext;
_identityService = identityService;
_userService = userService;
}

[Authorize(Policy = AccessCodes.SystemAccess)]
public async Task<IActionResult> Emulate(string id)
{
var iamId = Request.HttpContext.User.FindFirstValue(IamIdClaimFallbackTransformer.ClaimType);
var currentUser = await _userService.EnsureUserExists(iamId);
Log.Information($"Emulation attempted for {id} by {currentUser.Name}");
var lookupVal = id.Trim();

var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.Email == lookupVal || u.Kerberos == lookupVal);
if (user == null)
{
// not found in db, look up user in IAM
user = lookupVal.Contains("@")
? await _identityService.GetByEmail(lookupVal)
: await _identityService.GetByKerberos(lookupVal);

if (user != null)
{
// user found in IAM but not in our db
// let the UserService handle creating it and setting up account ownership when applicable
await _userService.EnsureUserExists(user.Iam);
}
else
{
throw new Exception("User is null");
}
}

var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Kerberos),
new Claim(ClaimTypes.Name, user.Kerberos),
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName),
new Claim("name", user.Name),
new Claim(ClaimTypes.Email, user.Email),
new Claim("ucdPersonIAMID", user.Iam),
}, CookieAuthenticationDefaults.AuthenticationScheme);

// kill old login
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

// create new login
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));

return LocalRedirect("/");
}

public async Task<IActionResult> EndEmulate()
{
await HttpContext.SignOutAsync();
return LocalRedirect("/");
}
}

25 changes: 25 additions & 0 deletions Finjector.Web/Extensions/AuthorizationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Finjector.Core.Domain;
using Finjector.Core.Models;
using Finjector.Web.Handlers;
using Microsoft.AspNetCore.Authorization;

namespace Finjector.Web.Extensions;

public static class AuthorizationExtensions
{
public static void AddAccessPolicy(this AuthorizationOptions options, string policy)
{
options.AddPolicy(policy, builder => builder.Requirements.Add(new VerifyRoleAccess(GetRoles(policy))));
}

public static string[] GetRoles(string accessCode)
{
return accessCode switch
{
// System requirement can only be fulfilled by a system user
AccessCodes.SystemAccess => new[] { Role.Codes.System },
_ => throw new ArgumentException($"{nameof(accessCode)} is not a valid {nameof(AccessCodes)} constant")
};
}
}

13 changes: 13 additions & 0 deletions Finjector.Web/Handlers/VerifyRoleAccess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;

namespace Finjector.Web.Handlers;

public class VerifyRoleAccess : IAuthorizationRequirement
{
public readonly string[] RoleStrings;

public VerifyRoleAccess(params string[] roleStrings)
{
RoleStrings = roleStrings;
}
}
40 changes: 40 additions & 0 deletions Finjector.Web/Handlers/VerifyRoleAccessHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Finjector.Core.Domain;
using Finjector.Core.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System.Security.Claims;

namespace Finjector.Web.Handlers;

/// <summary>
/// Currently only being used to verify system access, but can be expanded to verify other roles
/// </summary>
public class VerifyRoleAccessHandler : AuthorizationHandler<VerifyRoleAccess>
{
private readonly IHttpContextAccessor _httpContext;
private readonly SystemOptions _systemOptions;

public VerifyRoleAccessHandler(IHttpContextAccessor httpContext, IOptions<SystemOptions> systemOptions)
{
_httpContext = httpContext;
_systemOptions = systemOptions.Value;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, VerifyRoleAccess requirement)
{
var kerbId = context.User.Claims.SingleOrDefault(a => a.Type == ClaimTypes.NameIdentifier)?.Value;


if (string.IsNullOrWhiteSpace(kerbId))
{
return Task.CompletedTask;
}

if (requirement.RoleStrings.Contains(Role.Codes.System) && _systemOptions.Users.Contains(kerbId))
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}
27 changes: 20 additions & 7 deletions Finjector.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
using Finjector.Core.Data;
using System.Configuration;
using Microsoft.EntityFrameworkCore;
using Finjector.Web.Extensions;
using Microsoft.AspNetCore.Authorization;

#if DEBUG
Serilog.Debugging.SelfLog.Enable(msg => Debug.WriteLine(msg));
Expand Down Expand Up @@ -62,6 +64,7 @@
builder.Services.Configure<FinancialOptions>(builder.Configuration.GetSection("Financial"));
builder.Services.Configure<CosmosOptions>(builder.Configuration.GetSection("CosmosDb"));
builder.Services.Configure<AuthOptions>(builder.Configuration.GetSection("Authentication"));
builder.Services.Configure<SystemOptions>(builder.Configuration.GetSection("System"));

builder.Services.AddControllersWithViews();

Expand Down Expand Up @@ -101,10 +104,15 @@
NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"
};
});
builder.Services.AddAuthorization(options =>
{
options.AddAccessPolicy(AccessCodes.SystemAccess);
});
builder.Services.AddScoped<IAuthorizationHandler, VerifyRoleAccessHandler>();

// It's an SDK best practice to use a singleton instance of CosmosClient
builder.Services.AddSingleton<ICosmosDbService, CosmosDbService>();

// Add the IamId to claims if not provided by CAS
builder.Services.AddScoped<IClaimsTransformation, IamIdClaimFallbackTransformer>();
builder.Services.AddScoped<IIdentityService, IdentityService>(); //Lookup IAM to get user
Expand All @@ -114,15 +122,15 @@

builder.Services.AddDbContextPool<AppDbContext, AppDbContextSqlServer>((serviceProvider, o) =>
{
o.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"),
sqlOptions =>
{
sqlOptions.MigrationsAssembly("Finjector.Core");
});
o.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"),
sqlOptions =>
{
sqlOptions.MigrationsAssembly("Finjector.Core");
});
});

var app = builder.Build();


// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
Expand All @@ -143,6 +151,11 @@
pattern: "Account/Login",
defaults: new { controller = "Account", action = "Login" });

app.MapControllerRoute(
name: "system",
pattern: "/{controller}/{action}/{id?}",
constraints: new { controller = "System" });

app.MapControllerRoute(
name: "api",
pattern: "/api/{controller}/{action=Index}/{id?}");
Expand Down
3 changes: 3 additions & 0 deletions Finjector.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,8 @@
"TokenEndpoint": "[External]",
"ScopeApp": "Finjector",
"ScopeEnv": "Production"
},
"System": {
"Users": []
}
}

0 comments on commit 6778870

Please sign in to comment.