Skip to content

Commit 53e1f7e

Browse files
RequirePackageMetadataCompliancePolicy: allow exceptions for authors (#6728)
1 parent fe6fd30 commit 53e1f7e

7 files changed

Lines changed: 200 additions & 19 deletions

File tree

src/NuGetGallery/Security/MicrosoftTeamSubscription.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ private static List<UserSecurityPolicy> InitializePoliciesList()
6161
Name,
6262
MicrosoftUsername,
6363
allowedCopyrightNotices: AllowedCopyrightNotices,
64+
allowedAuthors: new[] { MicrosoftUsername },
6465
isLicenseUrlRequired: true,
6566
isProjectUrlRequired: true,
6667
errorMessageFormat: Strings.SecurityPolicy_RequireMicrosoftPackageMetadataComplianceForPush)

src/NuGetGallery/Security/RequirePackageMetadataCompliancePolicy.cs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public static UserSecurityPolicy CreatePolicy(
9595
string subscription,
9696
string requiredCoOwnerUsername,
9797
string[] allowedCopyrightNotices,
98+
string[] allowedAuthors,
9899
bool isLicenseUrlRequired,
99100
bool isProjectUrlRequired,
100101
string errorMessageFormat)
@@ -103,6 +104,7 @@ public static UserSecurityPolicy CreatePolicy(
103104
{
104105
RequiredCoOwnerUsername = requiredCoOwnerUsername,
105106
AllowedCopyrightNotices = allowedCopyrightNotices,
107+
AllowedAuthors = allowedAuthors,
106108
IsLicenseUrlRequired = isLicenseUrlRequired,
107109
IsProjectUrlRequired = isProjectUrlRequired,
108110
ErrorMessageFormat = errorMessageFormat
@@ -116,13 +118,7 @@ private bool IsPackageMetadataCompliant(Package package, State state, out IList<
116118
complianceFailures = new List<string>();
117119

118120
// Author validation
119-
if (!package.FlattenedAuthors
120-
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
121-
.Select(s => s.Trim())
122-
.Contains(state.RequiredCoOwnerUsername, StringComparer.InvariantCultureIgnoreCase))
123-
{
124-
complianceFailures.Add(string.Format(CultureInfo.CurrentCulture, Strings.SecurityPolicy_RequiredAuthorMissing, state.RequiredCoOwnerUsername));
125-
}
121+
ValidatePackageAuthors(package, state, complianceFailures);
126122

127123
// Copyright validation
128124
if (!state.AllowedCopyrightNotices.Contains(package.Copyright))
@@ -145,6 +141,47 @@ private bool IsPackageMetadataCompliant(Package package, State state, out IList<
145141
return !complianceFailures.Any();
146142
}
147143

144+
private static void ValidatePackageAuthors(Package package, State state, IList<string> complianceFailures)
145+
{
146+
var packageAuthors = package.FlattenedAuthors
147+
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
148+
.Select(s => s.Trim())
149+
.ToList();
150+
151+
// Check for duplicate entries
152+
var duplicateAuthors = packageAuthors
153+
.GroupBy(x => x)
154+
.Where(group => group.Count() > 1)
155+
.Select(group => group.Key)
156+
.ToList();
157+
158+
if (duplicateAuthors.Any())
159+
{
160+
complianceFailures.Add(string.Format(CultureInfo.CurrentCulture, Strings.SecurityPolicy_PackageAuthorDuplicatesNotAllowed, string.Join(",", duplicateAuthors)));
161+
}
162+
else
163+
{
164+
if (state.AllowedAuthors?.Length > 0)
165+
{
166+
foreach (var packageAuthor in packageAuthors)
167+
{
168+
if (!state.AllowedAuthors.Contains(packageAuthor))
169+
{
170+
complianceFailures.Add(string.Format(CultureInfo.CurrentCulture, Strings.SecurityPolicy_PackageAuthorNotAllowed, packageAuthor));
171+
}
172+
}
173+
}
174+
else
175+
{
176+
// No list of allowed authors is defined for this policy.
177+
// We require the required co-owner to be defined as the only package author.
178+
if (packageAuthors.Count() > 1 || packageAuthors.Single() != state.RequiredCoOwnerUsername)
179+
{
180+
complianceFailures.Add(string.Format(CultureInfo.CurrentCulture, Strings.SecurityPolicy_RequiredAuthorMissing, state.RequiredCoOwnerUsername));
181+
}
182+
}
183+
}
184+
}
148185

149186
/// <summary>
150187
/// Retrieve the policy state.
@@ -167,6 +204,9 @@ public class State
167204
[JsonProperty("copy")]
168205
public string[] AllowedCopyrightNotices { get; set; }
169206

207+
[JsonProperty("authors")]
208+
public string[] AllowedAuthors { get; set; }
209+
170210
[JsonProperty("licUrlReq")]
171211
public bool IsLicenseUrlRequired { get; set; }
172212

src/NuGetGallery/Strings.Designer.cs

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/NuGetGallery/Strings.resx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,4 +1077,12 @@ The {1} Team</value>
10771077
<data name="UploadPackage_MalformedLicenseUrl" xml:space="preserve">
10781078
<value>The package contains a malformed license URL.</value>
10791079
</data>
1080+
<data name="SecurityPolicy_PackageAuthorNotAllowed" xml:space="preserve">
1081+
<value>The package metadata defines '{0}' as one of the authors which is not allowed by policy.</value>
1082+
<comment>{0} is the author that is not in the list of allowed authors for this policy.</comment>
1083+
</data>
1084+
<data name="SecurityPolicy_PackageAuthorDuplicatesNotAllowed" xml:space="preserve">
1085+
<value>The package metadata defines '{0}' as author more than once, which is not allowed by policy.</value>
1086+
<comment>{0} is the list of duplicate package authors</comment>
1087+
</data>
10801088
</root>

tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1391,7 +1391,7 @@ public async Task AddsRequiredCoOwnerWhenPackageWithNewRegistrationIdIsCompliant
13911391
_packageId,
13921392
"1.0.0",
13931393
isSigned: true,
1394-
authors: $"{_user.Username},{_requiredCoOwner.Username}",
1394+
authors: $"{_requiredCoOwner.Username}",
13951395
licenseUrl: new Uri("https://github.com/NuGet/NuGetGallery/blob/master/LICENSE.txt"),
13961396
projectUrl: new Uri("https://www.nuget.org"));
13971397

tests/NuGetGallery.Facts/Security/MicrosoftTeamSubscriptionFacts.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public void Policies_ReturnsMicrosoftTeamSubscriptionPolicies()
4040
"\"© корпорация Майкрософт. Все права защищены.\"," +
4141
"\"© Microsoft Corporation。 著作權所有,並保留一切權利。\"" +
4242
"]," +
43+
"\"authors\":[\"Microsoft\"]," +
4344
"\"licUrlReq\":true," +
4445
"\"projUrlReq\":true," +
4546
"\"error\":\"The package is not compliant with metadata requirements for Microsoft packages on NuGet.org. Go to https://aka.ms/Microsoft-NuGet-Compliance for more information.\\r\\nPolicy violations: {0}\"}",

tests/NuGetGallery.Facts/Security/RequirePackageMetadataCompliancePolicyFacts.cs

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ public void Evaluate_ThrowsForNullArgument()
4444
Assert.ThrowsAsync<ArgumentNullException>(() => policyHandler.EvaluateAsync(null));
4545
}
4646

47-
4847
[Fact]
4948
public async Task Evaluate_DoesNotCommitChangesToEntityContext()
5049
{
@@ -71,13 +70,13 @@ public async Task Evaluate_DoesNotCommitChangesToEntityContext()
7170
var telemetryService = new Mock<ITelemetryService>().Object;
7271

7372
var context = new PackageSecurityPolicyEvaluationContext(
74-
userService.Object,
75-
packageOwnershipManagementService.Object,
73+
userService.Object,
74+
packageOwnershipManagementService.Object,
7675
telemetryService,
77-
subscription.Policies,
76+
subscription.Policies,
7877
newMicrosoftCompliantPackage,
7978
sourceAccount: nugetUser,
80-
targetAccount: nugetUser,
79+
targetAccount: nugetUser,
8180
httpContext: It.IsAny<HttpContextBase>());
8281

8382
// Act
@@ -97,9 +96,9 @@ public async Task Evaluate_SilentlySucceedsWhenRequiredCoOwnerDoesNotExist()
9796
var policyHandler = new RequirePackageMetadataCompliancePolicy();
9897
var fakes = new Fakes();
9998
var context = CreateTestContext(
100-
false,
101-
subscription.Policies,
102-
fakes.NewPackageVersion,
99+
false,
100+
subscription.Policies,
101+
fakes.NewPackageVersion,
103102
packageRegistrationAlreadyExists: false,
104103
sourceAccount: nugetUser,
105104
targetAccount: nugetUser);
@@ -206,6 +205,120 @@ public async Task Evaluate_NonCompliantPackage_CreatesErrorResult(Package nonCom
206205
Assert.False(newPackageRegistration.IsVerified);
207206
}
208207

208+
[Fact]
209+
public async Task Evaluate_NonCompliantPackageAuthor_CreatesErrorResult()
210+
{
211+
// Arrange
212+
var nugetUser = new User("NuGet");
213+
var newPackageRegistration = new PackageRegistration { Id = "NewPackageId", Owners = new List<User> { nugetUser } };
214+
var packageAuthors = new[] { MicrosoftTeamSubscription.MicrosoftUsername, "The Not-Allowed Package Authors" };
215+
var nonCompliantPackage = Fakes.CreateCompliantPackage("1.0.0", newPackageRegistration, packageAuthors);
216+
217+
var policy = RequirePackageMetadataCompliancePolicy.CreatePolicy(
218+
MicrosoftTeamSubscription.Name,
219+
MicrosoftTeamSubscription.MicrosoftUsername,
220+
allowedCopyrightNotices: MicrosoftTeamSubscription.AllowedCopyrightNotices,
221+
allowedAuthors: new[] { MicrosoftTeamSubscription.MicrosoftUsername },
222+
isLicenseUrlRequired: true,
223+
isProjectUrlRequired: true,
224+
errorMessageFormat: Strings.SecurityPolicy_RequireMicrosoftPackageMetadataComplianceForPush);
225+
226+
var policyHandler = new RequirePackageMetadataCompliancePolicy();
227+
228+
var context = CreateTestContext(
229+
true,
230+
new[] { policy },
231+
nonCompliantPackage,
232+
packageRegistrationAlreadyExists: false,
233+
sourceAccount: nugetUser,
234+
targetAccount: nugetUser);
235+
236+
// Act
237+
var result = await policyHandler.EvaluateAsync(context);
238+
239+
// Assert
240+
Assert.False(result.Success);
241+
Assert.Null(newPackageRegistration.Owners.SingleOrDefault(u => u.Username == MicrosoftTeamSubscription.MicrosoftUsername));
242+
Assert.False(newPackageRegistration.IsVerified);
243+
}
244+
245+
[Fact]
246+
public async Task Evaluate_DuplicatePackageAuthor_CreatesErrorResult()
247+
{
248+
// Arrange
249+
var nugetUser = new User("NuGet");
250+
var newPackageRegistration = new PackageRegistration { Id = "NewPackageId", Owners = new List<User> { nugetUser } };
251+
var packageAuthors = new[] { MicrosoftTeamSubscription.MicrosoftUsername, MicrosoftTeamSubscription.MicrosoftUsername };
252+
var nonCompliantPackage = Fakes.CreateCompliantPackage("1.0.0", newPackageRegistration, packageAuthors);
253+
254+
var policy = RequirePackageMetadataCompliancePolicy.CreatePolicy(
255+
MicrosoftTeamSubscription.Name,
256+
MicrosoftTeamSubscription.MicrosoftUsername,
257+
allowedCopyrightNotices: MicrosoftTeamSubscription.AllowedCopyrightNotices,
258+
allowedAuthors: new[] { MicrosoftTeamSubscription.MicrosoftUsername },
259+
isLicenseUrlRequired: true,
260+
isProjectUrlRequired: true,
261+
errorMessageFormat: Strings.SecurityPolicy_RequireMicrosoftPackageMetadataComplianceForPush);
262+
263+
var policyHandler = new RequirePackageMetadataCompliancePolicy();
264+
265+
var context = CreateTestContext(
266+
true,
267+
new[] { policy },
268+
nonCompliantPackage,
269+
packageRegistrationAlreadyExists: false,
270+
sourceAccount: nugetUser,
271+
targetAccount: nugetUser);
272+
273+
// Act
274+
var result = await policyHandler.EvaluateAsync(context);
275+
276+
// Assert
277+
Assert.False(result.Success);
278+
Assert.Null(newPackageRegistration.Owners.SingleOrDefault(u => u.Username == MicrosoftTeamSubscription.MicrosoftUsername));
279+
Assert.False(newPackageRegistration.IsVerified);
280+
}
281+
282+
[Fact]
283+
public async Task Evaluate_CompliantPackageAuthors_CreatesSuccessResult()
284+
{
285+
// Arrange
286+
var nugetUser = new User("NuGet");
287+
var newPackageRegistration = new PackageRegistration { Id = "NewPackageId", Owners = new List<User> { nugetUser } };
288+
var packageAuthors = new[] { MicrosoftTeamSubscription.MicrosoftUsername, "The Most-Awesome Package Authors" };
289+
var compliantPackage = Fakes.CreateCompliantPackage("1.0.0", newPackageRegistration, packageAuthors);
290+
291+
var policy = RequirePackageMetadataCompliancePolicy.CreatePolicy(
292+
MicrosoftTeamSubscription.Name,
293+
MicrosoftTeamSubscription.MicrosoftUsername,
294+
allowedCopyrightNotices: MicrosoftTeamSubscription.AllowedCopyrightNotices,
295+
allowedAuthors: packageAuthors,
296+
isLicenseUrlRequired: true,
297+
isProjectUrlRequired: true,
298+
errorMessageFormat: Strings.SecurityPolicy_RequireMicrosoftPackageMetadataComplianceForPush);
299+
300+
var policyHandler = new RequirePackageMetadataCompliancePolicy();
301+
302+
var packageOwnershipManagementService = new Mock<IPackageOwnershipManagementService>();
303+
packageOwnershipManagementService.Setup(m => m.AddPackageOwnerAsync(newPackageRegistration, It.IsAny<User>(), false)).Returns(Task.CompletedTask);
304+
305+
var context = CreateTestContext(
306+
true,
307+
new[] { policy },
308+
compliantPackage,
309+
packageRegistrationAlreadyExists: false,
310+
sourceAccount: nugetUser,
311+
targetAccount: nugetUser,
312+
packageOwnershipManagementService: packageOwnershipManagementService.Object);
313+
314+
// Act
315+
var result = await policyHandler.EvaluateAsync(context);
316+
317+
// Assert
318+
Assert.True(result.Success);
319+
packageOwnershipManagementService.Verify(s => s.AddPackageOwnerAsync(newPackageRegistration, Fakes.RequiredCoOwner, false), Times.Once);
320+
}
321+
209322
private static PackageSecurityPolicyEvaluationContext CreateTestContext(
210323
bool microsoftUserExists,
211324
IEnumerable<UserSecurityPolicy> policies,
@@ -298,7 +411,7 @@ public Fakes(
298411
};
299412
}
300413

301-
public static Package CreateCompliantPackage(string version, PackageRegistration packageRegistration)
414+
public static Package CreateCompliantPackage(string version, PackageRegistration packageRegistration, string[] allowedAuthors = null)
302415
{
303416
return new Package
304417
{
@@ -307,7 +420,7 @@ public static Package CreateCompliantPackage(string version, PackageRegistration
307420
Copyright = "(c) Microsoft Corporation. All rights reserved.",
308421
ProjectUrl = "https://github.com/NuGet/NuGetGallery",
309422
LicenseUrl = "https://github.com/NuGet/NuGetGallery/blob/master/LICENSE.txt",
310-
FlattenedAuthors = "NuGet, Microsoft"
423+
FlattenedAuthors = allowedAuthors == null ? "Microsoft" : string.Join(",", allowedAuthors)
311424
};
312425
}
313426

@@ -345,7 +458,7 @@ public static IReadOnlyCollection<Package> CreateNonCompliantPackages()
345458
public User Owner { get; }
346459

347460
public Package NewPackageVersion { get; }
348-
461+
349462
public PackageRegistration ExistingPackageRegistration { get; }
350463

351464
public static User RequiredCoOwner { get; }

0 commit comments

Comments
 (0)