Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a8cabdb
Add migration to insert a deleted user into the Users table
EinarSalbouvet Jun 8, 2026
6c1954a
Merge branch 'main' into feat/deleted-user
EinarSalbouvet Jul 15, 2026
56a4d7c
Remove deleted user data from Users table and associated migration files
EinarSalbouvet Jul 15, 2026
2fab124
Add migration to insert a "Deleted User" record in the Users table
EinarSalbouvet Jul 15, 2026
ba8f6df
Potential fix for pull request finding
EinarSalbouvet Jul 15, 2026
a3b6663
Potential fix for pull request finding
EinarSalbouvet Jul 15, 2026
81f05d7
Fix SQL update statement in DeleteByIdsAsync to prevent redundant par…
EinarSalbouvet Jul 15, 2026
86ecd1c
Merge branch 'feat/deleted-user' of https://github.com/equinor/prisma…
EinarSalbouvet Jul 15, 2026
2e1c70e
Remove migration for deleted user and update table mappings to use nu…
EinarSalbouvet Jul 22, 2026
139fb8c
Merge branch 'main' into feat/deleted-user
EinarSalbouvet Jul 22, 2026
1018bed
Add migration to insert a deleted user and update table mappings
EinarSalbouvet Jul 22, 2026
621d46b
Add migration to insert a deleted user into the Users table
EinarSalbouvet Jun 8, 2026
1551c63
Remove deleted user data from Users table and associated migration files
EinarSalbouvet Jul 15, 2026
6decc7d
Add migration to insert a "Deleted User" record in the Users table
EinarSalbouvet Jul 15, 2026
6055d4e
Potential fix for pull request finding
EinarSalbouvet Jul 15, 2026
7c0b4bb
Fix SQL update statement in DeleteByIdsAsync to prevent redundant par…
EinarSalbouvet Jul 15, 2026
75713ca
Potential fix for pull request finding
EinarSalbouvet Jul 15, 2026
78a7833
Remove migration for deleted user and update table mappings to use nu…
EinarSalbouvet Jul 22, 2026
266def3
Add migration to insert a deleted user and update table mappings
EinarSalbouvet Jul 22, 2026
e6a3933
Merge branch 'feat/deleted-user' of https://github.com/equinor/prisma…
EinarSalbouvet Aug 13, 2026
16abf5f
Add migration to insert a "Deleted User" record into the Users table
EinarSalbouvet Aug 13, 2026
c8284a3
Merge branch 'main' into feat/deleted-user
EinarSalbouvet Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions PrismaDotnetApi/PrismaApi.Api/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,17 @@ public IActionResult AuthPlaceholder(CancellationToken ct = default)
{
return StatusCode(StatusCodes.Status501NotImplemented);
}

[HttpDelete("users/{userId}")]
// <summary>
// Deletes a user. Users can only delete themselves, and the user will be anonymized in the database, but the record will not be deleted for audit purposes.
// </summary>
// <exception cref="InvalidOperationException">Thrown when a user attempts to delete a different user.</exception>
Comment thread
EinarSalbouvet marked this conversation as resolved.
public async Task<IActionResult> DeleteUser([FromRoute] string userId, CancellationToken ct = default)
{
var user = HttpContext.GetLoadedUser();

await _userService.DeleteUserAsync(userId, user, ct);
return NoContent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public interface IUserService
Task<List<UserOutgoingDto>> SearchUsersAsync(string query);
Task<UserOutgoingDto> GetOrCreateUserFromContextAsync(HttpContext context);
Task<List<UserOutgoingDto>> GetByIdsAsync(IEnumerable<string> ids);
Task DeleteUserAsync(string userId, UserOutgoingDto user, CancellationToken ct = default);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Graph.IdentityGovernance.EntitlementManagement.Assignments.AdditionalAccessWithAccessPackageIdWithIncompatibleAccessPackageId;
using PrismaApi.Application.Interfaces.Repositories;
using PrismaApi.Application.Mapping;
using PrismaApi.Domain.Constants;
using PrismaApi.Domain.Dtos;
using PrismaApi.Domain.Entities;
Comment thread
Copilot marked this conversation as resolved.
using PrismaApi.Infrastructure.Context;
Expand Down Expand Up @@ -79,4 +81,66 @@

return user;
}

/// <summary>
/// Deletes users by their ids.
/// Before deleting, it updates all auditable entities that reference the user to point to the deleted user entry.
/// This is done to avoid foreign key constraint violations when deleting a user.
/// Also, deletes all related project roles for the user
/// </summary>
public override async Task DeleteByIdsAsync(IEnumerable<string> ids, Expression<Func<User, bool>>? filterPredicate = null, CancellationToken ct = default)
{
var entries = await DbContext.Users
.Where(u => ids.Contains(u.Id))
.ToListAsync(cancellationToken: ct);
Comment thread
Copilot marked this conversation as resolved.

if (entries.Count == 0)
return;

// update all auditable entities to point to deleted user entry
var connection = DbContext.Database.GetDbConnection();
using var cmd = connection.CreateCommand();
var idParams = ids.Select((id, i) =>
{
var param = cmd.CreateParameter();
param.ParameterName = $"@id{i}";
param.Value = id;
return param;
}).ToArray();
var inClause = string.Join(",", idParams.Select(p => p.ParameterName));

// get all auditable entity types that need the user id updated to the deleted user id.
// This is done to avoid foreign key constraint violations when deleting a user.
var auditableEntityTypes = DbContext.Model.GetEntityTypes()
.Where(t => typeof(AuditableEntity).IsAssignableFrom(t.ClrType) && !t.ClrType.IsAbstract);

foreach (var entityType in auditableEntityTypes)
{
var tableName = entityType.GetTableName();

// using raw SQL over the parameterized ExecuteSqlAsync due to issues with the in clause.
// ExecuteSqlAsync protects against SQL injection,
// but all inputed data are controlled by the api and takes no user input.
await DbContext.Database.ExecuteSqlRawAsync($"""

Check warning on line 124 in PrismaDotnetApi/PrismaApi.Application/Repositories/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Integration tests

Method 'ExecuteSqlRawAsync' inserts interpolated strings directly into the SQL, without any protection against SQL injection. Consider using 'ExecuteSqlAsync' instead, which protects against SQL injection, or make sure that the value is sanitized and suppress the warning.

Check warning on line 124 in PrismaDotnetApi/PrismaApi.Application/Repositories/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build

Method 'ExecuteSqlRawAsync' inserts interpolated strings directly into the SQL, without any protection against SQL injection. Consider using 'ExecuteSqlAsync' instead, which protects against SQL injection, or make sure that the value is sanitized and suppress the warning.
UPDATE [{tableName}]
SET CreatedById = '{DomainConstants.DeletedUserId}'
WHERE CreatedById IN ({inClause});
UPDATE [{tableName}]
SET UpdatedById = '{DomainConstants.DeletedUserId}'
WHERE UpdatedById IN ({inClause});
""", [.. idParams.Concat(idParams).Cast<object>()], ct);
Comment thread
Copilot marked this conversation as resolved.
Outdated
}

// delete project roles
var projectRoles = await DbContext.ProjectRoles
.Where(e => ids.Contains(e.UserId))
.ToListAsync(cancellationToken: ct);

DbContext.ProjectRoles.RemoveRange(projectRoles);
foreach (var entry in entries)
{
DbContext.Users.Remove(entry);
}
await DbContext.SaveChangesAsync(ct);
}
Comment thread
EinarSalbouvet marked this conversation as resolved.
}
10 changes: 10 additions & 0 deletions PrismaDotnetApi/PrismaApi.Application/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,14 @@ public Task<UserOutgoingDto> GetOrCreateUserFromContextAsync(HttpContext context

public Task<List<UserOutgoingDto>> SearchUsersAsync(string query)
=> _userProvider.SearchUsersAsync(query);

public async Task DeleteUserAsync(string userId, UserOutgoingDto user, CancellationToken ct = default)
{
// user is the user making the request, and only allows to delete itself.
if (userId != user.Id)
{
throw new InvalidOperationException("Users can only delete themselves.");
}
await _userRepository.DeleteByIdsAsync([userId], ct: ct);
}
}
3 changes: 3 additions & 0 deletions PrismaDotnetApi/PrismaApi.Domain/Constants/DomainConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public static class DomainConstants
public const int MaxOpacity = 100;
public const int MinOpacity = 0;
public const int DefaultTextSize = 24;
public static readonly string DeletedUserId =
"d008bfdf-fe89-48f3-80a8-777bba4d9bbf";
public static readonly string DeletedUserName = "Deleted User";
public static readonly Guid DefaultValueMetricId =
Guid.Parse("288e0811-7ab6-5d24-b80c-9fa925b848a6");
public static readonly string DefaultValueMetricName = "value";
Expand Down
7 changes: 7 additions & 0 deletions PrismaDotnetApi/PrismaApi.Domain/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,12 @@ public static void OnModelConfiguring(ModelBuilder modelBuilder)
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).HasMaxLength(DomainConstants.MaxShortStringLength);
});
modelBuilder.Entity<User>().HasData(new User
{
Id = DomainConstants.DeletedUserId,
Name = DomainConstants.DeletedUserName,
CreatedAt = new DateTimeOffset(new DateTime(2020, 1, 1, 1, 1, 1, 1, DateTimeKind.Utc).AddTicks(1), TimeSpan.Zero),
UpdatedAt = new DateTimeOffset(new DateTime(2020, 1, 1, 1, 1, 1, 1, DateTimeKind.Utc).AddTicks(2), TimeSpan.Zero)
Comment thread
EinarSalbouvet marked this conversation as resolved.
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ private async Task EnforceMinimumProjectRoles(CancellationToken cancellationToke
var currentFacilitatorCount = await ProjectRoles
.AsNoTracking()
.CountAsync(r => r.ProjectId == projectId &&
r.Role.IsFacilitator(),
r.Role.ToLower() == ProjectRoleType.Facilitator.ToString().ToLower(),
cancellationToken);
Comment thread
EinarSalbouvet marked this conversation as resolved.

if (currentFacilitatorCount - facilitatorsBeingRemoved <= 0)
Expand Down
10 changes: 10 additions & 0 deletions PrismaDotnetApi/PrismaApi.Test/Mocks/TestUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,14 @@ public async Task<List<UserOutgoingDto>> GetByIdsAsync(IEnumerable<string> ids)
var users = await _userRepository.GetByIdsAsync(ids, withTracking: false);
return users.ToOutgoingDtos();
}

public async Task DeleteUserAsync(string userId, UserOutgoingDto user, CancellationToken ct = default)
{
// user is the user making the request, and only allows to delete itself.
if (userId != user.Id)
{
throw new InvalidOperationException("Users can only delete themselves.");
}
await _userRepository.DeleteByIdsAsync([userId], ct: ct);
}
}
Loading
Loading