Skip to content

Commit 29b7a79

Browse files
authored
Copilot Audit Log Fixes + User Filtering Performance (#57)
2 parents 50dbe77 + b000a30 commit 29b7a79

File tree

16 files changed

+301
-100
lines changed

16 files changed

+301
-100
lines changed

src/AnalyticsEngine/Common/DataUtils/Sql/Inserts/InsertBatchTypeFieldCache.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ public class InsertBatchTypeFieldCache<T>
1212
{
1313
private List<InsertBatchPropertyMapping> _fieldInfoPropertyInfoCache = null;
1414

15-
static List<Type> _validTempColumnTypes = new List<Type>() { typeof(string), typeof(DateTime), typeof(int), typeof(float), typeof(double), typeof(bool), typeof(Guid), typeof(int?),
16-
typeof(int?), typeof(double?) };
15+
static List<Type> _validTempColumnTypes = new List<Type>()
16+
{
17+
typeof(string), typeof(DateTime), typeof(DateTime?), typeof(int), typeof(float), typeof(double), typeof(bool), typeof(Guid), typeof(int?),
18+
typeof(int?), typeof(double?)
19+
};
1720

1821
public List<InsertBatchPropertyMapping> PropertyMappingInfo
1922
{
@@ -64,6 +67,10 @@ public List<InsertBatchPropertyMapping> PropertyMappingInfo
6467
{
6568
return ("datetime2", false);
6669
}
70+
else if (propertyType == typeof(DateTime?))
71+
{
72+
return ("datetime2", true);
73+
}
6774
else if (propertyType == typeof(int))
6875
{
6976
return ("int", false);

src/AnalyticsEngine/Common/Entities/ImportTaskSettings.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,14 @@ private void Parse(PropertyInfo propertyInfo, string token)
5050

5151
[ImportProp]
5252
public bool Calls { get; set; } = true;
53+
54+
5355
[ImportProp]
5456
public bool GraphUsersMetadata { get; set; } = true;
5557

58+
/// <summary>
59+
/// User Teams apps for user refresh
60+
/// </summary>
5661
[ImportProp]
5762
public bool GraphUserApps { get; set; } = true;
5863

src/AnalyticsEngine/Tests.UnitTests/CopilotTests.cs

Lines changed: 115 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -25,126 +25,165 @@ public CopilotTests()
2525
_config = new TestsAppConfig();
2626
}
2727

28-
// https://learn.microsoft.com/en-us/office/office-365-management-api/copilot-schema
29-
[TestMethod]
30-
public async Task CopilotEventManagerSaveTest()
28+
// Shared flow for saving Copilot events (normal + no permissions adaptor)
29+
private async Task ExecuteCopilotEventManagerSaveFlow(ICopilotMetadataLoader adaptor, AnalyticsEntitiesContext db)
3130
{
32-
using (var db = new AnalyticsEntitiesContext())
31+
32+
var copilotEventManager = new CopilotAuditEventManager(_config.ConnectionStrings.DatabaseConnectionString, adaptor, _logger);
33+
34+
var commonEventDocEdit = new Office365Event
3335
{
34-
// Clear events for test
35-
db.CopilotEventMetadataFiles.RemoveRange(db.CopilotEventMetadataFiles);
36-
db.CopilotEventMetadataMeetings.RemoveRange(db.CopilotEventMetadataMeetings);
37-
await db.SaveChangesAsync();
36+
TimeStamp = DateTime.Now,
37+
Operation = new EventOperation { Name = "Document Edit" + DateTime.Now.Ticks },
38+
User = new User { AzureAdId = "test", UserPrincipalName = "test doc user " + DateTime.Now.Ticks },
39+
Id = Guid.NewGuid()
40+
};
41+
var commonEventChat = new Office365Event
42+
{
43+
TimeStamp = DateTime.Now,
44+
Operation = new EventOperation { Name = "Chat or something" + DateTime.Now.Ticks },
45+
User = new User { AzureAdId = "test", UserPrincipalName = "test chat user " + DateTime.Now.Ticks },
46+
Id = Guid.NewGuid()
47+
};
48+
var commonEventMeeting = new Office365Event
49+
{
50+
TimeStamp = DateTime.Now,
51+
Operation = new EventOperation { Name = "Meeting Op" + DateTime.Now.Ticks },
52+
User = new User { AzureAdId = "test", UserPrincipalName = "test meeting user " + DateTime.Now.Ticks },
53+
Id = Guid.NewGuid()
54+
};
55+
var commonOutlook = new Office365Event
56+
{
57+
TimeStamp = DateTime.Now,
58+
Operation = new EventOperation { Name = "Outlook Op" + DateTime.Now.Ticks },
59+
User = new User { AzureAdId = "test", UserPrincipalName = "test outlook user " + DateTime.Now.Ticks },
60+
Id = Guid.NewGuid()
61+
};
3862

39-
var copilotEventAdaptor = new CopilotAuditEventManager(_config.ConnectionStrings.DatabaseConnectionString, new FakeCopilotEventAdaptor(), _logger);
40-
41-
var commonEventDocEdit = new Office365Event
42-
{
43-
TimeStamp = DateTime.Now,
44-
Operation = new EventOperation { Name = "Document Edit" + DateTime.Now.Ticks },
45-
User = new User { AzureAdId = "test", UserPrincipalName = "test doc user " + DateTime.Now.Ticks },
46-
Id = Guid.NewGuid()
47-
};
48-
var commonEventChat = new Office365Event
49-
{
50-
TimeStamp = DateTime.Now,
51-
Operation = new EventOperation { Name = "Chat or something" + DateTime.Now.Ticks },
52-
User = new User { AzureAdId = "test", UserPrincipalName = "test chat user " + DateTime.Now.Ticks },
53-
Id = Guid.NewGuid()
54-
};
55-
var commonEventMeeting = new Office365Event
56-
{
57-
TimeStamp = DateTime.Now,
58-
Operation = new EventOperation { Name = "Meeting Op" + DateTime.Now.Ticks },
59-
User = new User { AzureAdId = "test", UserPrincipalName = "test meeting user " + DateTime.Now.Ticks },
60-
Id = Guid.NewGuid()
61-
};
62-
var commonOutlook = new Office365Event
63-
{
64-
TimeStamp = DateTime.Now,
65-
Operation = new EventOperation { Name = "Outlook Op" + DateTime.Now.Ticks },
66-
User = new User { AzureAdId = "test", UserPrincipalName = "test outlook user " + DateTime.Now.Ticks },
67-
Id = Guid.NewGuid()
68-
};
69-
70-
// Audit metadata for our tests
71-
var meeting = new CopilotEventData
72-
{
73-
AppHost = "test",
74-
Contexts = new List<Context>
63+
// Audit metadata for tests
64+
var meeting = new CopilotEventData
65+
{
66+
AppHost = "test",
67+
Contexts = new List<Context>
7568
{
7669
new Context
7770
{
78-
Id = "https://microsoft.teams.com/threads/19:meeting_NDQ4MGRhYjgtMzc5MS00ZWMxLWJiZjEtOTIxZmM5Mzg3ZGFi@thread.v2", // Needs to be real
71+
Id = "https://microsoft.teams.com/threads/19:meeting_NDQ4MGRhYjgtMzc5MS00ZWMxLWJiZjEtOTIxZmM5Mzg3ZGFi@thread.v2", // Needs to be real
7972
Type = ActivityImportConstants.COPILOT_CONTEXT_TYPE_TEAMS_MEETING
8073
}
8174
}
82-
};
83-
var docEvent = new CopilotEventData
84-
{
85-
AppHost = "Word",
86-
Contexts = new List<Context>
75+
};
76+
var docEvent = new CopilotEventData
77+
{
78+
AppHost = "Word",
79+
Contexts = new List<Context>
8780
{
8881
new Context
8982
{
9083
Id = _config.TestCopilotDocContextIdSpSite,
9184
Type = _config.TeamSiteFileExtension
9285
}
9386
}
94-
};
95-
var teamsChat = new CopilotEventData
96-
{
97-
AppHost = "Teams",
98-
Contexts = new List<Context>
87+
};
88+
var teamsChat = new CopilotEventData
89+
{
90+
AppHost = "Teams",
91+
Contexts = new List<Context>
9992
{
10093
new Context
10194
{
10295
Id = "https://microsoft.teams.com/threads/19:somechatthread@thread.v2",
10396
Type = ActivityImportConstants.COPILOT_CONTEXT_TYPE_TEAMS_CHAT
10497
}
10598
}
106-
};
107-
108-
var outlook = new CopilotEventData
109-
{
110-
AppHost = "Outlook",
111-
AccessedResources = new List<AccessedResource>
99+
};
100+
var outlook = new CopilotEventData
101+
{
102+
AppHost = "Outlook",
103+
AccessedResources = new List<AccessedResource>
112104
{
113105
new AccessedResource{ Type = "http://schema.skype.com/HyperLink" }
114106
},
115-
};
107+
};
108+
109+
// Persist common events for FK usage
110+
db.AuditEventsCommon.Add(commonEventDocEdit);
111+
db.AuditEventsCommon.Add(commonEventMeeting);
112+
db.AuditEventsCommon.Add(commonEventChat);
113+
db.AuditEventsCommon.Add(commonOutlook);
114+
await db.SaveChangesAsync();
115+
116+
// Save Copilot events
117+
await copilotEventManager.SaveSingleCopilotEventToSql(meeting, commonEventMeeting);
118+
await copilotEventManager.SaveSingleCopilotEventToSql(docEvent, commonEventDocEdit);
119+
await copilotEventManager.SaveSingleCopilotEventToSql(teamsChat, commonEventChat);
120+
await copilotEventManager.SaveSingleCopilotEventToSql(outlook, commonOutlook);
121+
await copilotEventManager.CommitAllChanges();
116122

123+
}
124+
125+
[TestMethod]
126+
public async Task CopilotEventManagerSaveTest()
127+
{
128+
129+
using (var db = new AnalyticsEntitiesContext())
130+
{
131+
db.CopilotEventMetadataFiles.RemoveRange(db.CopilotEventMetadataFiles);
132+
db.CopilotEventMetadataMeetings.RemoveRange(db.CopilotEventMetadataMeetings);
133+
await db.SaveChangesAsync();
117134

118-
// Check counts before and after
135+
// Counts before
119136
var fileEventsPreCount = await db.CopilotEventMetadataFiles.CountAsync();
120137
var meetingEventsPreCount = await db.CopilotEventMetadataMeetings.CountAsync();
121138
var allCopilotEventsPreCount = await db.CopilotChats.CountAsync();
122139

123-
// Save common events as they are required for the foreign key - the common event is saved before CopilotAuditEventManager runs on the metadata
124-
db.AuditEventsCommon.Add(commonEventDocEdit);
125-
db.AuditEventsCommon.Add(commonEventMeeting);
126-
db.AuditEventsCommon.Add(commonEventChat);
127-
db.AuditEventsCommon.Add(commonOutlook);
128-
await db.SaveChangesAsync();
140+
await ExecuteCopilotEventManagerSaveFlow(new FakeCopilotEventAdaptor(), db);
129141

130-
// Save events
131-
await copilotEventAdaptor.SaveSingleCopilotEventToSql(meeting, commonEventMeeting);
132-
await copilotEventAdaptor.SaveSingleCopilotEventToSql(docEvent, commonEventDocEdit);
133-
await copilotEventAdaptor.SaveSingleCopilotEventToSql(teamsChat, commonEventChat);
134-
await copilotEventAdaptor.SaveSingleCopilotEventToSql(outlook, commonOutlook);
135-
await copilotEventAdaptor.CommitAllChanges();
136142

137-
// Verify counts have increased
143+
// Counts after
138144
var fileEventsPostCount = await db.CopilotEventMetadataFiles.CountAsync();
139145
var meetingEventsPostCount = await db.CopilotEventMetadataMeetings.CountAsync();
140146
var allCopilotEventsPostCount = await db.CopilotChats.CountAsync();
141147

148+
// Assertions
142149
Assert.IsTrue(fileEventsPostCount == fileEventsPreCount + 1);
143150
Assert.IsTrue(meetingEventsPostCount == meetingEventsPreCount + 1);
144151
Assert.IsTrue(allCopilotEventsPostCount == allCopilotEventsPreCount + 4); // 4 new events - 1 meeting, 1 file, 1 chat, 1 outlook
145152
}
146153
}
147154

155+
/// <summary>
156+
/// When there's no permissions to read files/meetings, we should still save the chat at least
157+
/// </summary>
158+
[TestMethod]
159+
public async Task CopilotEventManagerWithNoPermissionsSaveTest()
160+
{
161+
162+
using (var db = new AnalyticsEntitiesContext())
163+
{
164+
db.CopilotEventMetadataFiles.RemoveRange(db.CopilotEventMetadataFiles);
165+
db.CopilotEventMetadataMeetings.RemoveRange(db.CopilotEventMetadataMeetings);
166+
await db.SaveChangesAsync();
167+
168+
// Counts before
169+
var fileEventsPreCount = await db.CopilotEventMetadataFiles.CountAsync();
170+
var meetingEventsPreCount = await db.CopilotEventMetadataMeetings.CountAsync();
171+
var allCopilotEventsPreCount = await db.CopilotChats.CountAsync();
172+
173+
await ExecuteCopilotEventManagerSaveFlow(new ReturnNullFilesAndMeetingsAdaptor(), db);
174+
175+
// Counts after
176+
var fileEventsPostCount = await db.CopilotEventMetadataFiles.CountAsync();
177+
var meetingEventsPostCount = await db.CopilotEventMetadataMeetings.CountAsync();
178+
var allCopilotEventsPostCount = await db.CopilotChats.CountAsync();
179+
180+
// Assertions
181+
Assert.IsTrue(fileEventsPostCount == fileEventsPreCount); // No file data so no new file event
182+
Assert.IsTrue(meetingEventsPostCount == meetingEventsPreCount); // No meeting data so no new meeting event
183+
Assert.IsTrue(allCopilotEventsPostCount == allCopilotEventsPreCount + 4); // 4 new events - 1 meeting, 1 file, 1 chat, 1 outlook
184+
}
185+
}
186+
148187
/// <summary>
149188
/// Tests we can load metadata from Graph
150189
/// </summary>

src/AnalyticsEngine/Tests.UnitTests/DataUtilsTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,10 @@ class TestMultiPropTypeTempEntity
387387

388388
[Column("date")]
389389
public DateTime Timestamp { get; set; } = DateTime.Now;
390+
391+
392+
[Column("date_nullable")]
393+
public DateTime? NullableTimestamp { get; set; } = null;
390394
}
391395

392396
[TempTableName(TEMP_TABLE_NAME)]

src/AnalyticsEngine/Tests.UnitTests/FakeLoaderClasses/FakeCopilotEventAdaptor.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,20 @@ public Task<string> GetUserIdFromUpn(string userPrincipalName)
3535
return Task.FromResult("testId");
3636
}
3737
}
38+
39+
public class ReturnNullFilesAndMeetingsAdaptor : ICopilotMetadataLoader
40+
{
41+
public Task<MeetingMetadata> GetMeetingInfo(string meetingId, string userGuid)
42+
{
43+
return Task.FromResult<MeetingMetadata>(null);
44+
}
45+
public Task<SpoDocumentFileInfo> GetSpoFileInfo(string copilotId, string eventUpn)
46+
{
47+
return Task.FromResult<SpoDocumentFileInfo>(null);
48+
}
49+
public Task<string> GetUserIdFromUpn(string userPrincipalName)
50+
{
51+
return Task.FromResult("testId");
52+
}
53+
}
3854
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
3+
namespace WebJob.Office365ActivityImporter.Engine.ActivityAPI
4+
{
5+
/// <summary>
6+
/// Global trace configuration for capturing raw audit log JSON bodies that contain a specific email address.
7+
/// Populated by Program argument parsing in the WebJob project.
8+
/// </summary>
9+
public static class AuditTraceConfig
10+
{
11+
/// <summary>
12+
/// Email address to search for within individual audit log JSON items.
13+
/// </summary>
14+
public static string TraceEmail { get; set; } = null;
15+
/// <summary>
16+
/// Directory to write matching JSON items to. Must exist / be creatable.
17+
/// </summary>
18+
public static string TraceDirectory { get; set; } = null;
19+
}
20+
}

0 commit comments

Comments
 (0)