Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logging who runs a robot mission (action log in flotilla) #1712

Merged
merged 9 commits into from
Aug 13, 2024
4 changes: 3 additions & 1 deletion backend/api.test/Database/DatabaseUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class DatabaseUtilities
private readonly PlantService _plantService;
private readonly RobotModelService _robotModelService;
private readonly RobotService _robotService;
private readonly UserInfoService _userInfoService;

public DatabaseUtilities(FlotillaDbContext context)
{
Expand All @@ -30,7 +31,8 @@ public DatabaseUtilities(FlotillaDbContext context)
_plantService = new PlantService(context, _installationService, _accessRoleService);
_deckService = new DeckService(context, defaultLocalizationPoseService, _installationService, _plantService, _accessRoleService, new MockSignalRService());
_areaService = new AreaService(context, _installationService, _plantService, _deckService, defaultLocalizationPoseService, _accessRoleService);
_missionRunService = new MissionRunService(context, new MockSignalRService(), new Mock<ILogger<MissionRunService>>().Object, _accessRoleService);
_userInfoService = new UserInfoService(context, new HttpContextAccessor(), new Mock<ILogger<UserInfoService>>().Object);
_missionRunService = new MissionRunService(context, new MockSignalRService(), new Mock<ILogger<MissionRunService>>().Object, _accessRoleService, _userInfoService);
_robotModelService = new RobotModelService(context);
_robotService = new RobotService(context, new Mock<ILogger<RobotService>>().Object, _robotModelService, new MockSignalRService(), _accessRoleService, _installationService, _areaService, _missionRunService);
}
Expand Down
4 changes: 3 additions & 1 deletion backend/api.test/EventHandlers/TestMissionEventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ public TestMissionEventHandler(DatabaseFixture fixture)

var signalRService = new MockSignalRService();
var accessRoleService = new AccessRoleService(context, new HttpContextAccessor());
var userInfoService = new UserInfoService(context, new HttpContextAccessor(), new Mock<ILogger<UserInfoService>>().Object);

_mqttService = new MqttService(mqttServiceLogger, configuration);
_missionRunService = new MissionRunService(context, signalRService, missionLogger, accessRoleService);

_missionRunService = new MissionRunService(context, signalRService, missionLogger, accessRoleService, userInfoService);


var echoServiceMock = new MockEchoService();
Expand Down
4 changes: 3 additions & 1 deletion backend/api.test/Services/MissionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ public class MissionServiceTest : IDisposable
private readonly MissionRunService _missionRunService;
private readonly ISignalRService _signalRService;
private readonly IAccessRoleService _accessRoleService;
private readonly UserInfoService _userInfoService;

public MissionServiceTest(DatabaseFixture fixture)
{
_context = fixture.NewContext;
_logger = new Mock<ILogger<MissionRunService>>().Object;
_signalRService = new MockSignalRService();
_accessRoleService = new AccessRoleService(_context, new HttpContextAccessor());
_missionRunService = new MissionRunService(_context, _signalRService, _logger, _accessRoleService);
_userInfoService = new UserInfoService(_context, new HttpContextAccessor(), new Mock<ILogger<UserInfoService>>().Object);
_missionRunService = new MissionRunService(_context, _signalRService, _logger, _accessRoleService, _userInfoService);
_databaseUtilities = new DatabaseUtilities(_context);
}

Expand Down
4 changes: 3 additions & 1 deletion backend/api.test/Services/RobotService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class RobotServiceTest : IDisposable
private readonly IDeckService _deckService;
private readonly IAreaService _areaService;
private readonly IMissionRunService _missionRunService;
private readonly IUserInfoService _userInfoService;

public RobotServiceTest(DatabaseFixture fixture)
{
Expand All @@ -39,7 +40,8 @@ public RobotServiceTest(DatabaseFixture fixture)
_defaultLocalizationPoseService = new DefaultLocalizationPoseService(_context);
_deckService = new DeckService(_context, _defaultLocalizationPoseService, _installationService, _plantService, _accessRoleService, _signalRService);
_areaService = new AreaService(_context, _installationService, _plantService, _deckService, _defaultLocalizationPoseService, _accessRoleService);
_missionRunService = new MissionRunService(_context, _signalRService, new Mock<ILogger<MissionRunService>>().Object, _accessRoleService);
_userInfoService = new UserInfoService(_context, new HttpContextAccessor(), new Mock<ILogger<UserInfoService>>().Object);
_missionRunService = new MissionRunService(_context, _signalRService, new Mock<ILogger<MissionRunService>>().Object, _accessRoleService, _userInfoService);
}

public void Dispose()
Expand Down
1 change: 1 addition & 0 deletions backend/api/Database/Context/FlotillaDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class FlotillaDbContext(DbContextOptions options) : DbContext(options)
public DbSet<SafePosition> SafePositions => Set<SafePosition>();
public DbSet<DefaultLocalizationPose> DefaultLocalizationPoses => Set<DefaultLocalizationPose>();
public DbSet<AccessRole> AccessRoles => Set<AccessRole>();
public DbSet<UserInfo> UserInfos => Set<UserInfo>();

// Timeseries:
public DbSet<RobotPressureTimeseries> RobotPressureTimeseries => Set<RobotPressureTimeseries>();
Expand Down
15 changes: 15 additions & 0 deletions backend/api/Database/Models/UserInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

#pragma warning disable CS8618
namespace Api.Database.Models
{
public class UserInfo
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
[Required]
public string Oid { get; set; }
}
}
2 changes: 1 addition & 1 deletion backend/api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
builder.Services.AddScoped<RobotController>();
builder.Services.AddScoped<EmergencyActionController>();
builder.Services.AddScoped<InspectionFindingService>();

builder.Services.AddScoped<IUserInfoService, UserInfoService>();
builder.Services.AddTransient<ISignalRService, SignalRService>();

builder.Services.AddHostedService<InspectionFindingEventHandler>();
Expand Down
15 changes: 14 additions & 1 deletion backend/api/Services/MissionRunService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ public class MissionRunService(
FlotillaDbContext context,
ISignalRService signalRService,
ILogger<MissionRunService> logger,
IAccessRoleService accessRoleService) : IMissionRunService
IAccessRoleService accessRoleService,
IUserInfoService userInfoService) : IMissionRunService
{
public async Task<MissionRun> Create(MissionRun missionRun, bool triggerCreatedMissionRunEvent = true)
{
Expand Down Expand Up @@ -104,6 +105,18 @@ public async Task<MissionRun> Create(MissionRun missionRun, bool triggerCreatedM
OnMissionRunCreated(args);
}

try
{
var userInfo = await userInfoService.GetRequestedUserInfo();
if (userInfo != null)
{
logger.LogInformation($"Mission run created by user with Id {userInfo.Id}");
}
}
catch (HttpRequestException e)
{
logger.LogInformation(e, $"Failed to log user information because: {e.Message}");
}
return missionRun;
}

Expand Down
104 changes: 104 additions & 0 deletions backend/api/Services/UserInfoServices.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Api.Database.Context;
using Api.Database.Models;
using Api.Utilities;
using Microsoft.EntityFrameworkCore;

namespace Api.Services
{
public interface IUserInfoService
{
public Task<IEnumerable<UserInfo>> ReadAll();

public Task<UserInfo?> ReadById(string id);

public Task<UserInfo> Create(UserInfo userInfo);

public Task<UserInfo> Update(UserInfo userInfo);

public Task<UserInfo?> Delete(string id);

public Task<UserInfo?> GetRequestedUserInfo();
}

[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Globalization",
"CA1309:Use ordinal StringComparison",
Justification = "EF Core refrains from translating string comparison overloads to SQL"
)]
public class UserInfoService(FlotillaDbContext context, IHttpContextAccessor httpContextAccessor, ILogger<UserInfoService> logger) : IUserInfoService
{
public async Task<IEnumerable<UserInfo>> ReadAll()
{
return await GetUsersInfo().ToListAsync();
}

private DbSet<UserInfo> GetUsersInfo()
{
return context.UserInfos;
}

public async Task<UserInfo?> ReadById(string id)
{
return await GetUsersInfo()
.FirstOrDefaultAsync(a => a.Id.Equals(id));
}

public async Task<UserInfo?> ReadByOid(string oid)
{
return await GetUsersInfo()
.FirstOrDefaultAsync(a => a.Oid.Equals(oid));
}

public async Task<UserInfo> Create(UserInfo userInfo)
{
await context.UserInfos.AddAsync(userInfo);
await context.SaveChangesAsync();
return userInfo;
}

public async Task<UserInfo> Update(UserInfo userInfo)
{
var entry = context.Update(userInfo);
await context.SaveChangesAsync();
return entry.Entity;
}

public async Task<UserInfo?> Delete(string id)
{
var userInfo = await GetUsersInfo()
.FirstOrDefaultAsync(ev => ev.Id.Equals(id));
if (userInfo is null)
{
return null;
}

context.UserInfos.Remove(userInfo);
await context.SaveChangesAsync();

return userInfo;
}

public async Task<UserInfo?> GetRequestedUserInfo()
{
if (httpContextAccessor.HttpContext == null)
throw new HttpRequestException("User Info can only be requested in authenticated HTTP requests.");

string? objectId = httpContextAccessor.HttpContext.GetUserObjectId();
if (objectId is null)
{
logger.LogWarning("User objectId is null so it will not be added to the database.");
return null;
}
var userInfo = await ReadByOid(objectId);
if (userInfo is null)
{
var newUserInfo = new UserInfo
{
Oid = objectId
};
userInfo = await Create(newUserInfo);
}
return userInfo;
}
}
}
23 changes: 17 additions & 6 deletions backend/api/Utilities/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@ public static string GetRequestToken(this HttpContext client)

public static List<System.Security.Claims.Claim> GetRequestedRoles(this HttpContext client)
{
string accessTokenBase64 = client.GetRequestToken();

var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(accessTokenBase64);

var claims = jwtSecurityToken.Claims;
var claims = client.GetRequestedClaims();
var roles = claims.Where((c) => c.Type == "roles" || c.Type.EndsWith("role", StringComparison.CurrentCulture)).ToList();
return roles;
}
Expand All @@ -31,5 +26,21 @@ public static List<string> GetRequestedRoleNames(this HttpContext client)
var roleClaims = GetRequestedRoles(client);
return roleClaims.Select((c) => c.Value).ToList();
}

public static List<System.Security.Claims.Claim> GetRequestedClaims(this HttpContext client)
betaniat marked this conversation as resolved.
Show resolved Hide resolved
{
string accessTokenBase64 = client.GetRequestToken();
var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(accessTokenBase64);
return jwtSecurityToken.Claims.ToList();
}

public static string? GetUserObjectId(this HttpContext client)
{
var claims = client.GetRequestedClaims();
var objectIdClaim = claims.FirstOrDefault(c => c.Type == "oid");
if (objectIdClaim is null) { return null; }
return objectIdClaim.Value;
}
}
}
Loading