thepirat000 / Audit.NET

An extensible framework to audit executing operations in .NET and .NET Core.
MIT License
2.26k stars 324 forks source link

Audit.NET doesn't work with windows authentication #685

Closed vivek-outamation closed 3 weeks ago

vivek-outamation commented 1 month ago

Summary:

Details:

Issue Description:

In our .NET Core Web API project, we are using Windows Authentication to secure our API endpoints. The Audit.EntityFramework.Core package is integrated to audit changes made through Entity Framework Core. We are encountering the following issues:

Auditing Failure with Protected Endpoints: When accessing protected endpoints (those requiring Windows Authentication), audit records are not being generated or saved. This issue occurs despite the fact that auditing works correctly for endpoints marked with [AllowAnonymous].

Behavior Summary: Anonymous Endpoints: Audit logs are successfully created and saved. Protected Endpoints: No audit records are created, and no data is inserted into the audit log.

Error Messages and Logs: There are no specific error messages or exceptions being thrown related to the auditing process in the logs. The absence of audit records suggests a failure in the auditing mechanism when Windows Authentication is applied.

thepirat000 commented 1 month ago

Can you share a minimal project that reproduces the issue? There is no relation between the Audit.EntityFramework.Core and the Web API authentication mechanism, so I can't tell what the problem could be.

How are you configuring the Audit and the EF dbContext?

vivek-outamation commented 1 month ago

I have inherited AuditDbContext in MyDbContext

public class OutamateWorksCoreDbContext : AuditDbContext
{
    // My Entites added in DbSet
}

I have created my CustomAuditDataProvider by inheriting AuditDataProvider

public class CustomAuditDataProvider(IServiceScopeFactory _serviceScopeFactory) : AuditDataProvider
{
    public override async Task<object> InsertEvent(AuditEvent auditEvent)
    {
        //throw new Exception(JsonSerializer.Serialize(auditEvent));
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<OutamateWorksCoreDbContext>();
            var efEvent = auditEvent.GetEntityFrameworkEvent();
            foreach (var entry in efEvent.Entries)
            {
                string modifiedBy = null;
                if (entry.ColumnValues["ModifiedById"] != null)
                {
                    modifiedBy = await context.Users.Where(u => u.Id == int.Parse(Convert.ToString(entry.ColumnValues["ModifiedById"]))).Select(u => u.Name).FirstOrDefaultAsync();
                }

                var auditLog = new AuditLog
                {
                    TableName = entry.Table,
                    Schema = entry.Schema,
                    Changes = entry.Changes.ToJson(),
                    Action = entry.Action,
                    ConnectionId = efEvent.ConnectionId,
                    ContextId = efEvent.ContextId,
                    PrimaryKey = int.Parse(entry.PrimaryKey["Id"].ToString()),
                    Success = efEvent.Success,
                    ColumnValues = entry.ColumnValues.ToJson(),
                    ModifiedBy = modifiedBy,
                };
                context.AuditLogs.Add(auditLog);
            }

            context.SaveChanges();
        }
        return null;
    }
}

I have registered this CustomAuditDataProvider service in Program.cs

builder.Services.AddScoped<CustomAuditDataProvider>();
thepirat000 commented 1 month ago

You must override InsertEvent AND InsertEventAsync in your custom data provider. Otherwise the SaveChangesAsyc() calls won't trigger the audit

thepirat000 commented 1 month ago

I apologize for the confusion. The base AuditDataProvider.InsertEventAsync() actually invokes the synchronous method by default. However, it is highly recommended to implement the asynchronous methods in your custom data provider to avoid calling asynchronous methods from synchronous ones.

For more information, you can refer to this article: https://learn.microsoft.com/en-us/archive/msdn-magazine/2015/july/async-programming-brownfield-async-development

While I'm not certain this will resolve the issue, it's worth a try.

thepirat000 commented 1 month ago

I understand the issue now.

The Audit.EntityFramework.Core library does not use Dependency Injection (DI) to resolve the AuditDataProvider. Instead, it retrieves it from the AuditDbContext or the global configuration, which by default, uses a FileDataProvider that logs audits as .json files in the running application's directory.

One solution is to set the AuditDataProvider property in your DbContext constructor:

public class OutamateWorksCoreDbContext : AuditDbContext
{
    public OutamateWorksCoreDbContext(DbContextOptions options, IServiceScopeFactory serviceScopeFactory) : base(options)
    {
        this.AuditDataProvider = new CustomAuditDataProvider(serviceScopeFactory);
    }
}

Alternatively, you can set the global default data provider like this:

// in your startup code:
var app = builder.Build();

var serviceScopeFactory = app.Services.GetService<IServiceScopeFactory>();

Audit.Core.Configuration.Setup().Use(new CustomAuditDataProvider(serviceScopeFactory));

Moreover, you might not need the IServiceScopeFactory dependency in your DataProvider if you implement it this way:

using Microsoft.EntityFrameworkCore.Infrastructure;

public class CustomAuditDataProvider : AuditDataProvider
{
    public override object InsertEvent(AuditEvent auditEvent)
    {
        var serviceScopeFactory = auditEvent.GetEntityFrameworkEvent().GetDbContext().GetService<IServiceScopeFactory>();

        using (var scope = serviceScopeFactory.CreateScope())

        // ...

Also, be careful with the InsertEvent / InsertEventAsync implementation. I see you are implementing the sync InsertEvent as an async method returning Task which is wrong.

You should implement object InsertEvent(AuditEvent auditEvent) and Task<object> InsertEventAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default)