Skip to content

Commit c98395f

Browse files
authored
Audit add and remove ownership request action (#8764)
Progress on NuGet/Engineering#3994
1 parent 52704bf commit c98395f

14 files changed

Lines changed: 447 additions & 42 deletions

src/NuGetGallery.Core/Auditing/AuditedPackageRegistrationAction.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ public enum AuditedPackageRegistrationAction
99
RemoveOwner,
1010
MarkVerified,
1111
MarkUnverified,
12-
SetRequiredSigner
12+
SetRequiredSigner,
13+
AddOwnershipRequest,
14+
DeleteOwnershipRequest,
1315
}
1416
}

src/NuGetGallery.Core/Auditing/PackageRegistrationAuditRecord.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public class PackageRegistrationAuditRecord : AuditRecord<AuditedPackageRegistra
1414
public string Owner { get; }
1515
public string PreviousRequiredSigner { get; private set; }
1616
public string NewRequiredSigner { get; private set; }
17+
public string RequestingOwner { get; private set; }
18+
public string NewOwner { get; private set; }
1719

1820
public PackageRegistrationAuditRecord(
1921
string id, AuditedPackageRegistration registrationRecord, AuditedPackageRegistrationAction action, string owner)
@@ -55,5 +57,58 @@ public static PackageRegistrationAuditRecord CreateForSetRequiredSigner(
5557

5658
return record;
5759
}
60+
61+
private static PackageRegistrationAuditRecord CreateForOwnerRequest(
62+
PackageRegistration registration,
63+
string requestingOwner,
64+
string newOwner,
65+
AuditedPackageRegistrationAction action)
66+
{
67+
if (registration == null)
68+
{
69+
throw new ArgumentNullException(nameof(registration));
70+
}
71+
72+
if (requestingOwner == null)
73+
{
74+
throw new ArgumentNullException(nameof(requestingOwner));
75+
}
76+
77+
if (newOwner == null)
78+
{
79+
throw new ArgumentNullException(nameof(newOwner));
80+
}
81+
82+
var record = new PackageRegistrationAuditRecord(registration, action, owner: null);
83+
84+
record.RequestingOwner = requestingOwner;
85+
record.NewOwner = newOwner;
86+
87+
return record;
88+
}
89+
90+
public static PackageRegistrationAuditRecord CreateForAddOwnershipRequest(
91+
PackageRegistration registration,
92+
string requestingOwner,
93+
string newOwner)
94+
{
95+
return CreateForOwnerRequest(
96+
registration,
97+
requestingOwner,
98+
newOwner,
99+
AuditedPackageRegistrationAction.AddOwnershipRequest);
100+
}
101+
102+
public static PackageRegistrationAuditRecord CreateForDeleteOwnershipRequest(
103+
PackageRegistration registration,
104+
string requestingOwner,
105+
string newOwner)
106+
{
107+
return CreateForOwnerRequest(
108+
registration,
109+
requestingOwner,
110+
newOwner,
111+
AuditedPackageRegistrationAction.DeleteOwnershipRequest);
112+
}
58113
}
59114
}

src/NuGetGallery.Core/NuGetGallery.Core.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
</ItemGroup>
4444

4545
<ItemGroup>
46+
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory">
47+
<Version>5.2.6</Version>
48+
</PackageReference>
4649
<PackageReference Include="NuGet.Packaging">
4750
<Version>5.9.0</Version>
4851
</PackageReference>

src/NuGetGallery.Services/PackageManagement/IPackageOwnerRequestService.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,23 @@ namespace NuGetGallery
1010
public interface IPackageOwnerRequestService
1111
{
1212
/// <summary>
13-
/// Gets <see cref="PackageOwnerRequest"/>s that match the provided conditions.
13+
/// Gets <see cref="PackageOwnerRequest"/>s that match the provided conditions. User entities on the returned requests are not populated.
1414
/// </summary>
1515
/// <param name="package">If nonnull, only returns <see cref="PackageOwnerRequest"/>s that are for this package.</param>
1616
/// <param name="requestingOwner">If nonnull, only returns <see cref="PackageOwnerRequest"/>s that were requested by this owner.</param>
1717
/// <param name="newOwner">If nonnull, only returns <see cref="PackageOwnerRequest"/>s that are for this user to become an owner.</param>
1818
/// <returns>An <see cref="IEnumerable{PackageOwnerRequest}"/> containing all objects that matched the conditions.</returns>
1919
IEnumerable<PackageOwnerRequest> GetPackageOwnershipRequests(PackageRegistration package = null, User requestingOwner = null, User newOwner = null);
2020

21+
/// <summary>
22+
/// Gets <see cref="PackageOwnerRequest"/>s that match the provided conditions. User entities on the returned requests are populated.
23+
/// </summary>
24+
/// <param name="package">If nonnull, only returns <see cref="PackageOwnerRequest"/>s that are for this package.</param>
25+
/// <param name="requestingOwner">If nonnull, only returns <see cref="PackageOwnerRequest"/>s that were requested by this owner.</param>
26+
/// <param name="newOwner">If nonnull, only returns <see cref="PackageOwnerRequest"/>s that are for this user to become an owner.</param>
27+
/// <returns>An <see cref="IEnumerable{PackageOwnerRequest}"/> containing all objects that matched the conditions.</returns>
28+
IEnumerable<PackageOwnerRequest> GetPackageOwnershipRequestsWithUsers(PackageRegistration package = null, User requestingOwner = null, User newOwner = null);
29+
2130
/// <summary>
2231
/// Checks if the pending owner has a request for this package which matches the specified token.
2332
/// </summary>

src/NuGetGallery.Services/PackageManagement/PackageOwnerRequestService.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,37 @@ public PackageOwnerRequest GetPackageOwnershipRequest(PackageRegistration packag
4545
return request.ConfirmationCode == token ? request : null;
4646
}
4747

48-
public IEnumerable<PackageOwnerRequest> GetPackageOwnershipRequests(PackageRegistration package = null, User requestingOwner = null, User newOwner = null)
48+
public IEnumerable<PackageOwnerRequest> GetPackageOwnershipRequests(
49+
PackageRegistration package = null,
50+
User requestingOwner = null,
51+
User newOwner = null)
52+
{
53+
return GetPackageOwnershipRequests(includeUsers: false, package, requestingOwner, newOwner);
54+
}
55+
56+
public IEnumerable<PackageOwnerRequest> GetPackageOwnershipRequestsWithUsers(
57+
PackageRegistration package = null,
58+
User requestingOwner = null,
59+
User newOwner = null)
60+
{
61+
return GetPackageOwnershipRequests(includeUsers: true, package, requestingOwner, newOwner);
62+
}
63+
64+
private IEnumerable<PackageOwnerRequest> GetPackageOwnershipRequests(
65+
bool includeUsers,
66+
PackageRegistration package,
67+
User requestingOwner,
68+
User newOwner)
4969
{
5070
var query = _packageOwnerRequestRepository.GetAll().Include(e => e.PackageRegistration);
5171

72+
if (includeUsers)
73+
{
74+
query = query
75+
.Include(x => x.RequestingOwner)
76+
.Include(x => x.NewOwner);
77+
}
78+
5279
if (package != null)
5380
{
5481
query = query.Where(r => r.PackageRegistrationKey == package.Key);
@@ -101,18 +128,23 @@ public async Task<PackageOwnerRequest> AddPackageOwnershipRequest(PackageRegistr
101128

102129
_packageOwnerRequestRepository.InsertOnCommit(newRequest);
103130
await _packageOwnerRequestRepository.CommitChangesAsync();
131+
104132
return newRequest;
105133
}
106134

107135
public async Task DeletePackageOwnershipRequest(PackageOwnerRequest request, bool commitChanges = true)
108136
{
137+
if (request == null)
138+
{
139+
throw new ArgumentNullException(nameof(request));
140+
}
141+
109142
_packageOwnerRequestRepository.DeleteOnCommit(request);
110143

111144
if (commitChanges)
112145
{
113146
await _packageOwnerRequestRepository.CommitChangesAsync();
114147
}
115148
}
116-
117149
}
118150
}

src/NuGetGallery.Services/PackageManagement/PackageOwnershipManagementService.cs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,22 @@ private async Task AddPackageOwnerTask(PackageRegistration packageRegistration,
9595

9696
await _packageService.AddPackageOwnerAsync(packageRegistration, user, commitChanges);
9797

98-
await DeletePackageOwnershipRequestAsync(packageRegistration, user, commitChanges);
98+
// Delete any ownership request related to this new owner, since the work is done. Don't audit since the
99+
// "add owner" operation is audited itself. Having a "delete package ownership" audit here would be confusing
100+
// because the intended user action was to add an owner, not to reject (delete) an ownership request.
101+
await DeletePackageOwnershipRequestAsync(packageRegistration, user, commitChanges, saveAudit: false);
99102
}
100103

101104
public async Task<PackageOwnerRequest> AddPackageOwnershipRequestAsync(PackageRegistration packageRegistration, User requestingOwner, User newOwner)
102105
{
103-
return await _packageOwnerRequestService.AddPackageOwnershipRequest(packageRegistration, requestingOwner, newOwner);
106+
var request = await _packageOwnerRequestService.AddPackageOwnershipRequest(packageRegistration, requestingOwner, newOwner);
107+
108+
await _auditingService.SaveAuditRecordAsync(PackageRegistrationAuditRecord.CreateForAddOwnershipRequest(
109+
packageRegistration,
110+
requestingOwner.Username,
111+
newOwner.Username));
112+
113+
return request;
104114
}
105115

106116
public PackageOwnerRequest GetPackageOwnershipRequest(PackageRegistration package, User pendingOwner, string token)
@@ -184,6 +194,11 @@ private async Task RemovePackageOwnerImplAsync(PackageRegistration packageRegist
184194
}
185195

186196
public async Task DeletePackageOwnershipRequestAsync(PackageRegistration packageRegistration, User newOwner, bool commitChanges = true)
197+
{
198+
await DeletePackageOwnershipRequestAsync(packageRegistration, newOwner, commitChanges, saveAudit: true);
199+
}
200+
201+
private async Task DeletePackageOwnershipRequestAsync(PackageRegistration packageRegistration, User newOwner, bool commitChanges, bool saveAudit)
187202
{
188203
if (packageRegistration == null)
189204
{
@@ -195,10 +210,25 @@ public async Task DeletePackageOwnershipRequestAsync(PackageRegistration package
195210
throw new ArgumentNullException(nameof(newOwner));
196211
}
197212

198-
var request = _packageOwnerRequestService.GetPackageOwnershipRequests(package: packageRegistration, newOwner: newOwner).FirstOrDefault();
213+
var request = _packageOwnerRequestService
214+
.GetPackageOwnershipRequestsWithUsers(package: packageRegistration, newOwner: newOwner)
215+
.FirstOrDefault();
199216
if (request != null)
200217
{
218+
// We must capture this audit record prior to the actual deletion operation. Deletion of an entity in
219+
// Entity Framework clears the relationship properties cause us to lose the information needed to create
220+
// the audit record. The package ID and usernames become unavailable.
221+
var auditRecord = PackageRegistrationAuditRecord.CreateForDeleteOwnershipRequest(
222+
request.PackageRegistration,
223+
request.RequestingOwner.Username,
224+
request.NewOwner.Username);
225+
201226
await _packageOwnerRequestService.DeletePackageOwnershipRequest(request, commitChanges);
227+
228+
if (saveAudit)
229+
{
230+
await _auditingService.SaveAuditRecordAsync(auditRecord);
231+
}
202232
}
203233
}
204234

src/NuGetGallery/NuGetGallery.csproj

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2212,9 +2212,6 @@
22122212
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
22132213
<Version>2.2.0</Version>
22142214
</PackageReference>
2215-
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory">
2216-
<Version>5.2.6</Version>
2217-
</PackageReference>
22182215
<PackageReference Include="Microsoft.Net.Http">
22192216
<Version>2.2.29</Version>
22202217
</PackageReference>

tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageRegistrationActionTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ public void Definition_HasNotChanged()
1616
"RemoveOwner",
1717
"MarkVerified",
1818
"MarkUnverified",
19-
"SetRequiredSigner"
19+
"SetRequiredSigner",
20+
"AddOwnershipRequest",
21+
"DeleteOwnershipRequest",
2022
};
2123

2224
Verify(typeof(AuditedPackageRegistrationAction), expectedNames);

tests/NuGetGallery.Core.Facts/Auditing/PackageRegistrationAuditRecordTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@ public void GetPath_ReturnsLowerCasedId()
3838
Assert.Equal("a", actualPath);
3939
}
4040

41+
[Fact]
42+
public void CreateForAddOwnershipRequest_InitializesProperties()
43+
{
44+
var record = PackageRegistrationAuditRecord.CreateForAddOwnershipRequest(
45+
new PackageRegistration { Id = "NuGet.Versioning" },
46+
requestingOwner: "NuGet",
47+
newOwner: "Microsoft");
48+
49+
Assert.Equal("NuGet.Versioning", record.Id);
50+
Assert.Equal("NuGet", record.RequestingOwner);
51+
Assert.Equal("Microsoft", record.NewOwner);
52+
Assert.Equal(AuditedPackageRegistrationAction.AddOwnershipRequest, record.Action);
53+
}
54+
55+
[Fact]
56+
public void CreateForDeleteOwnershipRequest_InitializesProperties()
57+
{
58+
var record = PackageRegistrationAuditRecord.CreateForDeleteOwnershipRequest(
59+
new PackageRegistration { Id = "NuGet.Versioning" },
60+
requestingOwner: "NuGet",
61+
newOwner: "Microsoft");
62+
63+
Assert.Equal("NuGet.Versioning", record.Id);
64+
Assert.Equal("NuGet", record.RequestingOwner);
65+
Assert.Equal("Microsoft", record.NewOwner);
66+
Assert.Equal(AuditedPackageRegistrationAction.DeleteOwnershipRequest, record.Action);
67+
}
68+
4169
[Fact]
4270
public void CreateForSetRequiredSigner_WhenRegistrationIsNull_Throws()
4371
{

tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2659,10 +2659,10 @@ public async Task ReturnsProperResult(
26592659

26602660
public class PackageVerificationKeyContainer : TestContainer
26612661
{
2662-
public static int UserKey = 1234;
2663-
public static string Username = "testuser";
2664-
public static string PackageId = "foo";
2665-
public static string PackageVersion = "1.0.0";
2662+
public const int UserKey = 1234;
2663+
public const string Username = "testuser";
2664+
public const string PackageId = "foo";
2665+
public string PackageVersion = "1.0.0";
26662666

26672667
internal TestableApiController SetupController(string keyType, Scope scope, Package package, bool isOwner = true)
26682668
{

0 commit comments

Comments
 (0)