0

We are migrating from on-prem database into Azure SQL. We have audit fields in the database that grabs the SUSER_NAME() whenever a DML transaction (i.e., INSERT, UPDATE, DELETE - soft-delete) occurs. Since the on-prem database uses the Windows Domain Active Directory and the users login to the database using Integrated Security we can correctly get the current user's name.

However, when we use the managed identity in Azure SQL, the SUSER_NAME() comes down as a concatenation of two guids: the Managed Identity guid and the service container guid. As a result, we are losing the user name of the current user. Is it possible to get the user name with the SQL Function SUSER_NAME() (or some other SQL function) or will this be a code change where we now pass in the actual user name, for the audit field in question, to the call to the Azure SQL database?

Update 1:

Our application is designed to interact with Azure SQL using the following DbContext constructor:

public SomeDbEntities(IConfiguration objConfiguration, AzureAuthenticationInterceptor azureAuthenticationInterceptor) :
    base(new DbContextOptionsBuilder<SomeDbEntities>()
        .UseSqlServer(objConfiguration.GetValue<string>("Azure:ConnectionStrings:SomeDbEntities"))
        .AddInterceptors(azureAuthenticationInterceptor)
        .Options)
{ }

We are using an azureAuthenticationInterceptor in order to deal with a thread blocking issue. The solution for that was provided here: EF Core Connection to Azure SQL with Managed Identity

Update 2:

The audit fields are being set by triggers using the SUSER_NAME() function that log data to a history table.

J Weezy
  • 3,507
  • 3
  • 32
  • 88

2 Answers2

1

Since you're connecting to Azure SQL with a Service Prinicpal, you have to pass the AAD user's identity somehow. You can use a custom table, or call sp_set_session_context every time you connect.

Here's a example of a DbContext that sets the end user's identity using sp_set_session_context:

public class Order
{
    public int OrderId { get; set; }
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public DateTime OrderDate { get; set; }
    public string CreatedBy { get; set; }

}

and

using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SessionContextSample.Models
{
    public class OrderContext : DbContext
    {
        public OrderContext(IHttpContextAccessor ctxt, DbContextOptions opts) : base(opts)
        {
            
            //Skip this if running outside of an HttpRequest, as when running Migrations at startup, or in background workers
            if (ctxt.HttpContext != null)
            {
                var userName = ctxt.HttpContext.User.Identity.Name ?? "Unkown";

                //Open the DbConnection.  It will remain open for the lifetime of this web request
                this.Database.GetDbConnection().Open();  
                this.Database.ExecuteSqlRaw("EXEC sys.sp_set_session_context @key = N'user', @value = {0};  ",userName);
            }
        }

        public DbSet<Order> Orders { get; set; }
        
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);


        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Order>().Property(o => o.CreatedBy)
                        .HasDefaultValueSql("cast(SESSION_CONTEXT(N'user') as nvarchar(255))")
                        .ValueGeneratedOnAdd()
                        .HasMaxLength(255)
                        .IsRequired();
            base.OnModelCreating(modelBuilder);
        }
    }
}

If you Open() the connection EF will leave it open until the DbContext is Disposed (at the end of your Scope/WebRequest).

David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • @David-Browne-Microsoft Can you please expand on how I would work with the stored procedure? Do I need to call it in the DbContext constructor or everytime I call `SaveChanges()`? – J Weezy Mar 30 '21 at 19:38
  • Thank you for providing this as a code-change alternative. Our DBA has frowned on the idea of granting system SP access to a service account. I will mark this as the answer, though I cannot implement it. – J Weezy Mar 30 '21 at 21:46
  • Look at the permissions for sp_set_session_context: "Any user can set a session context for their session." There's no need to grant anything. And you can always have a table with the session_id (@@spid) as the PK and store your own "session context". – David Browne - Microsoft Mar 30 '21 at 21:49
  • @David-Browne-Microsoft Considering Azure is likely to grow and there will be more database migrations where others will run into this problem, would you be willing to provide a more comprehensive answer with code demos on on how to handle this with dependency injection (we don't open connections in our data layer - we just call the Add, Delete, Update, SaveChanges, etc. methods) and how to capture the SUSER_NAME() in the trigger from the current context? I think this will greatly help Microsoft's clients move to their cloud product. – J Weezy Mar 31 '21 at 13:27
  • It isn't clear to me how I can capture the session context, created from the application's `openConnection` call, \within the database trigger on delete. Note: we create a log in a history table that effectively implements a soft-delete by cloning the record and we need to capture the username that did it. Currently, we are looking at running a table update first in order to capture the username and then a delete so that the trigger can grab the username from the UpdateBy field that was populated from the previous update. This seems like an ugly hack. – J Weezy Mar 31 '21 at 13:34
  • @JWeezy agree this needs a sample. I'll work on something and add it to this answer. – David Browne - Microsoft Mar 31 '21 at 13:37
  • @David-Browne-Microsoft Great, looking forward to it. Your immediate solution will definitely be helpful as we are under a tight deadline and are willing to make the code change. Additionally, does it make sense to raise an issue with Microsoft's Azure SQL team and create a method, or modify an existing method, that will capture the actual user name that is logged in through Managed Identity? This may be an easier long-term solution for other clients to adopt because it would require less code-change. – J Weezy Mar 31 '21 at 14:05
  • 1
    sp_set_session_context _is_ that method from the SQL side, and it was put in specifically to support Row Level Security where the users authenticate directly to the database. – David Browne - Microsoft Mar 31 '21 at 14:09
  • @david-browne-microsoft I am following up to see if you have had time to create a sample, per your comment from 3-31-2021? – J Weezy Apr 05 '21 at 14:43
1

I have not researched the "can you tell me how to solve this the way I am already trying to solve it" method. I know you can keep parity with on prem by using lift & shift into an Azure VM. This gives you no cloud native capabilities of Azure SQL Database, but allows you to keep the same audit method. So, if the goal is move to the cloud with minimal changes to our methods, then lift & shift is the easiest, as you will still be running on the server, non-cloud version. You can then modernize and move to Azure SQL. This is not a horrible option if you have to get the apps into the cloud, with their data source, but don't have time to find all of the audit triggers, etc.

Option #2 is to move to the cloud native version of auditing, which is built into the Azure SQL database and can do DML. This is ideal, in many ways, but your existing code will not work. There are plenty of sites on how to set up auditing of DML and DDL. If you have a lot of audit triggers, this will mean setting up a deploy to the cloud strategy that removes them and switches to Azure cloud native approaches for the database.

You appear to want option #3 - cloud native without changing code? If so, I don't know how. It appears @David Browne - Microsoft has offered a way to set things up through C# code. I would still consider modernizing over time and move to cloud native audit approaches, but this could be a good stop gap (unless your data access code is spread all over the place).

Gregory A Beamer
  • 16,870
  • 3
  • 25
  • 32
  • Do you have any specific documentation or references on option 2? David's solution probably won't work because the DBA is frowning on the idea of granting SP access to a service account. – J Weezy Mar 30 '21 at 21:45
  • Off the cuff, I would start gaining some understanding [here](https://learn.microsoft.com/en-us/azure/azure-sql/database/auditing-overview). The audits can be set up through the API or using scripts, as well, but getting the high level. I have not done this hands on, as I am more management/architecture now, but if it is hard to dig and mine the gold, I can ask one of the hands on guys. :D If you have Ops separated out, you may have to work with Ops to set up the auditing (depends on your role). – Gregory A Beamer Mar 30 '21 at 22:01
  • And I just scrolled down and it has links to scripting and API versions. :D – Gregory A Beamer Mar 30 '21 at 22:02