Skip to content
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of what's contained in this code is related to what Devin is doing here; #457. So there might be some co-ordination required.

It's fine to populate the user account in JASPER with some basic information regarding a user authenticated by KeyCloak, but we still need to verify and validate (authorize) their access to JASPER before providing any access.

Short term:

  • If the user is not assigned to one of the known, expected, KeyCloak groups - then access denied.

Long term:

  • Existing account - access granted based on their account status (enabled/disabled), and assigned roles and permissions.
  • First time login - Populate basic account info (only if they request access or have a PCSS account), then verify and validate the request
    • Check with PCSS (if/when possible) - User has a valid account, configure their JASPER account accordingly and grant them equivalent access to JASPER.
    • No existing PCSS account - Provide the user with the option to request access.
      • Regardless of whether the user requests access - Notify an admin there was a failed login attempt.
      • User requests access - Notify an admin of the request and provide link the pre-populated account info for review and approval.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Wade, Ronnie and I talked about this, and we think this is logic is consistent with the goals you've listed above. Primarily, this is because Ronnie is only creating a skeleton user during login here - the logic surrounding the judge assignment as well as roles and permissions will not execute unless there is corresponding data in PCSS/Mongo.

However, Ronnie will need to update this to set the skeleton user to disabled.

So when a user logs in for the first time, this logic will run, the skeleton user gets created (which allows an admin to track failed authorization attempts). Next the frontend will redirect the user to the access request page, where they have the option of requesting access. This will allow the user to set a flag indicating their request for admin review.

Of course, the missing part here is the PCSS role synchronization, but that will come in its own PR.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Scv.Api.Helpers;
using Scv.Api.Helpers.Extensions;
using Scv.Api.Infrastructure.Encryption;
using Scv.Api.Models.AccessControlManagement;
using Scv.Api.Services;
using Scv.Db.Models;

Expand Down Expand Up @@ -171,34 +172,7 @@ await cookieCtx.HttpContext.SignOutAsync(CookieAuthenticationDefaults
new Claim(CustomClaimTypes.IsSupremeUser, isSupremeUser.ToString()),
});

// Get JudgeId and HomeLocationId from env variable until login process is finalized.
var judgeId = configuration.GetNonEmptyValue("PCSS:JudgeId");
var homeLocationId = configuration.GetNonEmptyValue("PCSS:JudgeHomeLocationId");

logger.LogInformation("Acting as Judge Id - {JudgeId}.", judgeId);

claims.Add(new Claim(CustomClaimTypes.JudgeId, judgeId));
claims.Add(new Claim(CustomClaimTypes.JudgeHomeLocationId, homeLocationId));

// Remove checking when the "real" mongo db has been configured
var connectionString = configuration.GetValue<string>("MONGODB_CONNECTION_STRING");
if (!string.IsNullOrEmpty(connectionString))
{
// Add user's permissions and roles as claims
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
var userDto = await userService.GetWithPermissionsAsync(context.Principal.Email());
if (userDto != null)
{
// UserId's value refers to the id in the User collection from MongoDb.
claims.Add(new Claim(CustomClaimTypes.UserId, userDto.Id));

var permissionsClaims = userDto.Permissions.Select(p => new Claim(CustomClaimTypes.Permission, p));
claims.AddRange(permissionsClaims);

var rolesClaims = userDto.Roles.Select(r => new Claim(CustomClaimTypes.Role, r));
claims.AddRange(rolesClaims);
}
}
await OnPostAuthSuccess(configuration, context, logger, claims);

identity.AddClaims(claims);
},
Expand Down Expand Up @@ -271,5 +245,92 @@ await cookieCtx.HttpContext.SignOutAsync(CookieAuthenticationDefaults

return services;
}

private static async Task OnPostAuthSuccess(
IConfiguration configuration,
Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext context,
ILogger logger,
List<Claim> claims)
{
var judgeId = configuration.GetNonEmptyValue("PCSS:JudgeId");
var homeLocationId = configuration.GetNonEmptyValue("PCSS:JudgeHomeLocationId");

// Remove checking when the "real" mongo db has been configured
var connectionString = configuration.GetValue<string>("MONGODB_CONNECTION_STRING");
if (string.IsNullOrWhiteSpace(connectionString))
{
// Defaults the logged in user to the default judge.
logger.LogInformation("Acting as Judge Id - {JudgeId}.", judgeId);
claims.Add(new Claim(CustomClaimTypes.JudgeId, judgeId));
claims.Add(new Claim(CustomClaimTypes.JudgeHomeLocationId, homeLocationId));
return;
}

try
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
var userDto = await userService.GetWithPermissionsAsync(context.Principal.Email());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ronaldo-macapobre Looking through this again and I'm not sure this is safe, as this will create a new user if the user's email changes. I think in most cases email is stable but may not always be - I know in the case of sso standard they recommend that the preferred_username guid is used instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks for catching that. I only used the email for duplicate checking as a quick safeguard, but we can definitely update it. I’ll switch the code to use preferred_username when I pick up a future story that involves this.


// Insert new user to the db. In the future, we need to ensure that the currently
// logged on user has an account in PCSS before it is added to the db.
if (userDto == null)
{
userDto = new UserDto
{
FirstName = context.Principal.FindFirstValue(ClaimTypes.GivenName),
LastName = context.Principal.FindFirstValue(ClaimTypes.Surname),
Email = context.Principal.Email(),
UserGuid = context.Principal.UserGuid(),
IsActive = false
};

var result = await userService.AddAsync(userDto);
if (!result.Succeeded)
{
logger.LogInformation("Unable to add user: {Message}", result.Errors);
}

logger.LogInformation("A user has been added to the db.");

userDto = result.Payload;
}

// Attempt to override the default Judge Id and HomeLocationId if the current user has been mapped.
if (userDto.JudgeId != null)
{
var dashboardService = context.HttpContext.RequestServices.GetRequiredService<IDashboardService>();

var judges = await dashboardService.GetJudges();

var judge = judges.FirstOrDefault(j => j.PersonId == userDto.JudgeId);
if (judge != null)
{
judgeId = judge.PersonId.ToString();
homeLocationId = judge.HomeLocationId.ToString();
}
}

// UserId's value refers to the id in the User collection from MongoDb.
claims.Add(new Claim(CustomClaimTypes.UserId, userDto.Id));

// Add Roles and Permissions as claims if available
var permissionsClaims = userDto.Permissions.Select(p => new Claim(CustomClaimTypes.Permission, p));
claims.AddRange(permissionsClaims);

var rolesClaims = userDto.Roles.Select(r => new Claim(CustomClaimTypes.Role, r));
claims.AddRange(rolesClaims);
}
catch (Exception ex)
{
logger.LogError(ex, "Something went wrong during post authentication process: {Exception}", ex);
}
finally
{
// Add the final value of judgeId and homeLocationId as claims of the current user
logger.LogInformation("Acting as Judge Id - {JudgeId}.", judgeId);
claims.Add(new Claim(CustomClaimTypes.JudgeId, judgeId));
claims.Add(new Claim(CustomClaimTypes.JudgeHomeLocationId, homeLocationId));
}
}
}
}
12 changes: 12 additions & 0 deletions api/Models/AccessControlManagement/UserDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ public class UserDto : BaseDto
public bool IsActive { get; set; }
public Guid? ADId { get; set; }
public string ADUsername { get; set; }
/// <summary>
/// Guid from DIAM
/// </summary>
public string UserGuid { get; set; }
/// <summary>
/// Guid from ProvJud. This is going to be mapped manually for now.
/// </summary>
public string NativeGuid { get; set; }
/// <summary>
/// Id used as parameter for external systems backend APIs. This is going to be mapped manually for now.
/// </summary>
public int? JudgeId { get; set; }
public List<string> GroupIds { get; set; } = [];
public List<string> Permissions { get; set; } = [];
public List<string> Roles { get; set; } = [];
Expand Down
15 changes: 15 additions & 0 deletions db/Models/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ public class User : EntityBase

public string ADUsername { get; set; }

/// <summary>
/// Guid from DIAM
/// </summary>
public string UserGuid { get; set; }

/// <summary>
/// Guid from ProvJud. This is going to be populated manually for now.
/// </summary>
public string NativeGuid { get; set; }

/// <summary>
/// Id used as parameter for external systems backend APIs. This is going to be mapped manually for now.
/// </summary>
public int? JudgeId { get; set; }

public List<string> GroupIds { get; set; } = [];
}
}
Loading