Skip to content

Commit 62a7254

Browse files
authored
Merge pull request #8416 from NuGet/master
Merge branch 'master' into dev
2 parents b53e196 + 2571315 commit 62a7254

20 files changed

Lines changed: 481 additions & 29 deletions

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# the repo. Unless a later match takes precedence,
33
# review when someone opens a pull request.
44
# For more on how to customize the CODEOWNERS file - https://help.github.com/en/articles/about-code-owners
5-
* @agr @cristinamanum @dannyjdev @drewgillies @joelverhagen @loic-sharma @lyndaidaii @ryuyu @skofman1 @shishirx34 @xavierdecoster @zhhyu
5+
* @agr @dannyjdev @drewgillies @joelverhagen @loic-sharma @lyndaidaii @ryuyu @skofman1 @shishirx34 @xavierdecoster @zhhyu
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Autofac;
10+
using Microsoft.Extensions.CommandLineUtils;
11+
using NuGet.Packaging;
12+
using NuGet.Packaging.Core;
13+
using NuGet.Services.Entities;
14+
using NuGet.Versioning;
15+
using NuGetGallery;
16+
17+
namespace GalleryTools.Commands
18+
{
19+
public static class ReserveNamespacesCommand
20+
{
21+
private const int DefaultSleepSeconds = 0;
22+
private const string PathOption = "--path";
23+
24+
public static void Configure(CommandLineApplication config)
25+
{
26+
config.Description = "Bulk reserve namespaces";
27+
config.HelpOption("-? | -h | --help");
28+
29+
var pathOptions = config.Option(
30+
$"-p | {PathOption}",
31+
"A path to a simple text file, which is the list of namespaces to reserve. One namespace per line. Prefix namespaces should end with '*', otherwise they will be exact match reservations.",
32+
CommandOptionType.SingleValue);
33+
34+
var sleepDurationOption = config.Option(
35+
"-s | --sleep",
36+
$"The duration in seconds to sleep between each reservation (default: {DefaultSleepSeconds}).",
37+
CommandOptionType.SingleValue);
38+
39+
var unreserveOption = config.Option(
40+
"-u | --unreserve",
41+
$"Unreserve namespaces, instead of reserving them. Meant for a rollback of bulk reserving. Note that you must clear the progress file to reuse the same input file.",
42+
CommandOptionType.NoValue);
43+
44+
config.OnExecute(() =>
45+
{
46+
return ExecuteAsync(pathOptions, sleepDurationOption, unreserveOption).GetAwaiter().GetResult();
47+
});
48+
}
49+
50+
private static async Task<int> ExecuteAsync(
51+
CommandOption pathOption,
52+
CommandOption sleepDurationOption,
53+
CommandOption unreserveOption)
54+
{
55+
if (!pathOption.HasValue())
56+
{
57+
Console.WriteLine($"The '{PathOption}' parameter is required.");
58+
return 1;
59+
}
60+
61+
var sleepDuration = TimeSpan.FromSeconds(DefaultSleepSeconds);
62+
if (sleepDurationOption.HasValue())
63+
{
64+
sleepDuration = TimeSpan.FromSeconds(int.Parse(sleepDurationOption.Value()));
65+
66+
if (sleepDuration < TimeSpan.Zero)
67+
{
68+
Console.WriteLine("The sleep duration must be zero or more seconds.");
69+
return 1;
70+
}
71+
}
72+
73+
var unreserve = unreserveOption.HasValue();
74+
75+
var path = pathOption.Value();
76+
var completedPath = path + ".progress";
77+
var remainingList = GetRemainingList(path, completedPath);
78+
Console.WriteLine($"{remainingList.Count} reserved namespace(s) to {(unreserve ? "remove" : "add")}.");
79+
if (!remainingList.Any())
80+
{
81+
Console.WriteLine("No namespaces were found to reserve.");
82+
return 1;
83+
}
84+
85+
var builder = new ContainerBuilder();
86+
builder.RegisterType<ReservedNamespaceService>().AsSelf();
87+
builder.RegisterAssemblyModules(typeof(DefaultDependenciesModule).Assembly);
88+
var container = builder.Build();
89+
var service = container.Resolve<ReservedNamespaceService>();
90+
91+
var totalCounter = 0;
92+
foreach (var reservedNamespace in remainingList)
93+
{
94+
Console.Write($"{(unreserve ? "Removing" : "Adding")} '{reservedNamespace.Value}' IsPrefix = {reservedNamespace.IsPrefix}...");
95+
try
96+
{
97+
var matching = service
98+
.FindReservedNamespacesForPrefixList(new[] { reservedNamespace.Value })
99+
.SingleOrDefault(x => ReservedNamespaceComparer.Instance.Equals(x, reservedNamespace));
100+
101+
if (unreserve)
102+
{
103+
if (matching == null)
104+
{
105+
Console.WriteLine(" does not exist.");
106+
AppendReservedNamespace(completedPath, reservedNamespace);
107+
continue;
108+
}
109+
110+
await service.DeleteReservedNamespaceAsync(reservedNamespace.Value);
111+
}
112+
else
113+
{
114+
if (matching != null)
115+
{
116+
Console.WriteLine(" already exists.");
117+
AppendReservedNamespace(completedPath, reservedNamespace);
118+
continue;
119+
}
120+
121+
await service.AddReservedNamespaceAsync(reservedNamespace);
122+
}
123+
124+
AppendReservedNamespace(completedPath, reservedNamespace);
125+
totalCounter++;
126+
Console.WriteLine(" done.");
127+
}
128+
catch (Exception e)
129+
{
130+
Console.WriteLine(" error!");
131+
Console.WriteLine(e);
132+
}
133+
134+
if (sleepDuration > TimeSpan.Zero)
135+
{
136+
Console.WriteLine($"Sleeping for {sleepDuration}.");
137+
await Task.Delay(sleepDuration);
138+
}
139+
}
140+
141+
Console.WriteLine($"All done. {(unreserve ? "Removed" : "Added")} {totalCounter} reserved namespace(s).");
142+
143+
return 0;
144+
}
145+
146+
private static List<ReservedNamespace> GetRemainingList(string path, string completedPath)
147+
{
148+
Console.WriteLine($"Reading reserved namespaces from {path}...");
149+
var all = ReadReservedNamespaces(path);
150+
151+
var completed = new List<ReservedNamespace>();
152+
if (File.Exists(completedPath))
153+
{
154+
Console.WriteLine($"Reading completed reserved namespaces from {completedPath}...");
155+
completed.AddRange(ReadReservedNamespaces(completedPath));
156+
}
157+
158+
var remaining = all.Except(completed, ReservedNamespaceComparer.Instance).ToList();
159+
if (remaining.Count != all.Count)
160+
{
161+
Console.WriteLine($"{all.Count - remaining.Count} reserved namespaces(s) are already done.");
162+
}
163+
164+
return remaining;
165+
}
166+
167+
private static void AppendReservedNamespace(string completedListPath, ReservedNamespace reservedNamespace)
168+
{
169+
using (var fileStream = new FileStream(completedListPath, FileMode.Append, FileAccess.Write))
170+
using (var writer = new StreamWriter(fileStream))
171+
{
172+
writer.WriteLine($"{reservedNamespace.Value}{(reservedNamespace.IsPrefix ? "*" : string.Empty)}");
173+
}
174+
}
175+
176+
private static List<ReservedNamespace> ReadReservedNamespaces(string path)
177+
{
178+
var uniqueReservedNamespaces = new HashSet<ReservedNamespace>(new ReservedNamespaceComparer());
179+
var output = new List<ReservedNamespace>();
180+
int lineNumber = 0;
181+
string line;
182+
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
183+
using (var reader = new StreamReader(fileStream))
184+
{
185+
while ((line = reader.ReadLine()) != null)
186+
{
187+
lineNumber++;
188+
189+
if (string.IsNullOrWhiteSpace(line))
190+
{
191+
continue;
192+
}
193+
194+
var value = line.Trim();
195+
var isPrefix = false;
196+
if (line.EndsWith("*"))
197+
{
198+
value = value.Substring(0, value.Length - 1);
199+
isPrefix = true;
200+
}
201+
202+
// Ensure the reserved namespace is actually a valid package ID.
203+
var validatedPrefix = value;
204+
if (isPrefix)
205+
{
206+
// Prefix reserved namespaces can end with '-' and '.'. Package IDs cannot.
207+
if (value.EndsWith("-") || value.EndsWith("."))
208+
{
209+
validatedPrefix = validatedPrefix.Substring(0, validatedPrefix.Length - 1);
210+
}
211+
}
212+
213+
if (!PackageIdValidator.IsValidPackageId(validatedPrefix))
214+
{
215+
Console.WriteLine($"Line {lineNumber}: Ignoring invalid reserved namespace (validated: '{validatedPrefix}', original: '{line}').");
216+
continue;
217+
}
218+
219+
var reservedNamespace = new ReservedNamespace
220+
{
221+
Value = value,
222+
IsPrefix = isPrefix,
223+
IsSharedNamespace = false,
224+
};
225+
226+
if (!uniqueReservedNamespaces.Add(reservedNamespace))
227+
{
228+
Console.WriteLine($"Line {lineNumber}: Ignoring duplicate reserved namespace.");
229+
continue;
230+
}
231+
232+
output.Add(reservedNamespace);
233+
}
234+
}
235+
236+
return output;
237+
}
238+
239+
private class ReservedNamespaceComparer : IEqualityComparer<ReservedNamespace>
240+
{
241+
public static ReservedNamespaceComparer Instance { get; } = new ReservedNamespaceComparer();
242+
243+
public bool Equals(ReservedNamespace x, ReservedNamespace y)
244+
{
245+
if (ReferenceEquals(x, y))
246+
{
247+
return true;
248+
}
249+
250+
if (x is null || y is null)
251+
{
252+
return false;
253+
}
254+
255+
return StringComparer.OrdinalIgnoreCase.Equals(x.Value, y.Value)
256+
&& x.IsPrefix == y.IsPrefix;
257+
}
258+
259+
public int GetHashCode(ReservedNamespace obj)
260+
{
261+
return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Value) ^ obj.IsPrefix.GetHashCode();
262+
}
263+
}
264+
}
265+
}

src/GalleryTools/GalleryTools.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<Compile Include="Commands\BackfillRepositoryMetadataCommand.cs" />
5050
<Compile Include="Commands\HashCommand.cs" />
5151
<Compile Include="Commands\ApplyTenantPolicyCommand.cs" />
52+
<Compile Include="Commands\ReserveNamespacesCommand.cs" />
5253
<Compile Include="Commands\ReflowCommand.cs" />
5354
<Compile Include="Commands\UpdateIsLatestCommand.cs" />
5455
<Compile Include="Commands\VerifyApiKeyCommand.cs" />

src/GalleryTools/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public static int Main(params string[] args)
2121
commandLineApplication.Command("filldevdeps", BackfillDevelopmentDependencyCommand.Configure);
2222
commandLineApplication.Command("verifyapikey", VerifyApiKeyCommand.Configure);
2323
commandLineApplication.Command("updateIsLatest", UpdateIsLatestCommand.Configure);
24+
commandLineApplication.Command("reservenamespaces", ReserveNamespacesCommand.Configure);
2425

2526
try
2627
{

src/GitHubVulnerabilities2Db/Gallery/ThrowingTelemetryService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,11 @@ public void TrackPackagePushNamespaceConflictEvent(string packageId, string pack
256256
throw new NotImplementedException();
257257
}
258258

259+
public void TrackPackagePushOwnerlessNamespaceConflictEvent(string packageId, string packageVersion, User user, IIdentity identity)
260+
{
261+
throw new NotImplementedException();
262+
}
263+
259264
public void TrackPackageReadMeChangeEvent(Package package, string readMeSourceType, PackageEditReadMeState readMeState)
260265
{
261266
throw new NotImplementedException();

src/NuGetGallery.Services/Permissions/ActionRequiringReservedNamespacePermissions.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,21 @@ protected override PermissionsCheckResult CheckPermissionsForEntity(User account
5757
return PermissionsCheckResult.Allowed;
5858
}
5959

60+
var hasAnyOwners = reservedNamespaces.Any(rn => rn.Owners.Any());
61+
6062
// Permissions on only a single namespace are required to perform the action.
61-
return reservedNamespaces.Any(rn => PermissionsHelpers.IsRequirementSatisfied(ReservedNamespacePermissionsRequirement, account, rn)) ?
62-
PermissionsCheckResult.Allowed : PermissionsCheckResult.ReservedNamespaceFailure;
63+
if (reservedNamespaces.Any(rn => PermissionsHelpers.IsRequirementSatisfied(ReservedNamespacePermissionsRequirement, account, rn)))
64+
{
65+
return PermissionsCheckResult.Allowed;
66+
}
67+
else if (hasAnyOwners)
68+
{
69+
return PermissionsCheckResult.ReservedNamespaceFailure;
70+
}
71+
else
72+
{
73+
return PermissionsCheckResult.OwnerlessReservedNamespaceFailure;
74+
}
6375
}
6476

6577
public PermissionsCheckResult CheckPermissionsOnBehalfOfAnyAccount(User currentUser, ActionOnNewPackageContext newPackageContext)

src/NuGetGallery.Services/Permissions/PermissionsCheckResult.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ public enum PermissionsCheckResult
3131
/// <summary>
3232
/// The current user does not have permissions to perform the action on the <see cref="ReservedNamespace"/> on behalf of another <see cref="User"/>.
3333
/// </summary>
34-
ReservedNamespaceFailure
34+
ReservedNamespaceFailure,
35+
36+
/// <summary>
37+
/// The current user does not have permissions to perform the action on the <see cref="ReservedNamespace"/> on behalf of another <see cref="User"/>
38+
/// but none of the namespaces currently have owners.
39+
/// </summary>
40+
OwnerlessReservedNamespaceFailure,
3541
}
3642
}

src/NuGetGallery.Services/Telemetry/ITelemetryService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ void TrackPackageDeprecate(
6161

6262
void TrackPackagePushNamespaceConflictEvent(string packageId, string packageVersion, User user, IIdentity identity);
6363

64+
void TrackPackagePushOwnerlessNamespaceConflictEvent(string packageId, string packageVersion, User user, IIdentity identity);
65+
6466
void TrackVerifyPackageKeyEvent(string packageId, string packageVersion, User user, IIdentity identity, int statusCode);
6567

6668
void TrackNewUserRegistrationEvent(User user, Credential identity);

src/NuGetGallery.Services/Telemetry/TelemetryService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class Events
2828
public const string VerifyPackageKey = "VerifyPackageKey";
2929
public const string PackageReadMeChanged = "PackageReadMeChanged";
3030
public const string PackagePushNamespaceConflict = "PackagePushNamespaceConflict";
31+
public const string PackagePushOwnerlessNamespaceConflict = "PackagePushOwnerlessNamespaceConflict";
3132
public const string NewUserRegistration = "NewUserRegistration";
3233
public const string CredentialAdded = "CredentialAdded";
3334
public const string CredentialUsed = "CredentialUsed";
@@ -364,6 +365,11 @@ public void TrackPackagePushNamespaceConflictEvent(string packageId, string pack
364365
TrackMetricForPackage(Events.PackagePushNamespaceConflict, packageId, packageVersion, user, identity);
365366
}
366367

368+
public void TrackPackagePushOwnerlessNamespaceConflictEvent(string packageId, string packageVersion, User user, IIdentity identity)
369+
{
370+
TrackMetricForPackage(Events.PackagePushOwnerlessNamespaceConflict, packageId, packageVersion, user, identity);
371+
}
372+
367373
public void TrackCreatePackageVerificationKeyEvent(string packageId, string packageVersion, User user, IIdentity identity)
368374
{
369375
TrackMetricForPackage(Events.CreatePackageVerificationKey, packageId, packageVersion, user, identity);

src/NuGetGallery/Controllers/ApiController.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,11 @@ private HttpStatusCodeWithBodyResult GetHttpResultFromFailedApiScopeEvaluationHe
11951195
TelemetryService.TrackPackagePushNamespaceConflictEvent(id, versionString, GetCurrentUser(), User.Identity);
11961196
return new HttpStatusCodeWithBodyResult(HttpStatusCode.Conflict, Strings.UploadPackage_IdNamespaceConflict);
11971197
}
1198+
else if (result.PermissionsCheckResult == PermissionsCheckResult.OwnerlessReservedNamespaceFailure)
1199+
{
1200+
TelemetryService.TrackPackagePushOwnerlessNamespaceConflictEvent(id, versionString, GetCurrentUser(), User.Identity);
1201+
return new HttpStatusCodeWithBodyResult(HttpStatusCode.Conflict, Strings.UploadPackage_OwnerlessIdNamespaceConflict);
1202+
}
11981203

11991204
var message = result.PermissionsCheckResult == PermissionsCheckResult.Allowed && !result.IsOwnerConfirmed ?
12001205
Strings.ApiKeyOwnerUnconfirmed : Strings.ApiKeyNotAuthorized;

0 commit comments

Comments
 (0)