From f5ab58347a640ae7e3eedde13fcd36e186d5f3d7 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Mon, 23 Oct 2017 11:59:42 -0700 Subject: [PATCH 01/16] Documentation parser and clamp fixes (#4853) --- src/NuGetGallery/App_Start/AppActivator.cs | 3 +- .../Controllers/PackagesController.cs | 2 +- src/NuGetGallery/Scripts/gallery/clamp.js | 271 ++++++++++++++++++ .../Scripts/gallery/page-display-package.js | 20 +- src/NuGetGallery/Services/IReadMeService.cs | 3 +- src/NuGetGallery/Services/ReadMeService.cs | 86 ++++-- .../ViewModels/DisplayPackageViewModel.cs | 1 - .../Views/Packages/DisplayPackage.cshtml | 20 +- .../Controllers/PackagesControllerFacts.cs | 17 +- .../Services/ReadMeServiceFacts.cs | 4 +- 10 files changed, 379 insertions(+), 48 deletions(-) create mode 100644 src/NuGetGallery/Scripts/gallery/clamp.js diff --git a/src/NuGetGallery/App_Start/AppActivator.cs b/src/NuGetGallery/App_Start/AppActivator.cs index 7295b273cb..9ebde45957 100644 --- a/src/NuGetGallery/App_Start/AppActivator.cs +++ b/src/NuGetGallery/App_Start/AppActivator.cs @@ -206,7 +206,8 @@ private static void BundlingPostStart() BundleTable.Bundles.Add(homeScriptBundle); var displayPackageScriptBundle = new ScriptBundle("~/Scripts/gallery/page-display-package.min.js") - .Include("~/Scripts/gallery/page-display-package.js"); + .Include("~/Scripts/gallery/page-display-package.js") + .Include("~/Scripts/gallery/clamp.js"); BundleTable.Bundles.Add(displayPackageScriptBundle); var managePackagesScriptBundle = new ScriptBundle("~/Scripts/gallery/page-manage-packages.min.js") diff --git a/src/NuGetGallery/Controllers/PackagesController.cs b/src/NuGetGallery/Controllers/PackagesController.cs index ca011f4819..f111897b7b 100644 --- a/src/NuGetGallery/Controllers/PackagesController.cs +++ b/src/NuGetGallery/Controllers/PackagesController.cs @@ -481,7 +481,7 @@ public virtual async Task DisplayPackage(string id, string version } } - await _readMeService.GetReadMeHtmlAsync(package, model, isReadMePending); + model.ReadMeHtml = await _readMeService.GetReadMeHtmlAsync(package, isReadMePending); model.PolicyMessage = GetDisplayPackagePolicyMessage(package.PackageRegistration); diff --git a/src/NuGetGallery/Scripts/gallery/clamp.js b/src/NuGetGallery/Scripts/gallery/clamp.js new file mode 100644 index 0000000000..a2c9814070 --- /dev/null +++ b/src/NuGetGallery/Scripts/gallery/clamp.js @@ -0,0 +1,271 @@ +/*! + * Clamp.js 0.7.0 + * + * Copyright 2011-2013, Joseph Schmitt http://joe.sh + * Released under the WTFPL license + * http://sam.zoy.org/wtfpl/ + */ + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define([], factory); + } else if (typeof exports === 'object') { + // Node, CommonJS-like + module.exports = factory(); + } else { + // Browser globals + root.$clamp = factory(); + } +}(this, function() { + /** + * Clamps a text node. + * @param {HTMLElement} element. Element containing the text node to clamp. + * @param {Object} options. Options to pass to the clamper. + */ + function clamp(element, options) { + options = options || {}; + + var self = this, + win = window, + opt = { + clamp: options.clamp || 2, + useNativeClamp: typeof(options.useNativeClamp) != 'undefined' ? options.useNativeClamp : true, + splitOnChars: options.splitOnChars || ['.', '-', '–', '—', ' '], //Split on sentences (periods), hypens, en-dashes, em-dashes, and words (spaces). + animate: options.animate || false, + truncationChar: options.truncationChar || '…', + truncationHTML: options.truncationHTML + }, + + sty = element.style, + originalText = element.innerHTML, + + supportsNativeClamp = typeof(element.style.webkitLineClamp) != 'undefined', + clampValue = opt.clamp, + isCSSValue = clampValue.indexOf && (clampValue.indexOf('px') > -1 || clampValue.indexOf('em') > -1), + truncationHTMLContainer; + + if (opt.truncationHTML) { + truncationHTMLContainer = document.createElement('span'); + truncationHTMLContainer.innerHTML = opt.truncationHTML; + } + + + // UTILITY FUNCTIONS __________________________________________________________ + + /** + * Return the current style for an element. + * @param {HTMLElement} elem The element to compute. + * @param {string} prop The style property. + * @returns {number} + */ + function computeStyle(elem, prop) { + if (!win.getComputedStyle) { + win.getComputedStyle = function(el, pseudo) { + this.el = el; + this.getPropertyValue = function(prop) { + var re = /(\-([a-z]){1})/g; + if (prop == 'float') prop = 'styleFloat'; + if (re.test(prop)) { + prop = prop.replace(re, function() { + return arguments[2].toUpperCase(); + }); + } + return el.currentStyle && el.currentStyle[prop] ? el.currentStyle[prop] : null; + }; + return this; + }; + } + + return win.getComputedStyle(elem, null).getPropertyValue(prop); + } + + /** + * Returns the maximum number of lines of text that should be rendered based + * on the current height of the element and the line-height of the text. + */ + function getMaxLines(height) { + var availHeight = height || element.clientHeight, + lineHeight = getLineHeight(element); + + return Math.max(Math.floor(availHeight / lineHeight), 0); + } + + /** + * Returns the maximum height a given element should have based on the line- + * height of the text and the given clamp value. + */ + function getMaxHeight(clmp) { + var lineHeight = getLineHeight(element); + return lineHeight * clmp; + } + + /** + * Returns the line-height of an element as an integer. + */ + function getLineHeight(elem) { + var lh = computeStyle(elem, 'line-height'); + if (lh == 'normal') { + // Normal line heights vary from browser to browser. The spec recommends + // a value between 1.0 and 1.2 of the font size. Using 1.1 to split the diff. + lh = parseInt(computeStyle(elem, 'font-size')) * 1.2; + } + return parseInt(lh); + } + + + // MEAT AND POTATOES (MMMM, POTATOES...) ______________________________________ + var splitOnChars = opt.splitOnChars.slice(0), + splitChar = splitOnChars[0], + chunks, + lastChunk; + + /** + * Gets an element's last child. That may be another node or a node's contents. + */ + function getLastChild(elem) { + //Current element has children, need to go deeper and get last child as a text node + if (elem.lastChild.children && elem.lastChild.children.length > 0) { + return getLastChild(Array.prototype.slice.call(elem.children).pop()); + } + //This is the absolute last child, a text node, but something's wrong with it. Remove it and keep trying + else if (!elem.lastChild || !elem.lastChild.nodeValue || elem.lastChild.nodeValue === '' || elem.lastChild.nodeValue == opt.truncationChar) { + elem.lastChild.parentNode.removeChild(elem.lastChild); + return getLastChild(element); + } + //This is the last child we want, return it + else { + return elem.lastChild; + } + } + + /** + * Removes one character at a time from the text until its width or + * height is beneath the passed-in max param. + */ + function truncate(target, maxHeight) { + if (!maxHeight) { + return; + } + + /** + * Resets global variables. + */ + function reset() { + splitOnChars = opt.splitOnChars.slice(0); + splitChar = splitOnChars[0]; + chunks = null; + lastChunk = null; + } + + var nodeValue = target.nodeValue.replace(opt.truncationChar, ''); + + //Grab the next chunks + if (!chunks) { + //If there are more characters to try, grab the next one + if (splitOnChars.length > 0) { + splitChar = splitOnChars.shift(); + } + //No characters to chunk by. Go character-by-character + else { + splitChar = ''; + } + + chunks = nodeValue.split(splitChar); + } + + //If there are chunks left to remove, remove the last one and see if + // the nodeValue fits. + if (chunks.length > 1) { + // console.log('chunks', chunks); + lastChunk = chunks.pop(); + // console.log('lastChunk', lastChunk); + applyEllipsis(target, chunks.join(splitChar)); + } + //No more chunks can be removed using this character + else { + chunks = null; + } + + //Insert the custom HTML before the truncation character + if (truncationHTMLContainer) { + target.nodeValue = target.nodeValue.replace(opt.truncationChar, ''); + element.innerHTML = target.nodeValue + ' ' + truncationHTMLContainer.innerHTML + opt.truncationChar; + } + + //Search produced valid chunks + if (chunks) { + //It fits + if (element.clientHeight <= maxHeight) { + //There's still more characters to try splitting on, not quite done yet + if (splitOnChars.length >= 0 && splitChar !== '') { + applyEllipsis(target, chunks.join(splitChar) + splitChar + lastChunk); + chunks = null; + } + //Finished! + else { + return element.innerHTML; + } + } + } + //No valid chunks produced + else { + //No valid chunks even when splitting by letter, time to move + //on to the next node + if (splitChar === '') { + applyEllipsis(target, ''); + target = getLastChild(element); + + reset(); + } + } + + //If you get here it means still too big, let's keep truncating + if (opt.animate) { + setTimeout(function() { + truncate(target, maxHeight); + }, opt.animate === true ? 10 : opt.animate); + } else { + return truncate(target, maxHeight); + } + } + + function applyEllipsis(elem, str) { + elem.nodeValue = str + opt.truncationChar; + } + + + // CONSTRUCTOR ________________________________________________________________ + + if (clampValue == 'auto') { + clampValue = getMaxLines(); + } else if (isCSSValue) { + clampValue = getMaxLines(parseInt(clampValue)); + } + + var clampedText; + if (supportsNativeClamp && opt.useNativeClamp) { + sty.overflow = 'hidden'; + sty.textOverflow = 'ellipsis'; + sty.webkitBoxOrient = 'vertical'; + sty.display = '-webkit-box'; + sty.webkitLineClamp = clampValue; + + if (isCSSValue) { + sty.height = opt.clamp + 'px'; + } + } else { + var height = getMaxHeight(clampValue); + if (height <= element.clientHeight) { + clampedText = truncate(getLastChild(element), height); + } + } + + return { + 'original': originalText, + 'clamped': clampedText + }; + } + + return clamp; +})); diff --git a/src/NuGetGallery/Scripts/gallery/page-display-package.js b/src/NuGetGallery/Scripts/gallery/page-display-package.js index 3d227c3068..5a9f4f17a6 100644 --- a/src/NuGetGallery/Scripts/gallery/page-display-package.js +++ b/src/NuGetGallery/Scripts/gallery/page-display-package.js @@ -18,15 +18,27 @@ $(function () { var readmeContainer = $("#readme-container"); if (readmeContainer[0]) { - window.nuget.configureExpanderHeading( - "readme-container"); + window.nuget.configureExpanderHeading("readme-container"); window.nuget.configureExpander( - "readme-full", + "readme-more", "CalculatorAddition", "Show less", "CalculatorSubtract", "Show more"); + + var showLess = $("#readme-less"); + $clamp(showLess[0], { clamp: 10, useNativeClamp: false }); + + $("#show-readme-more").click(function () { + showLess.collapse("toggle"); + }); + showLess.on('hide.bs.collapse', function (e) { + e.stopPropagation(); + }); + showLess.on('show.bs.collapse', function (e) { + e.stopPropagation(); + }); } window.nuget.configureExpanderHeading("dependency-groups"); @@ -36,7 +48,7 @@ $(function () { "CalculatorAddition", "Show less", "CalculatorSubtract", - "Show more"); + "Show more"); for (var i in packageManagers) { diff --git a/src/NuGetGallery/Services/IReadMeService.cs b/src/NuGetGallery/Services/IReadMeService.cs index f264729ae1..50d652c5ee 100644 --- a/src/NuGetGallery/Services/IReadMeService.cs +++ b/src/NuGetGallery/Services/IReadMeService.cs @@ -26,10 +26,9 @@ public interface IReadMeService /// Get the converted HTML from the stored ReadMe markdown. /// /// Package entity associated with the ReadMe. - /// Display package view model to populate. /// Whether to retrieve the pending ReadMe. /// Pending or active ReadMe converted to HTML. - Task GetReadMeHtmlAsync(Package package, DisplayPackageViewModel model, bool isPending = false); + Task GetReadMeHtmlAsync(Package package, bool isPending = false); /// /// Get package ReadMe markdown from storage. diff --git a/src/NuGetGallery/Services/ReadMeService.cs b/src/NuGetGallery/Services/ReadMeService.cs index 97a1750627..4222fcb074 100644 --- a/src/NuGetGallery/Services/ReadMeService.cs +++ b/src/NuGetGallery/Services/ReadMeService.cs @@ -7,22 +7,27 @@ using System.Linq; using System.Net.Http; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using CommonMark; -using System.Text.RegularExpressions; +using CommonMark.Syntax; namespace NuGetGallery { internal class ReadMeService : IReadMeService { + private static readonly TimeSpan RegexTimeout = TimeSpan.FromMinutes(1); + private static readonly Regex EncodedBlockQuotePattern = new Regex("^ {0,3}>", RegexOptions.Multiline, RegexTimeout); + private static readonly Regex CommonMarkLinkPattern = new Regex(" GetReadMeHtmlAsync(ReadMeRequest readMeRequest, Encodi /// Get the converted HTML from the stored ReadMe markdown. /// /// Package entity associated with the ReadMe. - /// Display package view model to populate. /// Whether to retrieve the pending ReadMe. /// Pending or active ReadMe converted to HTML. - public async Task GetReadMeHtmlAsync(Package package, DisplayPackageViewModel model, bool isPending = false) + public async Task GetReadMeHtmlAsync(Package package, bool isPending = false) { var readMeMd = await GetReadMeMdAsync(package, isPending); - if (!string.IsNullOrWhiteSpace(readMeMd)) - { - var readMeMdClamped = string.Join(Environment.NewLine, - readMeMd.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Take(ReadMeClampedLineCount)); - - model.ReadMeHtml = GetReadMeHtml(readMeMd).Trim(); - model.ReadMeHtmlClamped = GetReadMeHtml(readMeMdClamped).Trim(); - } + return string.IsNullOrEmpty(readMeMd) ? + string.Empty : + GetReadMeHtml(readMeMd); } /// @@ -149,10 +148,65 @@ public async Task SavePendingReadMeMdIfChanged(Package package, EditPackag /// HTML content. internal static string GetReadMeHtml(string readMeMd) { - var encodedMarkdown = HttpUtility.HtmlEncode(readMeMd); - var regex = new Regex(" "); + + var settings = CommonMarkSettings.Default.Clone(); + settings.RenderSoftLineBreaksAsLineBreaks = true; + + // Parse executes CommonMarkConverter's ProcessStage1 and ProcessStage2. + var document = CommonMarkConverter.Parse(encodedMarkdown, settings); + foreach (var node in document.AsEnumerable()) + { + if (node.IsOpening) + { + var block = node.Block; + if (block != null) + { + switch (block.Tag) + { + // Demote heading tags so they don't overpower expander headings. + case BlockTag.AtxHeading: + case BlockTag.SetextHeading: + var level = (byte)Math.Min(block.Heading.Level + 1, 6); + block.Heading = new HeadingData(level); + break; + + // Decode preformatted blocks to prevent double encoding. + // Skip BlockTag.BlockQuote, which are partially decoded upfront. + case BlockTag.FencedCode: + case BlockTag.IndentedCode: + if (block.StringContent != null) + { + var content = block.StringContent.TakeFromStart(block.StringContent.Length); + var unencodedContent = HttpUtility.HtmlDecode(content); + block.StringContent.Replace(unencodedContent, 0, unencodedContent.Length); + } + break; + } + } + + var inline = node.Inline; + if (inline != null && inline.Tag == InlineTag.Link) + { + // Allow only http or https links in markdown. + Uri targetUri; + if (! (Uri.TryCreate(inline.TargetUrl, UriKind.Absolute, out targetUri) + && (targetUri.Scheme == Uri.UriSchemeHttp || targetUri.Scheme == Uri.UriSchemeHttps) )) + { + inline.TargetUrl = string.Empty; + } + } + } + } + + // CommonMark.Net does not support link attributes, so manually inject nofollow. + using (var htmlWriter = new StringWriter()) + { + CommonMarkConverter.ProcessStage3(document, htmlWriter, settings); + + return CommonMarkLinkPattern.Replace(htmlWriter.ToString(), "$0" + " rel=\"nofollow\"").Trim(); + } } /// diff --git a/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs b/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs index 05d408dec0..f5979d2c82 100644 --- a/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs +++ b/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs @@ -71,7 +71,6 @@ public void SetPendingMetadata(PackageEdit pendingMetadata) public IEnumerable PackageVersions { get; set; } public string Copyright { get; set; } public string ReadMeHtml { get; set; } - public string ReadMeHtmlClamped { get; set; } public bool HasPendingMetadata { get; private set; } public bool IsLastEditFailed { get; private set; } public DateTime? LastEdited { get; set; } diff --git a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml index 17d3f88ed5..abbfe2b7cf 100644 --- a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml @@ -305,21 +305,19 @@
-
- @Html.Raw(Model.ReadMeHtmlClamped) +
+ @Html.Raw(Model.ReadMeHtml)
-
} diff --git a/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs index bd4f74a717..6b8c1275f8 100644 --- a/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs @@ -567,7 +567,7 @@ public async Task GivenAValidPackageWithNoVersionThatTheCurrentUserDoesNotOwnItD Assert.Equal("Foo", model.Id); Assert.Equal("1.1.1", model.Version); Assert.Equal("A test package!", model.Title); - Assert.Null(model.ReadMeHtml); + Assert.Equal("", model.ReadMeHtml); } [Fact] @@ -581,8 +581,7 @@ public async Task WhenHasReadMeAndMarkdownExists_ReturnsContent() // Assert var model = ResultAssert.IsView(result); - Assert.Equal("

Hello World!

", model.ReadMeHtml); - Assert.Equal("

Hello World!

", model.ReadMeHtmlClamped); + Assert.Equal("

Hello World!

", model.ReadMeHtml); } [Fact] @@ -598,33 +597,29 @@ public async Task WhenHasReadMeAndLongMarkdownExists_ReturnsClampedContent() var model = ResultAssert.IsView(result); var htmlCount = model.ReadMeHtml.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Length; - var htmlClampedCount = model.ReadMeHtmlClamped.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Length; Assert.Equal(20, htmlCount); - Assert.Equal(10, htmlClampedCount); } [Fact] - public async Task WhenHasReadMeAndFileNotFound_ReturnsNull() + public async Task WhenHasReadMeAndFileNotFound_ReturnsEmptyString() { // Arrange & Act var result = await GetDisplayPackageResult(null, true); // Assert var model = ResultAssert.IsView(result); - Assert.Null(model.ReadMeHtml); - Assert.Null(model.ReadMeHtmlClamped); + Assert.Equal("", model.ReadMeHtml); } [Fact] - public async Task WhenHasReadMeFalse_ReturnsNull() + public async Task WhenHasReadMeFalse_ReturnsEmptyString() { // Arrange and Act var result = await GetDisplayPackageResult(null, false); // Assert var model = ResultAssert.IsView(result); - Assert.Null(model.ReadMeHtml); - Assert.Null(model.ReadMeHtmlClamped); + Assert.Equal("", model.ReadMeHtml); } private async Task GetDisplayPackageResult(string readMeHtml, bool hasReadMe) diff --git a/tests/NuGetGallery.Facts/Services/ReadMeServiceFacts.cs b/tests/NuGetGallery.Facts/Services/ReadMeServiceFacts.cs index 52b81e8437..75a4698cad 100644 --- a/tests/NuGetGallery.Facts/Services/ReadMeServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/ReadMeServiceFacts.cs @@ -92,9 +92,11 @@ public void EncodesHtmlInMarkdown(string originalMd, string expectedHtml) } [Theory] - [InlineData("# Heading", "

Heading

")] + [InlineData("# Heading", "

Heading

")] [InlineData("- List", "
  • List
")] [InlineData("[text](http://www.test.com)", "

text

")] + [InlineData("[text](javascript:alert('hi'))", "

text

")] + [InlineData("> Blockquote", "

<text>Blockquote</text>

")] public void ConvertsMarkdownToHtml(string originalMd, string expectedHtml) { Assert.Equal(expectedHtml, StripNewLines(ReadMeService.GetReadMeHtml(originalMd))); From a5115a8dba3c4c48faca5bb962f7675691836f07 Mon Sep 17 00:00:00 2001 From: Xavier Decoster Date: Wed, 25 Oct 2017 19:27:33 +0200 Subject: [PATCH 02/16] Get rid of obsolete notifications on package details view (#4902) * Remove "pending edit" notifications from package details view * Remove min-client requirement for push message on package details view * Mark AuditedPackageAction.UndoEdit as obsolete --- .../Auditing/AuditedPackageAction.cs | 3 + .../Controllers/PackagesController.cs | 80 +------------------ src/NuGetGallery/UrlExtensions.cs | 17 ---- .../ViewModels/DisplayPackageViewModel.cs | 8 -- .../ViewModels/ListPackageItemViewModel.cs | 1 - .../Views/Packages/DisplayPackage.cshtml | 32 -------- .../Auditing/AuditedPackageActionTests.cs | 2 +- 7 files changed, 5 insertions(+), 138 deletions(-) diff --git a/src/NuGetGallery.Core/Auditing/AuditedPackageAction.cs b/src/NuGetGallery.Core/Auditing/AuditedPackageAction.cs index 2e649aae55..6d0c82df60 100644 --- a/src/NuGetGallery.Core/Auditing/AuditedPackageAction.cs +++ b/src/NuGetGallery.Core/Auditing/AuditedPackageAction.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; + namespace NuGetGallery.Auditing { public enum AuditedPackageAction @@ -11,6 +13,7 @@ public enum AuditedPackageAction List, Unlist, Edit, + [Obsolete("Undo package edit functionality is being retired.")] UndoEdit, Verify } diff --git a/src/NuGetGallery/Controllers/PackagesController.cs b/src/NuGetGallery/Controllers/PackagesController.cs index f111897b7b..7b8421059c 100644 --- a/src/NuGetGallery/Controllers/PackagesController.cs +++ b/src/NuGetGallery/Controllers/PackagesController.cs @@ -126,64 +126,6 @@ public virtual JsonResult UploadPackageProgress() return Json(progress, JsonRequestBehavior.AllowGet); } - [Authorize] - [HttpPost] - [RequiresAccountConfirmation("undo pending edits")] - [ValidateAntiForgeryToken] - public virtual async Task UndoPendingEdits(string id, string version) - { - var package = _packageService.FindPackageByIdAndVersion(id, version); - if (package == null) - { - return HttpNotFound(); - } - - if (!package.IsOwnerOrAdmin(User)) - { - return new HttpStatusCodeResult(403, "Forbidden"); - } - - // To do as much successful cancellation as possible, Will not batch, but will instead try to cancel - // pending edits 1 at a time, starting with oldest first. - var pendingEdits = _entitiesContext.Set() - .Where(pe => pe.PackageKey == package.Key) - .OrderBy(pe => pe.Timestamp) - .ToList(); - - int numOK = 0; - int numConflicts = 0; - foreach (var result in pendingEdits) - { - try - { - _entitiesContext.DeleteOnCommit(result); - await _entitiesContext.SaveChangesAsync(); - numOK += 1; - } - catch (DataException) - { - numConflicts += 1; - } - } - - if (numConflicts > 0) - { - TempData["Message"] = "Your pending edit has already been completed and could not be canceled."; - } - else if (numOK > 0) - { - await _auditingService.SaveAuditRecordAsync(new PackageAuditRecord(package, AuditedPackageAction.UndoEdit)); - - TempData["Message"] = "Your pending edits for this package were successfully canceled."; - } - else - { - TempData["Message"] = "No pending edits were found for this package. The edits may have already been completed."; - } - - return Redirect(Url.Package(id, version)); - } - [Authorize] [RequiresAccountConfirmation("upload a package")] public async virtual Task UploadPackage() @@ -482,9 +424,7 @@ public virtual async Task DisplayPackage(string id, string version } model.ReadMeHtml = await _readMeService.GetReadMeHtmlAsync(package, isReadMePending); - - model.PolicyMessage = GetDisplayPackagePolicyMessage(package.PackageRegistration); - + var externalSearchService = _searchService as ExternalSearchService; if (_searchService.ContainsAllVersions && externalSearchService != null) { @@ -530,24 +470,6 @@ public virtual async Task DisplayPackage(string id, string version return View(model); } - private string GetDisplayPackagePolicyMessage(PackageRegistration package) - { - // display package policy message to package owners and admins. - if (package.IsOwnerOrAdmin(User)) - { - var propagators = package.Owners.Where(RequireSecurePushForCoOwnersPolicy.IsSubscribed); - if (propagators.Any()) - { - return string.Format(CultureInfo.CurrentCulture, - Strings.DisplayPackage_SecurePushRequired, - string.Join(", ", propagators.Select(u => u.Username)), - SecurePushSubscription.MinProtocolVersion, - _config.GalleryOwner.Address); - } - } - return string.Empty; - } - public virtual async Task ListPackages(PackageListSearchViewModel searchAndListModel) { var page = searchAndListModel.Page; diff --git a/src/NuGetGallery/UrlExtensions.cs b/src/NuGetGallery/UrlExtensions.cs index 161fa0b4df..f453c7004f 100644 --- a/src/NuGetGallery/UrlExtensions.cs +++ b/src/NuGetGallery/UrlExtensions.cs @@ -207,23 +207,6 @@ public static string PackageList(this UrlHelper url, bool relativeUrl = true) return GetRouteLink(url, RouteName.ListPackages, relativeUrl); } - public static string UndoPendingEdits( - this UrlHelper url, - IPackageVersionModel package, - bool relativeUrl = true) - { - return GetActionLink( - url, - "UndoPendingEdits", - "Packages", - relativeUrl, - routeValues: new RouteValueDictionary - { - { "id", package.Id }, - { "version", package.Version } - }); - } - public static string Package(this UrlHelper url, string id, bool relativeUrl = true) { return url.Package(id, version: null, relativeUrl: relativeUrl); diff --git a/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs b/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs index f5979d2c82..3a355ae1c1 100644 --- a/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs +++ b/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs @@ -49,7 +49,6 @@ public void SetPendingMetadata(PackageEdit pendingMetadata) { if (pendingMetadata.TriedCount < 3) { - HasPendingMetadata = true; Authors = pendingMetadata.Authors; Copyright = pendingMetadata.Copyright; Description = pendingMetadata.Description; @@ -60,19 +59,12 @@ public void SetPendingMetadata(PackageEdit pendingMetadata) Tags = pendingMetadata.Tags.ToStringSafe().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries); Title = pendingMetadata.Title; } - else - { - HasPendingMetadata = false; - IsLastEditFailed = true; - } } public DependencySetsViewModel Dependencies { get; set; } public IEnumerable PackageVersions { get; set; } public string Copyright { get; set; } public string ReadMeHtml { get; set; } - public bool HasPendingMetadata { get; private set; } - public bool IsLastEditFailed { get; private set; } public DateTime? LastEdited { get; set; } public int DownloadsPerDay { get; private set; } public int TotalDaysSinceCreated { get; private set; } diff --git a/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs b/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs index a05fec8b7a..b99c866bc3 100644 --- a/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs +++ b/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs @@ -37,7 +37,6 @@ public ListPackageItemViewModel(Package package) public string MinClientVersion { get; set; } public string ShortDescription { get; set; } public bool IsDescriptionTruncated { get; set; } - public string PolicyMessage { get; set; } public bool? IsVerified { get; set; } public bool UseVersion diff --git a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml index abbfe2b7cf..830d1417bc 100644 --- a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml @@ -144,38 +144,6 @@

@Html.PreFormattedText(Model.Description)

} - @if (!string.IsNullOrEmpty(Model.PolicyMessage)) - { - @ViewHelpers.AlertWarning(@@Model.PolicyMessage) - } - - @if (Model.HasPendingMetadata) - { - using (Html.BeginForm(actionName: "UndoPendingEdits", controllerName: "Packages")) - { - @Html.AntiForgeryToken() - - @ViewHelpers.AlertInfo( - @ - An edit is pending for this package version. You are seeing the edited package description now. - These changes will be applied to the package file and become visible to everybody soon unless you - undo pending edits first. - - ) - } - } - - @if (Model.IsLastEditFailed) - { - @ViewHelpers.AlertDanger( - @ - Your last edit to this package was not successfully applied. - You can edit - the package and submit the edit again to retry the edit. - - ) - } - @if (Model.Validating) { @ViewHelpers.AlertWarning( diff --git a/tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageActionTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageActionTests.cs index c915f5130a..58677ce374 100644 --- a/tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageActionTests.cs +++ b/tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageActionTests.cs @@ -17,8 +17,8 @@ public void Definition_HasNotChanged() "Edit", "List", "SoftDelete", - "UndoEdit", "Unlist", + "UndoEdit", "Verify" }; From 7e72e47695c426bae732cc7a77862fcb11f421de Mon Sep 17 00:00:00 2001 From: Scott Bommarito Date: Wed, 25 Oct 2017 11:08:16 -0700 Subject: [PATCH 03/16] delete owner requests upon package hard delete (#4898) --- src/NuGetGallery/Services/PackageDeleteService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/NuGetGallery/Services/PackageDeleteService.cs b/src/NuGetGallery/Services/PackageDeleteService.cs index c270413063..4e340cd5c7 100644 --- a/src/NuGetGallery/Services/PackageDeleteService.cs +++ b/src/NuGetGallery/Services/PackageDeleteService.cs @@ -22,6 +22,9 @@ SELECT TOP 1 [Key] FROM Packages AS p WHERE p.[PackageRegistrationKey] = @key) BEGIN + DELETE por FROM PackageOwnerRequests As por + WHERE por.[PackageRegistrationKey] = @key + DELETE pro FROM PackageRegistrationOwners AS pro WHERE pro.[PackageRegistrationKey] = @key From 5b32ce3505273296620d9cb8cfdcc9e5e1ba3138 Mon Sep 17 00:00:00 2001 From: Shishir H Date: Wed, 25 Oct 2017 11:49:53 -0700 Subject: [PATCH 04/16] [Prefix] List reserved namespaces in the manage packages page (#4901) --- src/Bootstrap/dist/css/bootstrap-theme.css | 15 +++++ src/Bootstrap/less/theme/base.less | 5 ++ .../less/theme/common-user-package-list.less | 9 +++ .../Content/gallery/css/bootstrap-theme.css | 15 +++++ .../img/reserved-indicator-256x256.png | Bin 0 -> 5774 bytes .../Controllers/UsersController.cs | 4 +- src/NuGetGallery/NuGetGallery.csproj | 3 + .../Scripts/gallery/page-manage-packages.js | 2 + .../ViewModels/ManagePackagesViewModel.cs | 2 + .../ReservedNamespaceListItemViewModel.cs | 38 +++++++++++ .../ReservedNamespaceListViewModel.cs | 19 ++++++ src/NuGetGallery/Views/Users/Packages.cshtml | 2 + .../Users/_ReservedNamespacesList.cshtml | 63 ++++++++++++++++++ 13 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/NuGetGallery/Content/gallery/img/reserved-indicator-256x256.png create mode 100644 src/NuGetGallery/ViewModels/ReservedNamespaceListItemViewModel.cs create mode 100644 src/NuGetGallery/ViewModels/ReservedNamespaceListViewModel.cs create mode 100644 src/NuGetGallery/Views/Users/_ReservedNamespacesList.cshtml diff --git a/src/Bootstrap/dist/css/bootstrap-theme.css b/src/Bootstrap/dist/css/bootstrap-theme.css index e7e44d71ed..5edfbff594 100644 --- a/src/Bootstrap/dist/css/bootstrap-theme.css +++ b/src/Bootstrap/dist/css/bootstrap-theme.css @@ -212,6 +212,10 @@ img.package-icon { margin-right: auto; margin-left: auto; } +img.reserved-indicator-icon { + margin-right: auto; + margin-left: auto; +} .package-list { padding-left: 0; margin-top: 8px; @@ -397,6 +401,10 @@ img.package-icon { min-width: 20px; max-height: 2em; } +.user-package-list .manage-package-listing .reserved-indicator-icon { + min-width: 20px; + max-height: 2em; +} .user-package-list .manage-package-listing .align-middle { vertical-align: middle; } @@ -407,6 +415,13 @@ img.package-icon { overflow-wrap: break-word; } +.user-package-list .manage-package-listing .reserved-id { + word-break: normal; + word-break: break-word; + word-wrap: break-word; + + overflow-wrap: break-word; +} @media (min-width: 768px) { .user-package-list .manage-package-listing .package-controls { white-space: nowrap; diff --git a/src/Bootstrap/less/theme/base.less b/src/Bootstrap/less/theme/base.less index 53c649b01b..168e8aa87f 100644 --- a/src/Bootstrap/less/theme/base.less +++ b/src/Bootstrap/less/theme/base.less @@ -272,6 +272,11 @@ img.package-icon { margin-right: auto; } +img.reserved-indicator-icon { + margin-left: auto; + margin-right: auto; +} + .package-list { margin-top: 8px; margin-bottom: 8px; diff --git a/src/Bootstrap/less/theme/common-user-package-list.less b/src/Bootstrap/less/theme/common-user-package-list.less index 51ce74ff6e..d412837a57 100644 --- a/src/Bootstrap/less/theme/common-user-package-list.less +++ b/src/Bootstrap/less/theme/common-user-package-list.less @@ -5,6 +5,11 @@ min-width: 20px; } + .reserved-indicator-icon { + max-height: 2em; + min-width: 20px; + } + .align-middle { vertical-align: middle; } @@ -13,6 +18,10 @@ .break-word; } + .reserved-id { + .break-word; + } + .package-controls { @media (min-width: @screen-sm-min) { white-space: nowrap; diff --git a/src/NuGetGallery/Content/gallery/css/bootstrap-theme.css b/src/NuGetGallery/Content/gallery/css/bootstrap-theme.css index e7e44d71ed..5edfbff594 100644 --- a/src/NuGetGallery/Content/gallery/css/bootstrap-theme.css +++ b/src/NuGetGallery/Content/gallery/css/bootstrap-theme.css @@ -212,6 +212,10 @@ img.package-icon { margin-right: auto; margin-left: auto; } +img.reserved-indicator-icon { + margin-right: auto; + margin-left: auto; +} .package-list { padding-left: 0; margin-top: 8px; @@ -397,6 +401,10 @@ img.package-icon { min-width: 20px; max-height: 2em; } +.user-package-list .manage-package-listing .reserved-indicator-icon { + min-width: 20px; + max-height: 2em; +} .user-package-list .manage-package-listing .align-middle { vertical-align: middle; } @@ -407,6 +415,13 @@ img.package-icon { overflow-wrap: break-word; } +.user-package-list .manage-package-listing .reserved-id { + word-break: normal; + word-break: break-word; + word-wrap: break-word; + + overflow-wrap: break-word; +} @media (min-width: 768px) { .user-package-list .manage-package-listing .package-controls { white-space: nowrap; diff --git a/src/NuGetGallery/Content/gallery/img/reserved-indicator-256x256.png b/src/NuGetGallery/Content/gallery/img/reserved-indicator-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..7730b3d4f96766f4fd5fe924b4c10b49826e3b1d GIT binary patch literal 5774 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 + + @@ -1908,6 +1910,7 @@ + diff --git a/src/NuGetGallery/Scripts/gallery/page-manage-packages.js b/src/NuGetGallery/Scripts/gallery/page-manage-packages.js index 0f75fa69f7..326c8f40f4 100644 --- a/src/NuGetGallery/Scripts/gallery/page-manage-packages.js +++ b/src/NuGetGallery/Scripts/gallery/page-manage-packages.js @@ -6,4 +6,6 @@ window.nuget.configureExpanderHeading("requests-Incoming"); window.nuget.configureExpanderHeading("requests-Outgoing"); + + window.nuget.configureExpanderHeading("reservednamespaces"); }); diff --git a/src/NuGetGallery/ViewModels/ManagePackagesViewModel.cs b/src/NuGetGallery/ViewModels/ManagePackagesViewModel.cs index 8ab5a563f9..1c1f087bde 100644 --- a/src/NuGetGallery/ViewModels/ManagePackagesViewModel.cs +++ b/src/NuGetGallery/ViewModels/ManagePackagesViewModel.cs @@ -10,5 +10,7 @@ public class ManagePackagesViewModel public IEnumerable Packages { get; set; } public OwnerRequestsViewModel OwnerRequests { get; set; } + + public ReservedNamespaceListViewModel ReservedNamespaces { get; set; } } } \ No newline at end of file diff --git a/src/NuGetGallery/ViewModels/ReservedNamespaceListItemViewModel.cs b/src/NuGetGallery/ViewModels/ReservedNamespaceListItemViewModel.cs new file mode 100644 index 0000000000..a6d9882033 --- /dev/null +++ b/src/NuGetGallery/ViewModels/ReservedNamespaceListItemViewModel.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; + +namespace NuGetGallery +{ + public class ReservedNamespaceListItemViewModel + { + public string Value { get; } + + public bool IsPublic { get; } + + public bool IsPrefix { get; } + + public IEnumerable Owners { get; } + + public ReservedNamespaceListItemViewModel(ReservedNamespace reservedNamespace) + { + Value = reservedNamespace.Value; + IsPublic = reservedNamespace.IsSharedNamespace; + IsPrefix = reservedNamespace.IsPrefix; + Owners = reservedNamespace.Owners; + } + + public string GetPattern() + { + var namespaceValue = Value; + if (IsPrefix) + { + namespaceValue += "*"; + } + + return namespaceValue; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/ViewModels/ReservedNamespaceListViewModel.cs b/src/NuGetGallery/ViewModels/ReservedNamespaceListViewModel.cs new file mode 100644 index 0000000000..2515cfc178 --- /dev/null +++ b/src/NuGetGallery/ViewModels/ReservedNamespaceListViewModel.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Collections.Generic; + +namespace NuGetGallery +{ + public class ReservedNamespaceListViewModel + { + public IEnumerable ReservedNamespaces { get; } + + public ReservedNamespaceListViewModel(ICollection reservedNamespacesList) + { + ReservedNamespaces = reservedNamespacesList + .Select(rn => new ReservedNamespaceListItemViewModel(rn)); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Views/Users/Packages.cshtml b/src/NuGetGallery/Views/Users/Packages.cshtml index dcf200ea93..7a16f70abe 100644 --- a/src/NuGetGallery/Views/Users/Packages.cshtml +++ b/src/NuGetGallery/Views/Users/Packages.cshtml @@ -19,6 +19,8 @@ @Html.Partial("_UserPackagesList", new ManagePackagesListViewModel(unlistedPackages, "Unlisted")) } + @Html.Partial("_ReservedNamespacesList", Model.ReservedNamespaces) + @{ if (Model.OwnerRequests.Incoming.RequestItems.Any() || Model.OwnerRequests.Outgoing.RequestItems.Any()) { diff --git a/src/NuGetGallery/Views/Users/_ReservedNamespacesList.cshtml b/src/NuGetGallery/Views/Users/_ReservedNamespacesList.cshtml new file mode 100644 index 0000000000..8a6c585228 --- /dev/null +++ b/src/NuGetGallery/Views/Users/_ReservedNamespacesList.cshtml @@ -0,0 +1,63 @@ +@model ReservedNamespaceListViewModel +@{ + if (!Model.ReservedNamespaces.Any()) + { + return; + } +} + +
+
+

+ + + My Reserved Package IDs + +

+
+
+ + + + + + + + + + + @foreach (var reservedNamespace in @Model.ReservedNamespaces) + { + + + + + @if (reservedNamespace.IsPublic) + { + + } + else + { + + } + + } + +
Package ID or PrefixOwnersUpload Restrictions
+ @reservedNamespace.GetPattern() + + @foreach (var owner in reservedNamespace.Owners) + { + @owner.Username + } + Any NuGet.org AccountPrefix or ID Owners Only
+
+
+
+
\ No newline at end of file From d28063ad0fd512f503a31d276e372a816657095c Mon Sep 17 00:00:00 2001 From: Ryu Yu Date: Thu, 26 Oct 2017 10:22:43 -0700 Subject: [PATCH 05/16] Update number shortener to maintain enough sig figs for distinction. (#4896) --- .../Scripts/gallery/stats-perpackagestatsgraphs.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/NuGetGallery/Scripts/gallery/stats-perpackagestatsgraphs.js b/src/NuGetGallery/Scripts/gallery/stats-perpackagestatsgraphs.js index 86a74f6e78..73c7e4202f 100644 --- a/src/NuGetGallery/Scripts/gallery/stats-perpackagestatsgraphs.js +++ b/src/NuGetGallery/Scripts/gallery/stats-perpackagestatsgraphs.js @@ -261,13 +261,24 @@ var GetChartData = function (rawData, filter) { } var GetShortNumberString = function (number) { + if (number == 0) { + return "0"; + } + var abbreviation = ["", "k", "M", "B", "T", "q", "Q", "s", "S", "o", "n"]; var numDiv = 0; while (number >= 1000) { - number = Math.floor(number / 1000); + number = number / 1000.0; numDiv++; } + rounded = Math.floor(number); + if (rounded >= 100) { + number = number.toPrecision(3); + } else { + number = number.toPrecision(2); + } + if (numDiv >= abbreviation.Length) { return number + "10^" + numDiv*3; } From 42ce45314fcd5a47f6a0080b4f74344d07257d2d Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Thu, 26 Oct 2017 15:11:14 -0700 Subject: [PATCH 06/16] Schema changes for Organization membership (#4904) --- .../Entities/EntitiesContext.cs | 30 ++++- .../Entities/IEntitiesContext.cs | 11 +- src/NuGetGallery.Core/Entities/Membership.cs | 45 +++++++ .../Entities/Organization.cs | 32 +++++ src/NuGetGallery.Core/Entities/User.cs | 21 +++ .../NuGetGallery.Core.csproj | 2 + ...1710250430326_AddOrganizations.Designer.cs | 29 ++++ .../201710250430326_AddOrganizations.cs | 49 +++++++ .../201710250430326_AddOrganizations.resx | 126 ++++++++++++++++++ src/NuGetGallery/NuGetGallery.csproj | 7 + .../TestUtils/FakeEntitiesContext.cs | 12 ++ 11 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 src/NuGetGallery.Core/Entities/Membership.cs create mode 100644 src/NuGetGallery.Core/Entities/Organization.cs create mode 100644 src/NuGetGallery/Migrations/201710250430326_AddOrganizations.Designer.cs create mode 100644 src/NuGetGallery/Migrations/201710250430326_AddOrganizations.cs create mode 100644 src/NuGetGallery/Migrations/201710250430326_AddOrganizations.resx diff --git a/src/NuGetGallery.Core/Entities/EntitiesContext.cs b/src/NuGetGallery.Core/Entities/EntitiesContext.cs index 6c8b5834fb..3a6c8df815 100644 --- a/src/NuGetGallery.Core/Entities/EntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/EntitiesContext.cs @@ -41,10 +41,19 @@ public EntitiesContext(string connectionString, bool readOnly) public IDbSet PackageRegistrations { get; set; } public IDbSet Credentials { get; set; } public IDbSet Scopes { get; set; } - public IDbSet Users { get; set; } public IDbSet UserSecurityPolicies { get; set; } public IDbSet ReservedNamespaces { get; set; } + /// + /// User or organization accounts. + /// + public IDbSet Users { get; set; } + + /// + /// Organization accounts. + /// + public IDbSet Organizations { get; set; } + IDbSet IEntitiesContext.Set() { return base.Set(); @@ -118,6 +127,25 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) .Map(c => c.ToTable("UserRoles") .MapLeftKey("UserKey") .MapRightKey("RoleKey")); + + modelBuilder.Entity() + .HasKey(o => o.Key) + .HasRequired(o => o.Account) + .WithOptional(u => u.Organization) + .WillCascadeOnDelete(false); // Disabled to prevent multiple cascade paths. + + modelBuilder.Entity() + .HasKey(m => new { m.OrganizationKey, m.MemberKey }) + .HasRequired(m => m.Organization) + .WithMany(o => o.Memberships) + .HasForeignKey(m => m.OrganizationKey) + .WillCascadeOnDelete(true); // Memberships will be deleted with the Organization. + + modelBuilder.Entity() + .HasRequired(m => m.Member) + .WithMany(u => u.Memberships) + .HasForeignKey(m => m.MemberKey) + .WillCascadeOnDelete(true); // Memberships will be deleted with the User (Member). modelBuilder.Entity() .HasKey(u => u.Key); diff --git a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs index 6f85ac73e9..e523488caa 100644 --- a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs @@ -13,10 +13,19 @@ public interface IEntitiesContext IDbSet PackageRegistrations { get; set; } IDbSet Credentials { get; set; } IDbSet Scopes { get; set; } - IDbSet Users { get; set; } IDbSet UserSecurityPolicies { get; set; } IDbSet ReservedNamespaces { get; set; } + /// + /// User or organization accounts. + /// + IDbSet Users { get; set; } + + /// + /// Organization accounts. + /// + IDbSet Organizations { get; set; } + Task SaveChangesAsync(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Set", Justification="This is to match the EF terminology.")] IDbSet Set() where T : class; diff --git a/src/NuGetGallery.Core/Entities/Membership.cs b/src/NuGetGallery.Core/Entities/Membership.cs new file mode 100644 index 0000000000..23092444f7 --- /dev/null +++ b/src/NuGetGallery.Core/Entities/Membership.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace NuGetGallery +{ + /// + /// Tracks membership of a User account in an Organization account. + /// + public class Membership + { + /// + /// Organization foreign key. + /// + public int OrganizationKey { get; set; } + + /// + /// The that contains members. + /// + public virtual Organization Organization { get; set; } + + /// + /// Member (User) foreign key. + /// + public int MemberKey { get; set; } + + /// + /// The that is a member of the . + /// + /// Note that there is no database contraint preventing memberships of Organizations into other + /// Organizations. For now this is restricted by the Gallery, but could be considered in the + /// future if we want to support Organization teams. + /// + public virtual User Member { get; set; } + + /// + /// Whether the is an administrator for the . + /// + /// Administrators have the following capabilities that collaborators don't: + /// - Organization management (e.g., settings, membership) + /// - Package owner management + /// - Pushing new package registrations + /// + public bool IsAdmin { get; set; } + } +} diff --git a/src/NuGetGallery.Core/Entities/Organization.cs b/src/NuGetGallery.Core/Entities/Organization.cs new file mode 100644 index 0000000000..f8f00d4e6d --- /dev/null +++ b/src/NuGetGallery.Core/Entities/Organization.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace NuGetGallery +{ + /// + /// The organization associated with a account. + /// + /// The Users table contains both User and Organization accounts. The Organizations table exists both + /// as a constraint for Membership as well as a place for possible Organization-only settings. If User + /// and Organization settings diverge, we can consider creating a separate UserSettings table as well. + /// + public class Organization + { + /// + /// Organization primary key. + /// + public int Key { get; set; } + + /// + /// Organization account (User). + /// + public User Account { get; set; } + + /// + /// Organization memberships. + /// + public virtual ICollection Memberships { get; set; } + } +} diff --git a/src/NuGetGallery.Core/Entities/User.cs b/src/NuGetGallery.Core/Entities/User.cs index fd80103805..d272770c73 100644 --- a/src/NuGetGallery.Core/Entities/User.cs +++ b/src/NuGetGallery.Core/Entities/User.cs @@ -9,6 +9,17 @@ namespace NuGetGallery { + /// + /// NuGetGallery now supports both Users and Organizations which share common features such as: + /// - Namespace ownership + /// - Package ownership + /// - Security policies + /// - Name and email unique across both Users and Organizations + /// + /// Organizations should not support UserRoles or Credentials, but this is not constrained by the + /// database. In the future, we could consider renaming the Users table to Accounts and adding a + /// Users table to constrain UserRoles and Credentials to only User accounts. + /// public class User : IEntity { public User() : this(null) @@ -24,6 +35,16 @@ public User(string username) Username = username; } + /// + /// represented by this account, if any. + /// + public Organization Organization { get; set; } + + /// + /// Organization memberships for a account. + /// + public virtual ICollection Memberships { get; set; } + [StringLength(256)] public string EmailAddress { get; set; } diff --git a/src/NuGetGallery.Core/NuGetGallery.Core.csproj b/src/NuGetGallery.Core/NuGetGallery.Core.csproj index ab1496dde7..b34c1bfa13 100644 --- a/src/NuGetGallery.Core/NuGetGallery.Core.csproj +++ b/src/NuGetGallery.Core/NuGetGallery.Core.csproj @@ -157,6 +157,8 @@ + + diff --git a/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.Designer.cs b/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.Designer.cs new file mode 100644 index 0000000000..73f2758b43 --- /dev/null +++ b/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.Designer.cs @@ -0,0 +1,29 @@ +// +namespace NuGetGallery.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class AddOrganizations : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddOrganizations)); + + string IMigrationMetadata.Id + { + get { return "201710250430326_AddOrganizations"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.cs b/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.cs new file mode 100644 index 0000000000..848a251776 --- /dev/null +++ b/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.cs @@ -0,0 +1,49 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddOrganizations : DbMigration + { + public override void Up() + { + CreateTable( + "dbo.Memberships", + c => new + { + OrganizationKey = c.Int(nullable: false), + MemberKey = c.Int(nullable: false), + IsAdmin = c.Boolean(nullable: false), + }) + .PrimaryKey(t => new { t.OrganizationKey, t.MemberKey }) + .ForeignKey("dbo.Users", t => t.MemberKey, cascadeDelete: true) + .ForeignKey("dbo.Organizations", t => t.OrganizationKey, cascadeDelete: true) + .Index(t => t.OrganizationKey) + .Index(t => t.MemberKey); + + CreateTable( + "dbo.Organizations", + c => new + { + Key = c.Int(nullable: false), + AccountKey = c.Int(nullable: false), + }) + .PrimaryKey(t => t.Key) + .ForeignKey("dbo.Users", t => t.Key) + .Index(t => t.Key); + + } + + public override void Down() + { + DropForeignKey("dbo.Memberships", "OrganizationKey", "dbo.Organizations"); + DropForeignKey("dbo.Organizations", "Key", "dbo.Users"); + DropForeignKey("dbo.Memberships", "MemberKey", "dbo.Users"); + DropIndex("dbo.Organizations", new[] { "Key" }); + DropIndex("dbo.Memberships", new[] { "MemberKey" }); + DropIndex("dbo.Memberships", new[] { "OrganizationKey" }); + DropTable("dbo.Organizations"); + DropTable("dbo.Memberships"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.resx b/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.resx new file mode 100644 index 0000000000..0c6e911590 --- /dev/null +++ b/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index a04a78a05d..ffdb15e446 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -776,6 +776,10 @@ + + + 201710250430326_AddOrganizations.cs + @@ -1851,6 +1855,9 @@ 201709202249402_AddPackageOwnershipRequestsPage.cs + + 201710250430326_AddOrganizations.cs + diff --git a/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs b/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs index f3f5b9657c..fc644d9970 100644 --- a/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs +++ b/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs @@ -98,6 +98,18 @@ public IDbSet Users } } + public IDbSet Organizations + { + get + { + return Set(); + } + set + { + throw new NotSupportedException(); + } + } + public IDbSet UserSecurityPolicies { get From 5f88eecceb1f5c6bd641bb9bf74ac943a38f971d Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Thu, 26 Oct 2017 15:26:45 -0700 Subject: [PATCH 07/16] Gallery constraints to prevent Organization authentication (#4915) --- .../AuditedAuthenticatedOperationAction.cs | 5 ++ .../Authentication/AuthenticationService.cs | 39 +++++++++++++- src/NuGetGallery/ExtensionMethods.cs | 22 -------- src/NuGetGallery/Extensions/UserExtensions.cs | 51 ++++++++++++++++++ src/NuGetGallery/NuGetGallery.csproj | 1 + src/NuGetGallery/Strings.Designer.cs | 9 ++++ src/NuGetGallery/Strings.resx | 3 ++ ...uditedAuthenticatedOperationActionTests.cs | 1 + .../AuthenticationServiceFacts.cs | 52 +++++++++++++++++++ tests/NuGetGallery.Facts/Framework/Fakes.cs | 31 ++++++++++- 10 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 src/NuGetGallery/Extensions/UserExtensions.cs diff --git a/src/NuGetGallery.Core/Auditing/AuditedAuthenticatedOperationAction.cs b/src/NuGetGallery.Core/Auditing/AuditedAuthenticatedOperationAction.cs index 4e5c7ec896..91f6cdc336 100644 --- a/src/NuGetGallery.Core/Auditing/AuditedAuthenticatedOperationAction.cs +++ b/src/NuGetGallery.Core/Auditing/AuditedAuthenticatedOperationAction.cs @@ -19,5 +19,10 @@ public enum AuditedAuthenticatedOperationAction /// Login failed, user exists but password is invalid ///
FailedLoginInvalidPassword, + + /// + /// Login failed, user is an organization and should not have credentials. + /// + FailedLoginUserIsOrganization } } \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/AuthenticationService.cs b/src/NuGetGallery/Authentication/AuthenticationService.cs index d40c687750..72eef63c45 100644 --- a/src/NuGetGallery/Authentication/AuthenticationService.cs +++ b/src/NuGetGallery/Authentication/AuthenticationService.cs @@ -127,6 +127,17 @@ await Auditing.SaveAuditRecordAsync( return new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.BadCredentials); } + if (user.IsOrganization()) + { + _trace.Information($"Cannot authenticate organization account'{userNameOrEmail}'."); + + await Auditing.SaveAuditRecordAsync( + new FailedAuthenticatedOperationAuditRecord( + userNameOrEmail, AuditedAuthenticatedOperationAction.FailedLoginUserIsOrganization)); + + return new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.BadCredentials); + } + int remainingMinutes; if (IsAccountLocked(user, out remainingMinutes)) @@ -201,7 +212,21 @@ private async Task AuthenticateInternal(Func @@ -810,6 +845,8 @@ public virtual bool ValidatePasswordCredential(IEnumerable creds, st private async Task MigrateCredentials(User user, List creds, string password) { + // Authenticate already validated that user is not an Organization, so no need to replicate here. + var toRemove = creds.Where(c => !string.Equals( c.Type, diff --git a/src/NuGetGallery/ExtensionMethods.cs b/src/NuGetGallery/ExtensionMethods.cs index 7d50dfd62d..3fdadeb716 100644 --- a/src/NuGetGallery/ExtensionMethods.cs +++ b/src/NuGetGallery/ExtensionMethods.cs @@ -7,7 +7,6 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; -using System.Net.Mail; using System.Security; using System.Security.Claims; using System.Security.Principal; @@ -259,16 +258,6 @@ public static IQueryable SortBy(this IQueryable source, string sortExpr return source.Provider.CreateQuery(methodCallExpression); } - public static MailAddress ToMailAddress(this User user) - { - if (!user.Confirmed) - { - return new MailAddress(user.UnconfirmedEmailAddress, user.Username); - } - - return new MailAddress(user.EmailAddress, user.Username); - } - public static bool IsError(this HtmlHelper htmlHelper, Expression> expression) { var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); @@ -552,17 +541,6 @@ public static User GetCurrentUser(this IOwinContext self) return user; } - /// - /// Get the current API key credential, if available. - /// - public static Credential GetCurrentApiKeyCredential(this User user, IIdentity identity) - { - var claimsIdentity = identity as ClaimsIdentity; - var apiKey = claimsIdentity.GetClaimOrDefault(NuGetClaims.ApiKey); - - return user.Credentials.FirstOrDefault(c => c.Value == apiKey); - } - private static User LoadUser(IOwinContext context) { var principal = context.Authentication.User; diff --git a/src/NuGetGallery/Extensions/UserExtensions.cs b/src/NuGetGallery/Extensions/UserExtensions.cs new file mode 100644 index 0000000000..64555f65e8 --- /dev/null +++ b/src/NuGetGallery/Extensions/UserExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Net.Mail; +using System.Security.Claims; +using System.Security.Principal; +using NuGetGallery.Authentication; + +namespace NuGetGallery +{ + /// + /// Extension methods for the NuGetGallery.User entity. + /// + public static class UserExtensions + { + /// + /// Convert a User's email to a System.Net MailAddress. + /// + public static MailAddress ToMailAddress(this User user) + { + if (!user.Confirmed) + { + return new MailAddress(user.UnconfirmedEmailAddress, user.Username); + } + + return new MailAddress(user.EmailAddress, user.Username); + } + + /// + /// Get the current API key credential, if available. + /// + public static Credential GetCurrentApiKeyCredential(this User user, IIdentity identity) + { + var claimsIdentity = identity as ClaimsIdentity; + var apiKey = claimsIdentity.GetClaimOrDefault(NuGetClaims.ApiKey); + + return user.Credentials.FirstOrDefault(c => c.Value == apiKey); + } + + /// + /// Determines if the User (account) belongs to an organization. + /// + /// User (account) instance. + /// True for organizations, false for users. + public static bool IsOrganization(this User account) + { + return account.Organization != null; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index ffdb15e446..da11128f26 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -772,6 +772,7 @@ + diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index 8d10befefa..79728b2d71 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -755,6 +755,15 @@ public static string NuGetPackagePropertyTooLong { } } + /// + /// Looks up a localized string similar to Organization accounts cannot create credentials.. + /// + public static string OrganizationsCannotCreateCredentials { + get { + return ResourceManager.GetString("OrganizationsCannotCreateCredentials", resourceCulture); + } + } + /// /// Looks up a localized string similar to Package created from API.. /// diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index 83ac2bcab9..17b35f1777 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -606,4 +606,7 @@ For more information, please contact '{2}'. The user '{0}' does not have permission to remove the owner '{1}'. + + Organization accounts cannot create credentials. + \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/AuditedAuthenticatedOperationActionTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/AuditedAuthenticatedOperationActionTests.cs index 51cb5a548c..5c746a1f74 100644 --- a/tests/NuGetGallery.Core.Facts/Auditing/AuditedAuthenticatedOperationActionTests.cs +++ b/tests/NuGetGallery.Core.Facts/Auditing/AuditedAuthenticatedOperationActionTests.cs @@ -14,6 +14,7 @@ public void Definition_HasNotChanged() { "FailedLoginInvalidPassword", "FailedLoginNoSuchUser", + "FailedLoginUserIsOrganization", "PackagePushAttemptByNonOwner" }; diff --git a/tests/NuGetGallery.Facts/Authentication/AuthenticationServiceFacts.cs b/tests/NuGetGallery.Facts/Authentication/AuthenticationServiceFacts.cs index 2669fa18b4..3ade0ad7fe 100644 --- a/tests/NuGetGallery.Facts/Authentication/AuthenticationServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Authentication/AuthenticationServiceFacts.cs @@ -67,6 +67,16 @@ public async Task GivenUserNameDoesNotMatchPassword_ItReturnsFailure() Assert.Equal(PasswordAuthenticationResult.AuthenticationResult.BadCredentials, result.Result); } + [Fact] + public async Task GivenAnOrganization_ItReturnsFailure() + { + // Act + var result = await _authenticationService.Authenticate(_fakes.Organization.Username, Fakes.Password); + + // Assert + Assert.Equal(PasswordAuthenticationResult.AuthenticationResult.BadCredentials, result.Result); + } + [Fact] public async Task WritesAuditRecordWhenGivenUserNameDoesNotMatchPassword() { @@ -197,6 +207,21 @@ public async Task GivenInvalidApiKeyCredential_ItReturnsNull() Assert.Null(result); } + [Fact] + public async Task GivenAnOrganizationApiKeyCredential_ItReturnsNull() + { + // Arrange + var organization = _fakes.Organization; + var apiKey = Guid.Parse("1fdc96fe-0b41-4607-bc85-b6533b42d3f8"); + organization.Credentials.Add(TestCredentialHelper.CreateV2ApiKey(apiKey, TimeSpan.FromDays(1))); + + // Act + var result = await _authenticationService.Authenticate(apiKey.ToString()); + + // Assert + Assert.Null(result); + } + [Fact] public async Task WritesAuditRecordWhenGivenInvalidApiKeyCredential() { @@ -720,6 +745,19 @@ public async Task ThrowsExceptionIfNoUserWithProvidedUserName() Assert.Equal(Strings.UserNotFound, ex.Message); } + [Fact] + public async Task GivenAnOrganization_ThrowsInvalidOperationException() + { + // Arrange + var fakes = Get(); + var authService = Get(); + + // Act & Assert + await Assert.ThrowsAsync(async () => { + await authService.ReplaceCredential("testOrganization", fakes.Organization.Credentials.First()); + }); + } + [Fact] public async Task AddsNewCredentialIfNoneWithSameTypeForUser() { @@ -1214,6 +1252,20 @@ public async Task AddsTheCredentialToTheDataStore() authService.Entities.VerifyCommitChanges(); } + [Fact] + public async Task GivenAnOrganization_ThrowsInvalidOperationException() + { + // Arrange + var fakes = Get(); + var credential = new CredentialBuilder().CreatePasswordCredential("password"); + var authService = Get(); + + // Act & Assert + await Assert.ThrowsAsync(async () => { + await authService.AddCredential(fakes.Organization, credential); + }); + } + [Fact] public async Task WritesAuditRecordForTheNewCredential() { diff --git a/tests/NuGetGallery.Facts/Framework/Fakes.cs b/tests/NuGetGallery.Facts/Framework/Fakes.cs index da74024d87..09227086ac 100644 --- a/tests/NuGetGallery.Facts/Framework/Fakes.cs +++ b/tests/NuGetGallery.Facts/Framework/Fakes.cs @@ -21,13 +21,15 @@ public class Fakes public Fakes() { + var credentialBuilder = new CredentialBuilder(); + User = new User("testUser") { Key = 40, EmailAddress = "confirmed0@example.com", Credentials = new List { - new CredentialBuilder().CreatePasswordCredential(Password), + credentialBuilder.CreatePasswordCredential(Password), TestCredentialHelper.CreateV1ApiKey(Guid.Parse("669e180e-335c-491a-ac26-e83c4bd31d65"), ExpirationForApiKeyV1), TestCredentialHelper.CreateV2ApiKey(Guid.Parse("779e180e-335c-491a-ac26-e83c4bd31d87"), @@ -37,6 +39,31 @@ public Fakes() } }; + Organization = new User("testOrganization") + { + Key = 41, + EmailAddress = "confirmedOrganization@example.com", + Organization = new Organization() + { + Key = 1 + }, + // invalid credentials for testing authentication constraints + Credentials = new List + { + credentialBuilder.CreatePasswordCredential(Password) + } + }; + + Organization.Organization.Memberships = new List() + { + new Membership + { + Organization = Organization.Organization, + Member = User, + IsAdmin = true + } + }; + Pbkdf2User = new User("testPbkdf2User") { Key = 41, @@ -90,6 +117,8 @@ public Fakes() public User User { get; } + public User Organization { get; } + public User ShaUser { get; } public User Pbkdf2User { get; } From 4d8c2ecbe6a7dee64f2dfe47ce0f2b5aff2c1821 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Fri, 27 Oct 2017 07:58:24 -0700 Subject: [PATCH 08/16] Fix readme telemetry (#4905) --- .../App_Start/DefaultDependenciesModule.cs | 17 +- src/NuGetGallery/App_Start/OwinStartup.cs | 5 +- .../Diagnostics/DiagnosticsService.cs | 13 +- .../Diagnostics/TraceDiagnosticsSource.cs | 7 +- src/NuGetGallery/NuGetGallery.csproj | 3 +- .../OData/QueryAllowed/ODataQueryVerifier.cs | 11 +- .../Services/CloudDownloadCountService.cs | 11 +- src/NuGetGallery/Services/ITelemetryClient.cs | 18 ++ .../Services/ITelemetryService.cs | 9 + .../Services/TelemetryClientWrapper.cs | 56 ++++++ src/NuGetGallery/Services/TelemetryService.cs | 53 +++-- src/NuGetGallery/Telemetry/QuietLog.cs | 2 + src/NuGetGallery/Telemetry/Telemetry.cs | 43 ----- tests/NuGetGallery.Facts/Framework/Fakes.cs | 10 +- .../Services/TelemetryServiceFacts.cs | 182 +++++++++++++++++- 15 files changed, 348 insertions(+), 92 deletions(-) create mode 100644 src/NuGetGallery/Services/ITelemetryClient.cs create mode 100644 src/NuGetGallery/Services/TelemetryClientWrapper.cs delete mode 100644 src/NuGetGallery/Telemetry/Telemetry.cs diff --git a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs index ece7e29543..92c72423fc 100644 --- a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs +++ b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs @@ -39,7 +39,12 @@ public class DefaultDependenciesModule : Module [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:CyclomaticComplexity", Justification = "This code is more maintainable in the same function.")] protected override void Load(ContainerBuilder builder) { - var diagnosticsService = new DiagnosticsService(); + var telemetryClient = TelemetryClientWrapper.Instance; + builder.RegisterInstance(telemetryClient) + .As() + .SingleInstance(); + + var diagnosticsService = new DiagnosticsService(telemetryClient); builder.RegisterInstance(diagnosticsService) .AsSelf() .As() @@ -295,7 +300,7 @@ protected override void Load(ContainerBuilder builder) defaultAuditingService = GetAuditingServiceForLocalFileSystem(configuration); break; case StorageType.AzureStorage: - ConfigureForAzureStorage(builder, configuration); + ConfigureForAzureStorage(builder, configuration, telemetryClient); defaultAuditingService = GetAuditingServiceForAzureStorage(builder, configuration); break; } @@ -502,7 +507,7 @@ private static IAuditingService GetAuditingServiceForLocalFileSystem(IGalleryCon return new FileSystemAuditingService(auditingPath, AuditActor.GetAspNetOnBehalfOfAsync); } - private static void ConfigureForAzureStorage(ContainerBuilder builder, IGalleryConfigurationService configuration) + private static void ConfigureForAzureStorage(ContainerBuilder builder, IGalleryConfigurationService configuration, ITelemetryClient telemetryClient) { /// The goal here is to initialize a and /// instance for each unique connection string. Each dependent of (that @@ -563,7 +568,11 @@ private static void ConfigureForAzureStorage(ContainerBuilder builder, IGalleryC .SingleInstance(); // when running on Windows Azure, download counts come from the downloads.v1.json blob - var downloadCountService = new CloudDownloadCountService(configuration.Current.AzureStorage_Statistics_ConnectionString, configuration.Current.AzureStorageReadAccessGeoRedundant); + var downloadCountService = new CloudDownloadCountService( + telemetryClient, + configuration.Current.AzureStorage_Statistics_ConnectionString, + configuration.Current.AzureStorageReadAccessGeoRedundant); + builder.RegisterInstance(downloadCountService) .AsSelf() .As() diff --git a/src/NuGetGallery/App_Start/OwinStartup.cs b/src/NuGetGallery/App_Start/OwinStartup.cs index 882254beda..15edafcc60 100644 --- a/src/NuGetGallery/App_Start/OwinStartup.cs +++ b/src/NuGetGallery/App_Start/OwinStartup.cs @@ -78,9 +78,10 @@ public static void Configuration(IAppBuilder app) return processor; }); - + + var telemetry = dependencyResolver.GetService(); telemetryProcessorChainBuilder.Use( - next => new ExceptionTelemetryProcessor(next, Telemetry.TelemetryClient)); + next => new ExceptionTelemetryProcessor(next, telemetry.UnderlyingClient)); // Note: sampling rate must be a factor 100/N where N is a whole number // e.g.: 50 (= 100/2), 33.33 (= 100/3), 25 (= 100/4), ... diff --git a/src/NuGetGallery/Diagnostics/DiagnosticsService.cs b/src/NuGetGallery/Diagnostics/DiagnosticsService.cs index 0784afbf13..06029ed49a 100644 --- a/src/NuGetGallery/Diagnostics/DiagnosticsService.cs +++ b/src/NuGetGallery/Diagnostics/DiagnosticsService.cs @@ -8,11 +8,20 @@ namespace NuGetGallery.Diagnostics { public class DiagnosticsService : IDiagnosticsService { - public DiagnosticsService() + private readonly ITelemetryClient _telemetryClient; + + public DiagnosticsService(ITelemetryClient telemetryClient) { + _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); + Trace.AutoFlush = true; } + // Test constructor + internal DiagnosticsService() : this(TelemetryClientWrapper.Instance) + { + } + public IDiagnosticsSource GetSource(string name) { if (String.IsNullOrEmpty(name)) @@ -20,7 +29,7 @@ public IDiagnosticsSource GetSource(string name) throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, Strings.ParameterCannotBeNullOrEmpty, "name"), nameof(name)); } - return new TraceDiagnosticsSource(name); + return new TraceDiagnosticsSource(name, _telemetryClient); } } } \ No newline at end of file diff --git a/src/NuGetGallery/Diagnostics/TraceDiagnosticsSource.cs b/src/NuGetGallery/Diagnostics/TraceDiagnosticsSource.cs index 62ea746a44..afd811647c 100644 --- a/src/NuGetGallery/Diagnostics/TraceDiagnosticsSource.cs +++ b/src/NuGetGallery/Diagnostics/TraceDiagnosticsSource.cs @@ -18,11 +18,14 @@ public class TraceDiagnosticsSource : IDiagnosticsSource, IDisposable { private const string ObjectName = "TraceDiagnosticsSource"; private TraceSource _source; + private ITelemetryClient _telemetryClient; public string Name { get; private set; } - public TraceDiagnosticsSource(string name) + public TraceDiagnosticsSource(string name, ITelemetryClient telemetryClient) { + _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); + Name = name; _source = new TraceSource(name, SourceLevels.All); @@ -43,7 +46,7 @@ public virtual void ExceptionEvent(Exception exception) throw new ArgumentNullException(nameof(exception)); } - Telemetry.TrackException(exception); + _telemetryClient.TrackException(exception); } /// diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index da11128f26..9277eabb8d 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -785,6 +785,7 @@ + @@ -869,6 +870,7 @@ + @@ -986,7 +988,6 @@ - diff --git a/src/NuGetGallery/OData/QueryAllowed/ODataQueryVerifier.cs b/src/NuGetGallery/OData/QueryAllowed/ODataQueryVerifier.cs index 7d0aea71a9..29ccf3a9ff 100644 --- a/src/NuGetGallery/OData/QueryAllowed/ODataQueryVerifier.cs +++ b/src/NuGetGallery/OData/QueryAllowed/ODataQueryVerifier.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Web.Http.OData.Query; namespace NuGetGallery.OData.QueryFilter @@ -124,12 +123,10 @@ public static bool AreODataOptionsAllowed(ODataQueryOptions od } catch (Exception ex) { - var telemetryProperties = new Dictionary(); - telemetryProperties.Add(TelemetryService.CallContext, $"{telemetryContext}:{nameof(AreODataOptionsAllowed)}"); - telemetryProperties.Add(TelemetryService.IsEnabled, $"{isFeatureEnabled}"); - - // Log and do not throw - Telemetry.TrackException(ex, telemetryProperties); + _telemetryService.TrackException(ex, properties => { + properties.Add(TelemetryService.CallContext, $"{telemetryContext}:{nameof(AreODataOptionsAllowed)}"); + properties.Add(TelemetryService.IsEnabled, $"{isFeatureEnabled}"); + }); } _telemetryService.TrackODataQueryFilterEvent( diff --git a/src/NuGetGallery/Services/CloudDownloadCountService.cs b/src/NuGetGallery/Services/CloudDownloadCountService.cs index d2b88cf19a..8d019752d0 100644 --- a/src/NuGetGallery/Services/CloudDownloadCountService.cs +++ b/src/NuGetGallery/Services/CloudDownloadCountService.cs @@ -20,6 +20,7 @@ public class CloudDownloadCountService : IDownloadCountService private const string DownloadCountBlobName = "downloads.v1.json"; private const string TelemetryOriginForRefreshMethod = "CloudDownloadCountService.Refresh"; + private readonly ITelemetryClient _telemetryClient; private readonly string _connectionString; private readonly bool _readAccessGeoRedundant; @@ -30,8 +31,10 @@ public class CloudDownloadCountService : IDownloadCountService public DateTime LastRefresh { get; protected set; } - public CloudDownloadCountService(string connectionString, bool readAccessGeoRedundant) + public CloudDownloadCountService(ITelemetryClient telemetryClient, string connectionString, bool readAccessGeoRedundant) { + _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); + _connectionString = connectionString; _readAccessGeoRedundant = readAccessGeoRedundant; } @@ -181,7 +184,7 @@ private void RefreshCore() } catch (JsonReaderException ex) { - Telemetry.TrackException(ex, new Dictionary + _telemetryClient.TrackException(ex, new Dictionary { { "Origin", TelemetryOriginForRefreshMethod }, { "AdditionalInfo", "Invalid entry found in downloads.v1.json." } @@ -191,7 +194,7 @@ private void RefreshCore() } catch (JsonReaderException ex) { - Telemetry.TrackException(ex, new Dictionary + _telemetryClient.TrackException(ex, new Dictionary { { "Origin", TelemetryOriginForRefreshMethod }, { "AdditionalInfo", "Data present in downloads.v1.json is invalid. Couldn't get download data." } @@ -201,7 +204,7 @@ private void RefreshCore() } catch (Exception ex) { - Telemetry.TrackException(ex, new Dictionary + _telemetryClient.TrackException(ex, new Dictionary { { "Origin", TelemetryOriginForRefreshMethod }, { "AdditionalInfo", "Unknown exception." } diff --git a/src/NuGetGallery/Services/ITelemetryClient.cs b/src/NuGetGallery/Services/ITelemetryClient.cs new file mode 100644 index 0000000000..8a88ad5536 --- /dev/null +++ b/src/NuGetGallery/Services/ITelemetryClient.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace NuGetGallery +{ + /// + /// Interface for the Application Insights TelemetryClient class, for unit tests. + /// + public interface ITelemetryClient + { + void TrackEvent(string eventName, IDictionary properties = null, IDictionary metrics = null); + + void TrackException(Exception exception, IDictionary properties = null, IDictionary metrics = null); + } +} diff --git a/src/NuGetGallery/Services/ITelemetryService.cs b/src/NuGetGallery/Services/ITelemetryService.cs index 77164b1d0a..d2846bf29a 100644 --- a/src/NuGetGallery/Services/ITelemetryService.cs +++ b/src/NuGetGallery/Services/ITelemetryService.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Security.Principal; namespace NuGetGallery @@ -18,6 +19,14 @@ public interface ITelemetryService void TrackVerifyPackageKeyEvent(string packageId, string packageVersion, User user, IIdentity identity, int statusCode); + /// + /// Create a trace for an exception. These are informational for support requests. + /// void TraceException(Exception exception); + + /// + /// Create a log for an exception. These are warnings for live site. + /// + void TrackException(Exception exception, Action> addProperties); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/TelemetryClientWrapper.cs b/src/NuGetGallery/Services/TelemetryClientWrapper.cs new file mode 100644 index 0000000000..ebc6822b30 --- /dev/null +++ b/src/NuGetGallery/Services/TelemetryClientWrapper.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.ApplicationInsights; + +namespace NuGetGallery +{ + /// + /// Wrapper for the Application Insights TelemetryClient class. + /// + public class TelemetryClientWrapper : ITelemetryClient + { + private static Lazy Singleton = new Lazy(() => new TelemetryClientWrapper()); + + internal static TelemetryClientWrapper Instance + { + get + { + return Singleton.Value; + } + } + + private TelemetryClientWrapper() + { + UnderlyingClient = new TelemetryClient(); + } + + internal TelemetryClient UnderlyingClient { get; } + + public void TrackEvent(string eventName, IDictionary properties = null, IDictionary metrics = null) + { + try + { + UnderlyingClient.TrackEvent(eventName, properties, metrics); + } + catch + { + // logging failed, don't allow exception to escape + } + } + + public void TrackException(Exception exception, IDictionary properties = null, IDictionary metrics = null) + { + try + { + UnderlyingClient.TrackException(exception, properties, metrics); + } + catch + { + // logging failed, don't allow exception to escape + } + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/TelemetryService.cs b/src/NuGetGallery/Services/TelemetryService.cs index f75436dfda..c6eb46430e 100644 --- a/src/NuGetGallery/Services/TelemetryService.cs +++ b/src/NuGetGallery/Services/TelemetryService.cs @@ -5,20 +5,24 @@ using System.Collections.Generic; using System.Security.Principal; using System.Web; +using Elmah; using NuGetGallery.Diagnostics; namespace NuGetGallery { public class TelemetryService : ITelemetryService { - private IDiagnosticsSource _trace; + internal class Events + { + public const string ODataQueryFilter = "ODataQueryFilter"; + public const string PackagePush = "PackagePush"; + public const string CreatePackageVerificationKey = "CreatePackageVerificationKey"; + public const string VerifyPackageKey = "VerifyPackageKey"; + public const string PackageReadMeChanged = "PackageReadMeChanged"; + } - // Event types - public const string ODataQueryFilterEvent = "ODataQueryFilter"; - public const string PackagePushEvent = "PackagePush"; - public const string CreatePackageVerificationKeyEvent = "CreatePackageVerificationKeyEvent"; - public const string VerifyPackageKeyEvent = "VerifyPackageKeyEvent"; - public const string PackageReadMeChangeEvent = "PackageReadMeChanged"; + private IDiagnosticsSource _diagnosticsSource; + private ITelemetryClient _telemetryClient; // ODataQueryFilter properties public const string CallContext = "CallContext"; @@ -45,18 +49,20 @@ public class TelemetryService : ITelemetryService public const string ReadMeSourceType = "ReadMeSourceType"; public const string ReadMeState = "ReadMeState"; - public TelemetryService(IDiagnosticsService diagnosticsService) + public TelemetryService(IDiagnosticsService diagnosticsService, ITelemetryClient telemetryClient = null) { if (diagnosticsService == null) { throw new ArgumentNullException(nameof(diagnosticsService)); } - _trace = diagnosticsService.GetSource("TelemetryService"); + _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); + + _diagnosticsSource = diagnosticsService.GetSource("TelemetryService"); } // Used by ODataQueryVerifier. Should consider refactoring to make this non-static. - internal TelemetryService() : this(new DiagnosticsService()) + internal TelemetryService() : this(new DiagnosticsService(), TelemetryClientWrapper.Instance) { } @@ -67,12 +73,12 @@ public void TraceException(Exception exception) throw new ArgumentNullException(nameof(exception)); } - _trace.Warning(exception.ToString()); + _diagnosticsSource.Warning(exception.ToString()); } public void TrackODataQueryFilterEvent(string callContext, bool isEnabled, bool isAllowed, string queryPattern) { - TrackEvent(ODataQueryFilterEvent, properties => + TrackEvent(Events.ODataQueryFilter, properties => { properties.Add(CallContext, callContext); properties.Add(IsEnabled, $"{isEnabled}"); @@ -93,8 +99,8 @@ public void TrackPackageReadMeChangeEvent(Package package, string readMeSourceTy { throw new ArgumentNullException(nameof(readMeSourceType)); } - - TrackEvent(PackagePushEvent, properties => { + + TrackEvent(Events.PackageReadMeChanged, properties => { properties.Add(PackageId, package.PackageRegistration.Id); properties.Add(PackageVersion, package.Version); properties.Add(ReadMeSourceType, readMeSourceType); @@ -119,7 +125,7 @@ public void TrackPackagePushEvent(Package package, User user, IIdentity identity throw new ArgumentNullException(nameof(identity)); } - TrackEvent(PackagePushEvent, properties => { + TrackEvent(Events.PackagePush, properties => { properties.Add(ClientVersion, GetClientVersion()); properties.Add(ProtocolVersion, GetProtocolVersion()); properties.Add(ClientInformation, GetClientInformation()); @@ -144,7 +150,7 @@ public void TrackCreatePackageVerificationKeyEvent(string packageId, string pack throw new ArgumentNullException(nameof(identity)); } - TrackEvent(CreatePackageVerificationKeyEvent, properties => { + TrackEvent(Events.CreatePackageVerificationKey, properties => { properties.Add(ClientVersion, GetClientVersion()); properties.Add(ProtocolVersion, GetProtocolVersion()); properties.Add(ClientInformation, GetClientInformation()); @@ -168,7 +174,7 @@ public void TrackVerifyPackageKeyEvent(string packageId, string packageVersion, throw new ArgumentNullException(nameof(identity)); } - TrackEvent(VerifyPackageKeyEvent, properties => + TrackEvent(Events.VerifyPackageKey, properties => { properties.Add(PackageId, packageId); properties.Add(PackageVersion, packageVersion); @@ -210,13 +216,22 @@ private static string GetApiKeyCreationDate(User user, IIdentity identity) return apiKey?.Created.ToString("O") ?? "N/A"; } - private static void TrackEvent(string eventName, Action> addProperties) + protected virtual void TrackEvent(string eventName, Action> addProperties) + { + var telemetryProperties = new Dictionary(); + + addProperties(telemetryProperties); + + _telemetryClient.TrackEvent(eventName, telemetryProperties, metrics: null); + } + + public void TrackException(Exception exception, Action> addProperties) { var telemetryProperties = new Dictionary(); addProperties(telemetryProperties); - Telemetry.TrackEvent(eventName, telemetryProperties, metrics: null); + _telemetryClient.TrackException(exception, telemetryProperties, metrics: null); } } } \ No newline at end of file diff --git a/src/NuGetGallery/Telemetry/QuietLog.cs b/src/NuGetGallery/Telemetry/QuietLog.cs index 0d9f6323af..21885339a1 100644 --- a/src/NuGetGallery/Telemetry/QuietLog.cs +++ b/src/NuGetGallery/Telemetry/QuietLog.cs @@ -10,6 +10,8 @@ namespace NuGetGallery { internal static class QuietLog { + public static ITelemetryClient Telemetry = TelemetryClientWrapper.Instance; + public static void LogHandledException(Exception e) { var aggregateExceptionId = Guid.NewGuid().ToString(); diff --git a/src/NuGetGallery/Telemetry/Telemetry.cs b/src/NuGetGallery/Telemetry/Telemetry.cs deleted file mode 100644 index 08ace7c533..0000000000 --- a/src/NuGetGallery/Telemetry/Telemetry.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.ApplicationInsights; - -namespace NuGetGallery -{ - internal static class Telemetry - { - static Telemetry() - { - TelemetryClient = new TelemetryClient(); - } - - internal static TelemetryClient TelemetryClient { get; } - - public static void TrackEvent(string eventName, IDictionary properties = null, IDictionary metrics = null) - { - try - { - TelemetryClient.TrackEvent(eventName, properties, metrics); - } - catch - { - // logging failed, don't allow exception to escape - } - } - - public static void TrackException(Exception exception, IDictionary properties = null, IDictionary metrics = null) - { - try - { - TelemetryClient.TrackException(exception, properties, metrics); - } - catch - { - // logging failed, don't allow exception to escape - } - } - } -} \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Framework/Fakes.cs b/tests/NuGetGallery.Facts/Framework/Fakes.cs index 09227086ac..55623e8156 100644 --- a/tests/NuGetGallery.Facts/Framework/Fakes.cs +++ b/tests/NuGetGallery.Facts/Framework/Fakes.cs @@ -107,11 +107,11 @@ public Fakes() { Id = "FakePackage", Owners = new List {Owner}, - Packages = new List - { - new Package {Version = "1.0"}, - new Package {Version = "2.0"} - } + }; + Package.Packages = new List + { + new Package { Version = "1.0", PackageRegistration = Package }, + new Package { Version = "2.0", PackageRegistration = Package } }; } diff --git a/tests/NuGetGallery.Facts/Services/TelemetryServiceFacts.cs b/tests/NuGetGallery.Facts/Services/TelemetryServiceFacts.cs index 9d2bef7b95..3dfe81a30e 100644 --- a/tests/NuGetGallery.Facts/Services/TelemetryServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/TelemetryServiceFacts.cs @@ -2,11 +2,16 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Moq; using NuGetGallery.Diagnostics; +using NuGetGallery.Framework; using Xunit; +using TrackAction = System.Action; + namespace NuGetGallery { public class TelemetryServiceFacts @@ -20,6 +25,167 @@ public void ThrowsIfDiagnosticsServiceIsNull() } } + public class TheTrackEventMethod : BaseFacts + { + private static Fakes fakes = new Fakes(); + + public static IEnumerable TrackEventNames_Data + { + get + { + var package = fakes.Package.Packages.First(); + var identity = Fakes.ToIdentity(fakes.User); + + yield return new object[] { "ODataQueryFilter", + (TrackAction)(s => s.TrackODataQueryFilterEvent("callContext", true, true, "queryPattern")) + }; + + yield return new object[] { "PackagePush", + (TrackAction)(s => s.TrackPackagePushEvent(package, fakes.User, identity)) + }; + + yield return new object[] { "CreatePackageVerificationKey", + (TrackAction)(s => s.TrackCreatePackageVerificationKeyEvent(fakes.Package.Id, package.Version, fakes.User, identity)) + }; + + yield return new object[] { "VerifyPackageKey", + (TrackAction)(s => s.TrackVerifyPackageKeyEvent(fakes.Package.Id, package.Version, fakes.User, identity, 0)) + }; + + yield return new object[] { "PackageReadMeChanged", + (TrackAction)(s => s.TrackPackageReadMeChangeEvent(package, "written", PackageEditReadMeState.Changed)) + }; + } + } + + [Fact] + public void TrackEventNamesIncludesAllEvents() + { + var count = typeof(TelemetryService.Events).GetFields().Length; + var testData = TrackEventNames_Data; + + Assert.Equal(count, testData.Count()); + } + + [Theory] + [MemberData(nameof(TrackEventNames_Data))] + public void TrackEventNames(string eventName, TrackAction track) + { + // Arrange + var service = CreateService(); + + // Act + track(service); + + // Assert + service.TelemetryClient.Verify(c => c.TrackEvent(eventName, + It.IsAny>(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public void TrackPackageReadMeChangeEventThrowsIfPackageIsNull() + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => + service.TrackPackageReadMeChangeEvent(null, "written", PackageEditReadMeState.Changed)); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void TrackPackageReadMeChangeEventThrowsIfSourceTypeIsNullOrEmpty(string sourceType) + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => + service.TrackPackageReadMeChangeEvent(fakes.Package.Packages.First(), sourceType, PackageEditReadMeState.Changed)); + } + + [Fact] + public void TrackPackagePushEventThrowsIfPackageIsNull() + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => + service.TrackPackagePushEvent(null, fakes.User, Fakes.ToIdentity(fakes.User))); + } + + [Fact] + public void TrackPackagePushEventThrowsIfUserIsNull() + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => + service.TrackPackagePushEvent(fakes.Package.Packages.First(), null, Fakes.ToIdentity(fakes.User))); + } + + [Fact] + public void TrackPackagePushEventThrowsIfIdentityIsNull() + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => + service.TrackPackagePushEvent(fakes.Package.Packages.First(), fakes.User, null)); + } + + [Fact] + public void TrackCreatePackageVerificationKeyEventThrowsIfUserIsNull() + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => + service.TrackCreatePackageVerificationKeyEvent("id", "1.0.0", null, Fakes.ToIdentity(fakes.User))); + } + + [Fact] + public void TrackCreatePackageVerificationKeyEventThrowsIfIdentityIsNull() + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => + service.TrackCreatePackageVerificationKeyEvent("id", "1.0.0", fakes.User, null)); + } + + [Fact] + public void TrackVerifyPackageKeyEventThrowsIfUserIsNull() + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => + service.TrackVerifyPackageKeyEvent("id", "1.0.0", null, Fakes.ToIdentity(fakes.User), 200)); + } + + [Fact] + public void TrackVerifyPackageKeyEventThrowsIfIdentityIsNull() + { + // Arrange + var service = CreateService(); + + // Act & Assert + Assert.Throws(() => + service.TrackVerifyPackageKeyEvent("id", "1.0.0", fakes.User, null, 200)); + } + } + public class TheTraceExceptionMethod : BaseFacts { [Fact] @@ -58,13 +224,15 @@ public class BaseFacts { public class TelemetryServiceWrapper : TelemetryService { - public TelemetryServiceWrapper(IDiagnosticsService diagnosticsService) - : base(diagnosticsService) + public TelemetryServiceWrapper(IDiagnosticsService diagnosticsService, ITelemetryClient telemetryClient) + : base(diagnosticsService, telemetryClient) { } public Mock TraceSource { get; set; } + public Mock TelemetryClient { get; set; } + public string LastTraceMessage { get; set; } } @@ -72,13 +240,21 @@ public TelemetryServiceWrapper CreateService() { var traceSource = new Mock(); var traceService = new Mock(); + var telemetryClient = new Mock(); traceService.Setup(s => s.GetSource(It.IsAny())) .Returns(traceSource.Object); - var telemetryService = new TelemetryServiceWrapper(traceService.Object); + telemetryClient.Setup(c => c.TrackEvent( + It.IsAny(), + It.IsAny>(), + It.IsAny>())) + .Verifiable(); + + var telemetryService = new TelemetryServiceWrapper(traceService.Object, telemetryClient.Object); telemetryService.TraceSource = traceSource; + telemetryService.TelemetryClient = telemetryClient; traceSource.Setup(t => t.TraceEvent( It.IsAny(), From e359ad6559dd18052f002952be0a669d2536309c Mon Sep 17 00:00:00 2001 From: Svetlana Kofman Date: Fri, 27 Oct 2017 09:58:56 -0700 Subject: [PATCH 09/16] Add support for building signed&unsigned nugetgallery.core packages (#4919) --- build.ps1 | 12 +++++++----- src/NuGetGallery.Core/NuGetGallery.Core.nuspec | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/build.ps1 b/build.ps1 index 87cae72f03..4eab978485 100644 --- a/build.ps1 +++ b/build.ps1 @@ -7,6 +7,7 @@ param ( [switch]$CleanCache, [string]$SimpleVersion = '1.0.0', [string]$SemanticVersion = '1.0.0-zlocal', + [string]$PackageSuffix, [string]$Branch, [string]$CommitSHA, [string]$BuildBranch = '37ff6e758c38b3f513af39f881399ce85f4ff20b' @@ -84,12 +85,13 @@ Invoke-BuildStep 'Building solution' { Build-Solution $Configuration $BuildNumber -MSBuildVersion "15" $SolutionPath -SkipRestore:$SkipRestore -MSBuildProperties "/p:MvcBuildViews=true" ` } ` -ev +BuildErrors - -Invoke-BuildStep 'Creating artifacts' { - New-Package (Join-Path $PSScriptRoot "src\NuGetGallery.Core\NuGetGallery.Core.csproj") -Configuration $Configuration -Symbols -BuildNumber $BuildNumber -Version $SemanticVersion ` - -ev +BuildErrors - } +Invoke-BuildStep 'Creating artifacts' { + $packageId = 'NuGetGallery.Core'+$PackageSuffix + New-Package (Join-Path $PSScriptRoot "src\NuGetGallery.Core\NuGetGallery.Core.csproj") -Configuration $Configuration -Symbols -BuildNumber $BuildNumber -Version $SemanticVersion -PackageId $packageId ` + -ev +BuildErrors + } + Trace-Log ('-' * 60) ## Calculating Build time diff --git a/src/NuGetGallery.Core/NuGetGallery.Core.nuspec b/src/NuGetGallery.Core/NuGetGallery.Core.nuspec index 986fee0669..dfc30613f7 100644 --- a/src/NuGetGallery.Core/NuGetGallery.Core.nuspec +++ b/src/NuGetGallery.Core/NuGetGallery.Core.nuspec @@ -1,7 +1,7 @@ - $id$ + $PackageId$ $version$ $title$ $author$ From 0fca0c62e4c77eb901890afb6f485ba02209b263 Mon Sep 17 00:00:00 2001 From: Andrei Grigorev Date: Fri, 27 Oct 2017 10:56:03 -0700 Subject: [PATCH 10/16] Slightly different way to initialize service bus topic client wrapper (#4917) --- src/NuGetGallery/App_Start/DefaultDependenciesModule.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs index 92c72423fc..95a9a1c0cf 100644 --- a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs +++ b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs @@ -381,10 +381,15 @@ private void RegisterAsynchronousValidation(ContainerBuilder builder, Configurat .RegisterType() .As(); + // we retrieve the values here (on main thread) because otherwise it would run in another thread + // and potentially cause a deadlock on async operation. + var validationConnectionString = configuration.ServiceBus.Validation_ConnectionString; + var validationTopicName = configuration.ServiceBus.Validation_TopicName; + builder .Register(c => new TopicClientWrapper( - configuration.ServiceBus.Validation_ConnectionString, - configuration.ServiceBus.Validation_TopicName)) + validationConnectionString, + validationTopicName)) .As() .SingleInstance() .OnRelease(x => x.Close()); From 0366b89090ce8faa36c19a1c67fd4c6703a4c6de Mon Sep 17 00:00:00 2001 From: Xavier Decoster Date: Fri, 27 Oct 2017 20:22:55 +0200 Subject: [PATCH 11/16] Fix #4814 - do not consider unlisted prerelease versions when showing "newer prerelease available" message. (#4922) --- .../ViewModels/DisplayPackageViewModel.cs | 2 +- .../DisplayPackageViewModelFacts.cs | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs b/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs index 3a355ae1c1..beaaa8d4a7 100644 --- a/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs +++ b/src/NuGetGallery/ViewModels/DisplayPackageViewModel.cs @@ -74,7 +74,7 @@ public bool HasNewerPrerelease get { var latestPrereleaseVersion = PackageVersions - .Where(pv => pv.Prerelease && pv.Available) + .Where(pv => pv.Prerelease && pv.Available && pv.Listed) .Max(pv => pv.NuGetVersion); return latestPrereleaseVersion > NuGetVersion; diff --git a/tests/NuGetGallery.Facts/ViewModels/DisplayPackageViewModelFacts.cs b/tests/NuGetGallery.Facts/ViewModels/DisplayPackageViewModelFacts.cs index 6ea96e00fa..352657b25b 100644 --- a/tests/NuGetGallery.Facts/ViewModels/DisplayPackageViewModelFacts.cs +++ b/tests/NuGetGallery.Facts/ViewModels/DisplayPackageViewModelFacts.cs @@ -199,5 +199,44 @@ public void HasNewerPrereleaseReturnsTrueWhenNewerPrereleaseAvailable( // Assert Assert.Equal(expectedNewerPrereleaseAvailable, hasNewerPrerelease); } + + [Fact] + public void HasNewerPrereleaseDoesNotConsiderUnlistedVersions() + { + // Arrange + var dependencies = Enumerable.Empty().ToList(); + var packageRegistration = new PackageRegistration + { + Owners = Enumerable.Empty().ToList(), + }; + + var package = new Package + { + Dependencies = dependencies, + PackageRegistration = packageRegistration, + IsPrerelease = true, + Version = "1.0.0-alpha.1" + }; + + // This is a newer prerelease version, however unlisted. + var otherPackage = new Package + { + Dependencies = dependencies, + PackageRegistration = packageRegistration, + IsPrerelease = true, + Version = "1.0.0-alpha.2", + Listed = false + }; + + package.PackageRegistration.Packages = new[] { package, otherPackage }; + + var viewModel = new DisplayPackageViewModel(package, package.PackageRegistration.Packages.OrderByDescending(p => new NuGetVersion(p.Version))); + + // Act + var hasNewerPrerelease = viewModel.HasNewerPrerelease; + + // Assert + Assert.False(hasNewerPrerelease); + } } } \ No newline at end of file From e34a93ef9e80e6ba822ccec4e8cd0a0f19ca6a0c Mon Sep 17 00:00:00 2001 From: Scott Bommarito Date: Fri, 27 Oct 2017 15:52:53 -0700 Subject: [PATCH 12/16] Use PackagePermissionsService to determine actions a user can perform on a package (#4923) --- src/NuGetGallery/Controllers/ApiController.cs | 8 +- .../Controllers/JsonApiController.cs | 4 +- .../Controllers/PackagesController.cs | 24 +-- src/NuGetGallery/ExtensionMethods.cs | 36 ---- src/NuGetGallery/NuGetGallery.csproj | 5 +- src/NuGetGallery/Services/PackageAction.cs | 38 ++++ .../Services/PackagePermissionsService.cs | 196 ++++++++++++++++++ src/NuGetGallery/Services/PackageService.cs | 6 +- src/NuGetGallery/Services/PermissionLevel.cs | 33 +++ .../ViewModels/ListPackageItemViewModel.cs | 13 +- .../Views/Packages/DisplayPackage.cshtml | 20 +- .../Views/Shared/_ListPackage.cshtml | 51 +++-- .../Views/Users/_UserPackagesList.cshtml | 27 ++- .../ExtensionMethodsFacts.cs | 1 + .../NuGetGallery.Facts.csproj | 1 + .../PackagePermissionsServiceFacts.cs | 126 +++++++++++ .../TestUtils/TestDataUtility.cs | 12 +- 17 files changed, 488 insertions(+), 113 deletions(-) create mode 100644 src/NuGetGallery/Services/PackageAction.cs create mode 100644 src/NuGetGallery/Services/PackagePermissionsService.cs create mode 100644 src/NuGetGallery/Services/PermissionLevel.cs create mode 100644 tests/NuGetGallery.Facts/Services/PackagePermissionsServiceFacts.cs diff --git a/src/NuGetGallery/Controllers/ApiController.cs b/src/NuGetGallery/Controllers/ApiController.cs index 14e67ee323..f138d916e2 100644 --- a/src/NuGetGallery/Controllers/ApiController.cs +++ b/src/NuGetGallery/Controllers/ApiController.cs @@ -291,7 +291,7 @@ private async Task VerifyPackageKeyInternalAsync(U await AuditingService.SaveAuditRecordAsync( new PackageAuditRecord(package, AuditedPackageAction.Verify)); - if (!package.IsOwner(user)) + if (!PackagePermissionsService.IsActionAllowed(package, user, PackageAction.UploadNewVersion)) { return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); } @@ -428,7 +428,7 @@ private async Task CreatePackageInternal() else { // Is the user allowed to push this Id? - if (!packageRegistration.IsOwner(user)) + if (!PackagePermissionsService.IsActionAllowed(packageRegistration, user, PackageAction.UploadNewVersion)) { // Audit that a non-owner tried to push the package await AuditingService.SaveAuditRecordAsync( @@ -576,7 +576,7 @@ public virtual async Task DeletePackage(string id, string version) } var user = GetCurrentUser(); - if (!package.IsOwner(user)) + if (!PackagePermissionsService.IsActionAllowed(package, user, PackageAction.Unlist)) { return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); } @@ -608,7 +608,7 @@ public virtual async Task PublishPackage(string id, string version } User user = GetCurrentUser(); - if (!package.IsOwner(user)) + if (!PackagePermissionsService.IsActionAllowed(package, user, PackageAction.UploadNewVersion)) { return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, String.Format(CultureInfo.CurrentCulture, Strings.ApiKeyNotAuthorized, "publish")); } diff --git a/src/NuGetGallery/Controllers/JsonApiController.cs b/src/NuGetGallery/Controllers/JsonApiController.cs index df8dd625c4..4f5453c7ae 100644 --- a/src/NuGetGallery/Controllers/JsonApiController.cs +++ b/src/NuGetGallery/Controllers/JsonApiController.cs @@ -50,7 +50,7 @@ public virtual ActionResult GetPackageOwners(string id, string version) return Json(new { message = Strings.AddOwner_PackageNotFound }); } - if (!package.IsOwnerOrAdmin(HttpContext.User)) + if (!PackagePermissionsService.IsActionAllowed(package, HttpContext.User, PackageAction.ManagePackageOwners)) { return new HttpUnauthorizedResult(); } @@ -309,7 +309,7 @@ private bool TryGetManagePackageOwnerModel(string id, string username, out Manag model = new ManagePackageOwnerModel(Strings.AddOwner_PackageNotFound); return false; } - if (!package.IsOwnerOrAdmin(HttpContext.User)) + if (!PackagePermissionsService.IsActionAllowed(package, HttpContext.User, PackageAction.ManagePackageOwners)) { model = new ManagePackageOwnerModel(Strings.AddOwner_NotPackageOwner); return false; diff --git a/src/NuGetGallery/Controllers/PackagesController.cs b/src/NuGetGallery/Controllers/PackagesController.cs index 7b8421059c..cfa3dad277 100644 --- a/src/NuGetGallery/Controllers/PackagesController.cs +++ b/src/NuGetGallery/Controllers/PackagesController.cs @@ -290,7 +290,7 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadFil } // For existing package id verify if it is owned by the current user - if (packageRegistration != null && !packageRegistration.Owners.AnySafe(x => x.Key == currentUser.Key)) + if (packageRegistration != null && !PackagePermissionsService.IsActionAllowed(packageRegistration, currentUser, PackageAction.UploadNewVersion)) { ModelState.AddModelError( string.Empty, string.Format(CultureInfo.CurrentCulture, Strings.PackageIdNotAvailable, packageRegistration.Id)); @@ -392,7 +392,7 @@ public virtual async Task DisplayPackage(string id, string version if (package == null || ((package.PackageStatusKey == PackageStatus.Validating || package.PackageStatusKey == PackageStatus.FailedValidation) - && !package.IsOwnerOrAdmin(User))) + && !PackagePermissionsService.IsActionAllowed(package, User, PackageAction.DisplayPrivatePackage))) { return HttpNotFound(); } @@ -406,7 +406,7 @@ public virtual async Task DisplayPackage(string id, string version var model = new DisplayPackageViewModel(package, packageHistory); var isReadMePending = false; - if (package.IsOwnerOrAdmin(User)) + if (PackagePermissionsService.IsActionAllowed(package, User, PackageAction.Edit)) { // Tell logged-in package owners not to cache the package page, // so they won't be confused about the state of pending edits. @@ -593,7 +593,7 @@ public virtual ActionResult ReportAbuse(string id, string version) var user = GetCurrentUser(); // If user logged on in as owner a different tab, then clicked the link, we can redirect them to ReportMyPackage - if (package.IsOwner(user)) + if (PackagePermissionsService.IsActionAllowed(package, user, PackageAction.ReportMyPackage)) { return RedirectToAction("ReportMyPackage", new { id, version }); } @@ -628,7 +628,7 @@ public virtual ActionResult ReportMyPackage(string id, string version) } // If user hit this url by constructing it manually but is not the owner, redirect them to ReportAbuse - if (!package.IsOwnerOrAdmin(User)) + if (!PackagePermissionsService.IsActionAllowed(package, User, PackageAction.ReportMyPackage)) { return RedirectToAction("ReportAbuse", new { id, version }); } @@ -826,7 +826,7 @@ public virtual ActionResult ManagePackageOwners(string id) { return HttpNotFound(); } - if (!package.IsOwnerOrAdmin(User)) + if (!PackagePermissionsService.IsActionAllowed(package, User, PackageAction.ManagePackageOwners)) { return new HttpStatusCodeResult(401, "Unauthorized"); } @@ -846,7 +846,7 @@ public virtual ActionResult Delete(string id, string version) { return HttpNotFound(); } - if (!package.IsOwnerOrAdmin(User)) + if (!PackagePermissionsService.IsActionAllowed(package, User, PackageAction.Unlist)) { return new HttpStatusCodeResult(401, "Unauthorized"); } @@ -1002,7 +1002,7 @@ public virtual async Task Edit(string id, string version) return Json(404, new[] { string.Format(Strings.PackageWithIdAndVersionNotFound, id, version) }); } - if (!package.IsOwnerOrAdmin(User)) + if (!PackagePermissionsService.IsActionAllowed(package, User, PackageAction.Edit)) { return Json(403, new[] { Strings.Unauthorized }); } @@ -1056,7 +1056,7 @@ public virtual async Task Edit(string id, string version, VerifyPack return Json(404, new[] { string.Format(Strings.PackageWithIdAndVersionNotFound, id, version) }); } - if (!package.IsOwnerOrAdmin(User)) + if (!PackagePermissionsService.IsActionAllowed(package, User, PackageAction.Edit)) { return Json(403, new[] { Strings.Unauthorized }); } @@ -1136,7 +1136,7 @@ private async Task HandleOwnershipRequest(string id, string userna } var user = GetCurrentUser(); - if (package.IsOwner(user)) + if (PackagePermissionsService.GetPermissionLevels(package, user).Contains(PermissionLevel.Owner)) { return View("ConfirmOwner", new PackageOwnerConfirmationModel(id, username, ConfirmOwnershipResult.AlreadyOwner)); } @@ -1311,7 +1311,7 @@ internal virtual async Task Edit(string id, string version, bool? { return HttpNotFound(); } - if (!package.IsOwnerOrAdmin(User)) + if (!PackagePermissionsService.IsActionAllowed(package, User, PackageAction.Edit)) { return new HttpStatusCodeResult(401, "Unauthorized"); } @@ -1584,7 +1584,7 @@ internal virtual async Task SetLicenseReportVisibility(string id, { return HttpNotFound(); } - if (!package.IsOwnerOrAdmin(User)) + if (!PackagePermissionsService.IsActionAllowed(package, User, PackageAction.Edit)) { return new HttpStatusCodeResult(401, "Unauthorized"); } diff --git a/src/NuGetGallery/ExtensionMethods.cs b/src/NuGetGallery/ExtensionMethods.cs index 3fdadeb716..61f45194b0 100644 --- a/src/NuGetGallery/ExtensionMethods.cs +++ b/src/NuGetGallery/ExtensionMethods.cs @@ -176,42 +176,6 @@ public static bool AnySafe(this IEnumerable items, Func predicate return items.Any(predicate); } - public static bool IsOwnerOrAdmin(this Package package, IPrincipal user) - { - return package.PackageRegistration.IsOwnerOrAdmin(user); - } - - public static bool IsOwner(this Package package, User user) - { - return package.PackageRegistration.IsOwner(user); - } - - public static bool IsOwnerOrAdmin(this PackageRegistration package, IPrincipal user) - { - if (package == null) - { - throw new ArgumentNullException(nameof(package)); - } - if (user == null || user.Identity == null) - { - return false; - } - return user.IsAdministrator() || package.Owners.Any(u => u.Username == user.Identity.Name); - } - - public static bool IsOwner(this PackageRegistration package, User user) - { - if (package == null) - { - throw new ArgumentNullException(nameof(package)); - } - if (user == null) - { - return false; - } - return package.Owners.Any(u => u.Key == user.Key); - } - // apple polish! public static string CardinalityLabel(this int count, string singular, string plural) { diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index 9277eabb8d..42fb211e17 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -785,10 +785,11 @@ - + + @@ -868,6 +869,8 @@ + + diff --git a/src/NuGetGallery/Services/PackageAction.cs b/src/NuGetGallery/Services/PackageAction.cs new file mode 100644 index 0000000000..1066b94b9b --- /dev/null +++ b/src/NuGetGallery/Services/PackageAction.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace NuGetGallery +{ + public enum PackageAction + { + /// + /// The ability to view hidden package versions or metadata. + /// + DisplayPrivatePackage, + + /// + /// The ability to upload new versions of an existing package ID. + /// + UploadNewVersion, + + /// + /// The ability to edit an existing package version. + /// + Edit, + + /// + /// The ability to unlist or relist an existing package version. + /// + Unlist, + + /// + /// The ability to add or remove owners of the package. + /// + ManagePackageOwners, + + /// + /// The ability to report a package as its owner. + /// + ReportMyPackage, + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/PackagePermissionsService.cs b/src/NuGetGallery/Services/PackagePermissionsService.cs new file mode 100644 index 0000000000..b07e3402b7 --- /dev/null +++ b/src/NuGetGallery/Services/PackagePermissionsService.cs @@ -0,0 +1,196 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; + +namespace NuGetGallery +{ + public static class PackagePermissionsService + { + private static readonly IDictionary> _allowedActions = + new Dictionary> + { + { + PermissionLevel.Anonymous, + new PackageAction[0] + }, + { + PermissionLevel.OrganizationCollaborator, + new [] + { + PackageAction.DisplayPrivatePackage, + PackageAction.UploadNewVersion, + PackageAction.Edit, + PackageAction.Unlist, + } + }, + { + PermissionLevel.SiteAdmin, + new [] + { + PackageAction.DisplayPrivatePackage, + PackageAction.UploadNewVersion, + PackageAction.Edit, + PackageAction.Unlist, + PackageAction.ManagePackageOwners, + } + }, + { + PermissionLevel.OrganizationAdmin, + new [] + { + PackageAction.DisplayPrivatePackage, + PackageAction.UploadNewVersion, + PackageAction.Edit, + PackageAction.Unlist, + PackageAction.ManagePackageOwners, + PackageAction.ReportMyPackage, + } + }, + { + PermissionLevel.Owner, + new [] + { + PackageAction.DisplayPrivatePackage, + PackageAction.UploadNewVersion, + PackageAction.Edit, + PackageAction.Unlist, + PackageAction.ManagePackageOwners, + PackageAction.ReportMyPackage, + } + } + }; + + public static bool IsActionAllowed(Package package, IPrincipal principal, PackageAction action) + { + return IsActionAllowed(package.PackageRegistration, principal, action); + } + + public static bool IsActionAllowed(PackageRegistration packageRegistration, IPrincipal principal, PackageAction action) + { + return IsActionAllowed(packageRegistration.Owners, principal, action); + } + + public static bool IsActionAllowed(IEnumerable owners, IPrincipal principal, PackageAction action) + { + var permissionLevels = GetPermissionLevels(owners, principal); + return IsActionAllowed(permissionLevels, action); + } + + public static bool IsActionAllowed(Package package, User user, PackageAction action) + { + return IsActionAllowed(package.PackageRegistration, user, action); + } + + public static bool IsActionAllowed(PackageRegistration packageRegistration, User user, PackageAction action) + { + return IsActionAllowed(packageRegistration.Owners, user, action); + } + + public static bool IsActionAllowed(IEnumerable owners, User user, PackageAction action) + { + var permissionLevels = GetPermissionLevels(owners, user); + return IsActionAllowed(permissionLevels, action); + } + + private static bool IsActionAllowed(IEnumerable permissionLevels, PackageAction action) + { + return permissionLevels.Any(permissionLevel => _allowedActions[permissionLevel].Contains(action)); + } + + public static IEnumerable GetPermissionLevels(Package package, User user) + { + return GetPermissionLevels(package, user); + } + + public static IEnumerable GetPermissionLevels(PackageRegistration packageRegistration, User user) + { + return GetPermissionLevels(packageRegistration.Owners, user); + } + + public static IEnumerable GetPermissionLevels(IEnumerable owners, User user) + { + if (user == null) + { + return new[] { PermissionLevel.Anonymous }; + } + + return GetPermissionLevels( + owners, + user.IsInRole(Constants.AdminRoleName), + u => UserMatchesUser(u, user)); + } + + public static IEnumerable GetPermissionLevels(Package package, IPrincipal principal) + { + return GetPermissionLevels(package, principal); + } + + public static IEnumerable GetPermissionLevels(PackageRegistration packageRegistration, IPrincipal principal) + { + return GetPermissionLevels(packageRegistration.Owners, principal); + } + + public static IEnumerable GetPermissionLevels(IEnumerable owners, IPrincipal principal) + { + if (principal == null) + { + return new[] { PermissionLevel.Anonymous }; + } + + return GetPermissionLevels( + owners, + principal.IsAdministrator(), + u => UserMatchesPrincipal(u, principal)); + } + + private static IEnumerable GetPermissionLevels(IEnumerable owners, bool isUserAdmin, Func isUserMatch) + { + if (owners == null) + { + yield return PermissionLevel.Anonymous; + yield break; + } + + if (isUserAdmin) + { + yield return PermissionLevel.SiteAdmin; + } + + if (owners.Any(isUserMatch)) + { + yield return PermissionLevel.Owner; + } + + var matchingMembers = owners + .Where(u => u.Organization != null) + .SelectMany(u => u.Organization.Memberships) + .Where(m => isUserMatch(m.Member)); + + if (matchingMembers.Any(m => m.IsAdmin)) + { + yield return PermissionLevel.OrganizationAdmin; + } + + if (matchingMembers.Any()) + { + yield return PermissionLevel.OrganizationCollaborator; + } + + yield return PermissionLevel.Anonymous; + } + + private static bool UserMatchesPrincipal(User user, IPrincipal principal) + { + return user.Username == principal.Identity.Name; + } + + private static bool UserMatchesUser(User first, User second) + { + return first.Key == second.Key; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/PackageService.cs b/src/NuGetGallery/Services/PackageService.cs index c251dae0a3..f371759ca6 100644 --- a/src/NuGetGallery/Services/PackageService.cs +++ b/src/NuGetGallery/Services/PackageService.cs @@ -259,7 +259,7 @@ private void MergeLatestPackagesByOwner(User user, bool includeUnlisted, Diction if (includeUnlisted) { latestPackageVersions = _packageRegistrationRepository.GetAll() - .Where(pr => pr.Owners.Where(owner => owner.Username == user.Username).Any()) + .Where(pr => pr.Owners.Any(owner => owner.Key == user.Key)) .Select(pr => pr.Packages.OrderByDescending(p => p.Version).FirstOrDefault()) .Where(p => p != null) .Include(p => p.PackageRegistration) @@ -305,7 +305,7 @@ private void MergeLatestPackagesByOwner(User user, bool includeUnlisted, Diction public IEnumerable FindPackageRegistrationsByOwner(User user) { - return _packageRegistrationRepository.GetAll().Where(p => p.Owners.Any(o => o.Username == user.Username)); + return _packageRegistrationRepository.GetAll().Where(p => p.Owners.Any(o => o.Key == user.Key)); } public IEnumerable FindDependentPackages(Package package) @@ -440,7 +440,7 @@ private PackageRegistration CreateOrGetPackageRegistration(User currentUser, Pac { var packageRegistration = FindPackageRegistrationById(packageMetadata.Id); - if (packageRegistration != null && !packageRegistration.Owners.Contains(currentUser)) + if (packageRegistration != null && !PackagePermissionsService.IsActionAllowed(packageRegistration, currentUser, PackageAction.UploadNewVersion)) { throw new EntityException(Strings.PackageIdNotAvailable, packageMetadata.Id); } diff --git a/src/NuGetGallery/Services/PermissionLevel.cs b/src/NuGetGallery/Services/PermissionLevel.cs new file mode 100644 index 0000000000..3c28b9805a --- /dev/null +++ b/src/NuGetGallery/Services/PermissionLevel.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace NuGetGallery +{ + public enum PermissionLevel + { + /// + /// The default rights to a package, held by all users on every package. + /// + Anonymous, + + /// + /// The user is a direct owner of the package. + /// + Owner, + + /// + /// The user is a site admin and has administrative permissions on all packages. + /// + SiteAdmin, + + /// + /// The user is an administrator of an organization that is a direct owner of the package. + /// + OrganizationAdmin, + + /// + /// The user is a collaborator of an organization that is a direct owner of the package. + /// + OrganizationCollaborator, + } +} \ No newline at end of file diff --git a/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs b/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs index b99c866bc3..191eefbfe1 100644 --- a/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs +++ b/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs @@ -49,18 +49,9 @@ public bool UseVersion } } - public bool IsOwner(IPrincipal user) + public bool HasPermission(IPrincipal principal, PackageAction permission) { - if (user == null || user.Identity == null) - { - return false; - } - return Owners.Any(u => u.Username == user.Identity.Name); - } - - public bool IsOwnerOrAdmin(IPrincipal user) - { - return IsOwner(user) || user.IsAdministrator(); + return PackagePermissionsService.IsActionAllowed(Owners, principal, permission); } } } \ No newline at end of file diff --git a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml index 830d1417bc..d828a1beca 100644 --- a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml @@ -211,7 +211,7 @@ @if (!Model.Listed && Model.Available) { - if ((Model.IsOwnerOrAdmin(User))) + if (Model.HasPermission(User, PackageAction.DisplayPrivatePackage)) { @ViewHelpers.AlertWarning( @ @@ -298,7 +298,7 @@ @if (Model.Dependencies.DependencySets == null) { - if ((Model.IsOwnerOrAdmin(User))) + if (Model.HasPermission(User, PackageAction.DisplayPrivatePackage)) {

Dependencies

@@ -374,7 +374,7 @@ Version Downloads Last updated - @if (Model.IsOwnerOrAdmin(User)) + @if (Model.HasPermission(User, PackageAction.DisplayPrivatePackage)) { Listed if (Config.Current.AsynchronousPackageValidationEnabled) @@ -395,7 +395,7 @@ @foreach (var packageVersion in Model.PackageVersions) { if ((packageVersion.Available && packageVersion.Listed) - || (!packageVersion.Deleted && Model.IsOwnerOrAdmin(User))) + || (!packageVersion.Deleted && Model.HasPermission(User, PackageAction.DisplayPrivatePackage))) { rowCount++; @VersionListDivider(rowCount, versionsExpanded) @@ -426,7 +426,7 @@ @packageVersion.LastUpdated.ToNuGetShortDateString() - @if (packageVersion.IsOwnerOrAdmin(User)) + @if (packageVersion.HasPermission(User, PackageAction.DisplayPrivatePackage)) { @(packageVersion.Listed ? "yes" : "no") @@ -451,7 +451,7 @@ } } - else if (packageVersion.Deleted && packageVersion.IsOwnerOrAdmin(User)) + else if (packageVersion.Deleted && packageVersion.HasPermission(User, PackageAction.DisplayPrivatePackage)) { rowCount++; @VersionListDivider(rowCount, versionsExpanded) @@ -528,7 +528,7 @@ Contact owners - @if (Model.IsOwnerOrAdmin(User)) + @if (Model.HasPermission(User, PackageAction.ReportMyPackage)) {

  • @@ -559,21 +559,21 @@
  • } - @if ((Model.Available || Model.Validating) && Model.IsOwnerOrAdmin(User)) + @if ((Model.Available || Model.Validating) && Model.HasPermission(User, PackageAction.Edit)) {
  • Edit package
  • } - @if (Model.IsOwnerOrAdmin(User)) + @if (Model.HasPermission(User, PackageAction.ManagePackageOwners)) {
  • Manage owners
  • } - @if (!Model.Deleted && Model.IsOwnerOrAdmin(User)) + @if (!Model.Deleted && Model.HasPermission(User, PackageAction.Unlist)) {
  • diff --git a/src/NuGetGallery/Views/Shared/_ListPackage.cshtml b/src/NuGetGallery/Views/Shared/_ListPackage.cshtml index 7a896fc671..b60ba1b2d6 100644 --- a/src/NuGetGallery/Views/Shared/_ListPackage.cshtml +++ b/src/NuGetGallery/Views/Shared/_ListPackage.cshtml @@ -76,28 +76,39 @@ @Html.RouteLink("More information", RouteName.DisplayPackage, new { Model.Id, Model.Version }) } - - @if (Model.IsOwner(User)) + + @if (Model.HasPermission(User, Permission.Edit) || + Model.HasPermission(User, Permission.ManagePackageOwners) || + Model.HasPermission(User, Permission.Delete)) { } diff --git a/src/NuGetGallery/Views/Users/_UserPackagesList.cshtml b/src/NuGetGallery/Views/Users/_UserPackagesList.cshtml index 7a794386b8..d6061fee17 100644 --- a/src/NuGetGallery/Views/Users/_UserPackagesList.cshtml +++ b/src/NuGetGallery/Views/Users/_UserPackagesList.cshtml @@ -51,15 +51,24 @@ @package.TotalDownloadCount.ToNuGetNumberString() @package.FullVersion.Abbreviate(25) - - - - - - - - - + @if (package.HasPermission(User, PackageAction.Edit)) + { + + + + } + @if (package.HasPermission(User, PackageAction.ManagePackageOwners)) + { + + + + } + @if (package.HasPermission(User, PackageAction.Unlist)) + { + + + + } } diff --git a/tests/NuGetGallery.Facts/ExtensionMethodsFacts.cs b/tests/NuGetGallery.Facts/ExtensionMethodsFacts.cs index 290f14280b..407414b3ff 100644 --- a/tests/NuGetGallery.Facts/ExtensionMethodsFacts.cs +++ b/tests/NuGetGallery.Facts/ExtensionMethodsFacts.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System.Security.Claims; using NuGet.Frameworks; using NuGetGallery.Authentication; diff --git a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj index 2b378705d2..8452215e0d 100644 --- a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj +++ b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj @@ -432,6 +432,7 @@ + diff --git a/tests/NuGetGallery.Facts/Services/PackagePermissionsServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackagePermissionsServiceFacts.cs new file mode 100644 index 0000000000..fd92adcde5 --- /dev/null +++ b/tests/NuGetGallery.Facts/Services/PackagePermissionsServiceFacts.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using Moq; +using Xunit; + +namespace NuGetGallery.Services +{ + public class PackagePermissionsServiceFacts + { + public class TheGetPermissionLevelMethod + { + private int _key = 0; + + [Flags] + public enum ReturnsExpectedPermissionLevels_State + { + IsOwner = 1, + IsSiteAdmin = 2, + IsOrganizationAdmin = 4, + IsOrganizationCollaborator = 8, + } + + private static readonly IEnumerable _stateValues = + Enum.GetValues(typeof(ReturnsExpectedPermissionLevels_State)).Cast(); + + public static IEnumerable ReturnsExpectedPermissionLevels_Data + { + get + { + for (int i = 0; i < Enum.GetValues(typeof(ReturnsExpectedPermissionLevels_State)).Cast().Max() * 2; i++) + { + yield return new object[] + { + _stateValues.Where(s => Includes(i, s)) + }; + } + } + } + + private static bool Includes(int i, ReturnsExpectedPermissionLevels_State state) + { + return (i & (int)state) == 0; + } + + [Theory] + [MemberData(nameof(ReturnsExpectedPermissionLevels_Data))] + public void ReturnsExpectedPermissionLevels(IEnumerable states) + { + // Arrange + var expectedPermissionLevels = new List() { PermissionLevel.Anonymous }; + + var owners = new List(); + + var user = new User("testuser") { Key = _key++ }; + + if (states.Contains(ReturnsExpectedPermissionLevels_State.IsOwner)) + { + owners.Add(user); + expectedPermissionLevels.Add(PermissionLevel.Owner); + } + + if (states.Contains(ReturnsExpectedPermissionLevels_State.IsSiteAdmin)) + { + user.Roles.Add(new Role { Name = Constants.AdminRoleName }); + expectedPermissionLevels.Add(PermissionLevel.SiteAdmin); + } + + if (states.Contains(ReturnsExpectedPermissionLevels_State.IsOrganizationAdmin)) + { + CreateOrganizationOwnerAndAddUserAsMember(owners, user, true); + expectedPermissionLevels.Add(PermissionLevel.OrganizationAdmin); + } + + if (states.Contains(ReturnsExpectedPermissionLevels_State.IsOrganizationCollaborator)) + { + CreateOrganizationOwnerAndAddUserAsMember(owners, user, false); + expectedPermissionLevels.Add(PermissionLevel.OrganizationCollaborator); + } + + // Assert + AssertPermissionLevels(owners, user, expectedPermissionLevels); + } + + private void CreateOrganizationOwnerAndAddUserAsMember(List owners, User user, bool isAdmin) + { + var organization = new Organization() { Memberships = new List() }; + var organizationOwner = new User("testorganization") { Key = _key++, Organization = organization }; + owners.Add(organizationOwner); + + var organizationMembership = new Membership() { Organization = organization, Member = user, IsAdmin = isAdmin }; + organization.Memberships.Add(organizationMembership); + } + + private void AssertPermissionLevels(IEnumerable owners, User user, IEnumerable expectedLevels) + { + AssertEqual(expectedLevels, PackagePermissionsService.GetPermissionLevels(owners, user)); + + var principal = GetPrincipal(user); + AssertEqual(expectedLevels, PackagePermissionsService.GetPermissionLevels(owners, principal)); + } + + private void AssertEqual(IEnumerable expected, IEnumerable actual) + { + Assert.True(!expected.Except(actual).Any()); + Assert.True(!actual.Except(expected).Any()); + } + + private IPrincipal GetPrincipal(User u) + { + var identityMock = new Mock(); + identityMock.Setup(x => x.Name).Returns(u.Username); + identityMock.Setup(x => x.IsAuthenticated).Returns(true); + + var principalMock = new Mock(); + principalMock.Setup(x => x.Identity).Returns(identityMock.Object); + principalMock.Setup(x => x.IsInRole(It.IsAny())).Returns(role => u.IsInRole(role)); + return principalMock.Object; + } + } + } +} diff --git a/tests/NuGetGallery.Facts/TestUtils/TestDataUtility.cs b/tests/NuGetGallery.Facts/TestUtils/TestDataUtility.cs index 36c32822f5..50c78f9626 100644 --- a/tests/NuGetGallery.Facts/TestUtils/TestDataUtility.cs +++ b/tests/NuGetGallery.Facts/TestUtils/TestDataUtility.cs @@ -37,12 +37,14 @@ public static IList GetRegistrations() public static IList GetTestUsers() { + var key = 0; + var result = new List(); - result.Add(new User("test1")); - result.Add(new User("test2")); - result.Add(new User("test3")); - result.Add(new User("test4")); - result.Add(new User("test5")); + result.Add(new User("test1") { Key = key++ }); + result.Add(new User("test2") { Key = key++ }); + result.Add(new User("test3") { Key = key++ }); + result.Add(new User("test4") { Key = key++ }); + result.Add(new User("test5") { Key = key++ }); return result; } From 367dc1c530cdc43bed1bc6e4d61338bd89b171fc Mon Sep 17 00:00:00 2001 From: Andrei Grigorev Date: Mon, 30 Oct 2017 11:55:38 -0700 Subject: [PATCH 13/16] Pakage URL generation method and checks whether package file exists (#4918) * Pakage URL generation method and checks whether package file exists for the NuGetGallery.Core (Validation.Orchestrator needs those) * GetPackageUrlAsync -> GetPackageReadUriAsync * Minor test fixes --- .../Services/CorePackageFileService.cs | 18 ++++ .../Services/ICorePackageFileService.cs | 25 ++++++ .../Services/CorePackageFileServiceFacts.cs | 87 ++++++++++++++++++- 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/src/NuGetGallery.Core/Services/CorePackageFileService.cs b/src/NuGetGallery.Core/Services/CorePackageFileService.cs index 59f097c496..c84db2ca4a 100644 --- a/src/NuGetGallery.Core/Services/CorePackageFileService.cs +++ b/src/NuGetGallery.Core/Services/CorePackageFileService.cs @@ -34,6 +34,18 @@ public Task DownloadPackageFileAsync(Package package) return _fileStorageService.GetFileAsync(CoreConstants.PackagesFolderName, fileName); } + public Task GetPackageReadUriAsync(Package package) + { + var fileName = BuildFileName(package, CoreConstants.PackageFileSavePathTemplate, CoreConstants.NuGetPackageFileExtension); + return _fileStorageService.GetFileReadUriAsync(CoreConstants.PackagesFolderName, fileName, endOfAccess: null); + } + + public Task DoesPackageFileExistAsync(Package package) + { + var fileName = BuildFileName(package, CoreConstants.PackageFileSavePathTemplate, CoreConstants.NuGetPackageFileExtension); + return _fileStorageService.FileExistsAsync(CoreConstants.PackagesFolderName, fileName); + } + public Task SaveValidationPackageFileAsync(Package package, Stream packageFile) { if (packageFile == null) @@ -110,6 +122,12 @@ public Task GetValidationPackageReadUriAsync(Package package, DateTimeOffse return _fileStorageService.GetFileReadUriAsync(CoreConstants.ValidationFolderName, fileName, endOfAccess); } + public Task DoesValidationPackageFileExistAsync(Package package) + { + var fileName = BuildFileName(package, CoreConstants.PackageFileSavePathTemplate, CoreConstants.NuGetPackageFileExtension); + return _fileStorageService.FileExistsAsync(CoreConstants.ValidationFolderName, fileName); + } + protected static string BuildFileName(Package package, string format, string extension) { if (package == null) diff --git a/src/NuGetGallery.Core/Services/ICorePackageFileService.cs b/src/NuGetGallery.Core/Services/ICorePackageFileService.cs index 1a5189bff9..f7e8fd17d0 100644 --- a/src/NuGetGallery.Core/Services/ICorePackageFileService.cs +++ b/src/NuGetGallery.Core/Services/ICorePackageFileService.cs @@ -19,6 +19,24 @@ public interface ICorePackageFileService ///
  • Task DownloadPackageFileAsync(Package package); + /// + /// Generates the URL for the specified package in the public container for available packages. + /// + /// The package metadata. + /// Package download URL + /// + /// The returned URL is only intended to be used by the internal tooling and not for the user: + /// it might not make any sense to external users as it can be, for example, a file:/// URL. + /// + Task GetPackageReadUriAsync(Package package); + + /// + /// Checks whether package file exists in the public container for available packages + /// + /// The package metadata + /// True if file exists, false otherwise + Task DoesPackageFileExistAsync(Package package); + /// /// Saves the contents of the package to the private container for packages that are being validated. If the /// file already exists, an exception will be thrown. @@ -42,6 +60,13 @@ public interface ICorePackageFileService /// Time limited (if implementation supports) URI for the validation package Task GetValidationPackageReadUriAsync(Package package, DateTimeOffset endOfAccess); + /// + /// Checks whether package file exists in the private validation container + /// + /// The package metadata + /// True if file exists, false otherwise + Task DoesValidationPackageFileExistAsync(Package package); + /// /// Deletes the validating package from the file storage. If the file does not exist this method will not throw /// any exception. diff --git a/tests/NuGetGallery.Core.Facts/Services/CorePackageFileServiceFacts.cs b/tests/NuGetGallery.Core.Facts/Services/CorePackageFileServiceFacts.cs index 9bcf16eeeb..9035875076 100644 --- a/tests/NuGetGallery.Core.Facts/Services/CorePackageFileServiceFacts.cs +++ b/tests/NuGetGallery.Core.Facts/Services/CorePackageFileServiceFacts.cs @@ -405,10 +405,93 @@ public async Task WillUseTheFileStorageService() _fileStorageService.Verify( x => x.GetFileReadUriAsync(ValidationFolderName, ValidationFileName, endOfAccess), - Times.Once); + Times.Once()); _fileStorageService.Verify( x => x.GetFileReadUriAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Once); + Times.Once()); + } + } + + public class TheGetPackageReadUriMethod : FactsBase + { + [Fact] + public async Task WillThrowIfPackageIsNull() + { + var ex = await Assert.ThrowsAsync(() => _service.GetPackageReadUriAsync(null)); + Assert.Equal("package", ex.ParamName); + } + + [Fact] + public async Task WillUseFileStorageService() + { + await _service.GetPackageReadUriAsync(_package); + + string filename = BuildFileName(_package.PackageRegistration.Id, _package.NormalizedVersion, CoreConstants.NuGetPackageFileExtension, CoreConstants.PackageFileSavePathTemplate); + + _fileStorageService.Verify( + x => x.GetFileReadUriAsync(PackagesFolderName, filename, null), + Times.Once()); + _fileStorageService.Verify( + x => x.GetFileReadUriAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once()); + } + } + + public class TheDoesPackageFileExistMethod : FactsBase + { + [Fact] + public async Task WillThrowIfPackageIsNull() + { + var ex = await Assert.ThrowsAsync(() => _service.DoesPackageFileExistAsync(null)); + Assert.Equal("package", ex.ParamName); + } + + [Fact] + public async Task WillUseFileStorageService() + { + string filename = BuildFileName(_package.PackageRegistration.Id, _package.NormalizedVersion, CoreConstants.NuGetPackageFileExtension, CoreConstants.PackageFileSavePathTemplate); + + _fileStorageService + .Setup(x => x.FileExistsAsync(PackagesFolderName, filename)) + .ReturnsAsync(true); + + var result = await _service.DoesPackageFileExistAsync(_package); + + Assert.True(result); + _fileStorageService.Verify( + x => x.FileExistsAsync(PackagesFolderName, filename), + Times.Once()); + _fileStorageService.Verify( + x => x.FileExistsAsync(It.IsAny(), It.IsAny()), + Times.Once()); + } + } + + public class TheDoesValidationPackageFileExistMethod : FactsBase + { + [Fact] + public async Task WillThrowIfPackageIsNull() + { + var ex = await Assert.ThrowsAsync(() => _service.DoesValidationPackageFileExistAsync(null)); + Assert.Equal("package", ex.ParamName); + } + + [Fact] + public async Task WillUseFileStorageService() + { + _fileStorageService + .Setup(x => x.FileExistsAsync(ValidationFolderName, ValidationFileName)) + .ReturnsAsync(true); + + var result = await _service.DoesValidationPackageFileExistAsync(_package); + + Assert.True(result); + _fileStorageService.Verify( + x => x.FileExistsAsync(ValidationFolderName, ValidationFileName), + Times.Once()); + _fileStorageService.Verify( + x => x.FileExistsAsync(It.IsAny(), It.IsAny()), + Times.Once()); } } From 7c087cf406db59ca097bae6f2b9e6cf3a209740c Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Mon, 30 Oct 2017 14:58:08 -0700 Subject: [PATCH 14/16] Switch Organizations to use TPT inheritance (was fix for Organization-User FK) (#4926) --- .../Entities/EntitiesContext.cs | 34 ++--- .../Entities/IEntitiesContext.cs | 11 +- .../Entities/Organization.cs | 29 ++-- src/NuGetGallery.Core/Entities/User.cs | 22 +-- .../Authentication/AuthenticationService.cs | 8 +- src/NuGetGallery/Extensions/UserExtensions.cs | 10 -- .../201710250430326_AddOrganizations.resx | 126 ------------------ ...201710301857232_Organizations.Designer.cs} | 6 +- ...ns.cs => 201710301857232_Organizations.cs} | 8 +- .../201710301857232_Organizations.resx | 126 ++++++++++++++++++ src/NuGetGallery/NuGetGallery.csproj | 10 +- .../Services/PackagePermissionsService.cs | 5 +- tests/NuGetGallery.Facts/Framework/Fakes.cs | 12 +- .../PackagePermissionsServiceFacts.cs | 17 ++- 14 files changed, 194 insertions(+), 230 deletions(-) delete mode 100644 src/NuGetGallery/Migrations/201710250430326_AddOrganizations.resx rename src/NuGetGallery/Migrations/{201710250430326_AddOrganizations.Designer.cs => 201710301857232_Organizations.Designer.cs} (79%) rename src/NuGetGallery/Migrations/{201710250430326_AddOrganizations.cs => 201710301857232_Organizations.cs} (91%) create mode 100644 src/NuGetGallery/Migrations/201710301857232_Organizations.resx diff --git a/src/NuGetGallery.Core/Entities/EntitiesContext.cs b/src/NuGetGallery.Core/Entities/EntitiesContext.cs index 3a6c8df815..6b19dfe444 100644 --- a/src/NuGetGallery.Core/Entities/EntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/EntitiesContext.cs @@ -49,11 +49,6 @@ public EntitiesContext(string connectionString, bool readOnly) /// public IDbSet Users { get; set; } - /// - /// Organization accounts. - /// - public IDbSet Organizations { get; set; } - IDbSet IEntitiesContext.Set() { return base.Set(); @@ -127,25 +122,24 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) .Map(c => c.ToTable("UserRoles") .MapLeftKey("UserKey") .MapRightKey("RoleKey")); - + modelBuilder.Entity() - .HasKey(o => o.Key) - .HasRequired(o => o.Account) - .WithOptional(u => u.Organization) - .WillCascadeOnDelete(false); // Disabled to prevent multiple cascade paths. + .ToTable("Organizations"); modelBuilder.Entity() - .HasKey(m => new { m.OrganizationKey, m.MemberKey }) - .HasRequired(m => m.Organization) - .WithMany(o => o.Memberships) - .HasForeignKey(m => m.OrganizationKey) - .WillCascadeOnDelete(true); // Memberships will be deleted with the Organization. - - modelBuilder.Entity() - .HasRequired(m => m.Member) - .WithMany(u => u.Memberships) + .HasKey(m => new { m.OrganizationKey, m.MemberKey }); + + modelBuilder.Entity() + .HasMany(u => u.Organizations) + .WithRequired(m => m.Member) .HasForeignKey(m => m.MemberKey) - .WillCascadeOnDelete(true); // Memberships will be deleted with the User (Member). + .WillCascadeOnDelete(true); // Membership will be deleted with the Member account. + + modelBuilder.Entity() + .HasMany(o => o.Members) + .WithRequired(m => m.Organization) + .HasForeignKey(m => m.OrganizationKey) + .WillCascadeOnDelete(true); // Memberships will be deleted with the Organization account. modelBuilder.Entity() .HasKey(u => u.Key); diff --git a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs index e523488caa..6f85ac73e9 100644 --- a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs @@ -13,19 +13,10 @@ public interface IEntitiesContext IDbSet PackageRegistrations { get; set; } IDbSet Credentials { get; set; } IDbSet Scopes { get; set; } + IDbSet Users { get; set; } IDbSet UserSecurityPolicies { get; set; } IDbSet ReservedNamespaces { get; set; } - /// - /// User or organization accounts. - /// - IDbSet Users { get; set; } - - /// - /// Organization accounts. - /// - IDbSet Organizations { get; set; } - Task SaveChangesAsync(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Set", Justification="This is to match the EF terminology.")] IDbSet Set() where T : class; diff --git a/src/NuGetGallery.Core/Entities/Organization.cs b/src/NuGetGallery.Core/Entities/Organization.cs index f8f00d4e6d..03310a88bf 100644 --- a/src/NuGetGallery.Core/Entities/Organization.cs +++ b/src/NuGetGallery.Core/Entities/Organization.cs @@ -6,27 +6,26 @@ namespace NuGetGallery { /// - /// The organization associated with a account. + /// Organization account, based on TPT hierarchy. /// - /// The Users table contains both User and Organization accounts. The Organizations table exists both - /// as a constraint for Membership as well as a place for possible Organization-only settings. If User - /// and Organization settings diverge, we can consider creating a separate UserSettings table as well. + /// With the addition of organizations, the users table effectively becomes an account table. Organizations accounts + /// are child types created using TPT inheritance. User accounts are not child types, but this could be done in the + /// future if we want to add constraints for user accounts or user-only settings. /// - public class Organization + /// + public class Organization : User { - /// - /// Organization primary key. - /// - public int Key { get; set; } + public Organization() : base() + { + } - /// - /// Organization account (User). - /// - public User Account { get; set; } + public Organization(string name) : base(name) + { + } /// - /// Organization memberships. + /// Organization Memberships to this organization. /// - public virtual ICollection Memberships { get; set; } + public virtual ICollection Members { get; set; } } } diff --git a/src/NuGetGallery.Core/Entities/User.cs b/src/NuGetGallery.Core/Entities/User.cs index d272770c73..a6d4f69e06 100644 --- a/src/NuGetGallery.Core/Entities/User.cs +++ b/src/NuGetGallery.Core/Entities/User.cs @@ -10,16 +10,11 @@ namespace NuGetGallery { /// - /// NuGetGallery now supports both Users and Organizations which share common features such as: - /// - Namespace ownership - /// - Package ownership - /// - Security policies - /// - Name and email unique across both Users and Organizations - /// - /// Organizations should not support UserRoles or Credentials, but this is not constrained by the - /// database. In the future, we could consider renaming the Users table to Accounts and adding a - /// Users table to constrain UserRoles and Credentials to only User accounts. + /// With the addition of organizations, the users table effectively becomes an account table. Organizations accounts + /// are child types created using TPT inheritance. User accounts are not child types, but this could be done in the + /// future if we want to add constraints for user accounts or user-only settings. /// + /// public class User : IEntity { public User() : this(null) @@ -36,14 +31,9 @@ public User(string username) } /// - /// represented by this account, if any. + /// Organization memberships for a non-organization account. /// - public Organization Organization { get; set; } - - /// - /// Organization memberships for a account. - /// - public virtual ICollection Memberships { get; set; } + public virtual ICollection Organizations { get; set; } [StringLength(256)] public string EmailAddress { get; set; } diff --git a/src/NuGetGallery/Authentication/AuthenticationService.cs b/src/NuGetGallery/Authentication/AuthenticationService.cs index 72eef63c45..2ee9311730 100644 --- a/src/NuGetGallery/Authentication/AuthenticationService.cs +++ b/src/NuGetGallery/Authentication/AuthenticationService.cs @@ -127,7 +127,7 @@ await Auditing.SaveAuditRecordAsync( return new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.BadCredentials); } - if (user.IsOrganization()) + if (user is Organization) { _trace.Information($"Cannot authenticate organization account'{userNameOrEmail}'."); @@ -219,7 +219,7 @@ await Auditing.SaveAuditRecordAsync( return null; } - if (matched.User.IsOrganization()) + if (matched.User is Organization) { _trace.Information($"Cannot authenticate organization account '{matched.User.Username}'."); @@ -490,7 +490,7 @@ public virtual ActionResult Challenge(string providerName, string redirectUrl) public virtual async Task AddCredential(User user, Credential credential) { - if (user.IsOrganization()) + if (user is Organization) { throw new InvalidOperationException(Strings.OrganizationsCannotCreateCredentials); } @@ -646,7 +646,7 @@ public static ClaimsIdentity CreateIdentity(User user, string authenticationType private async Task ReplaceCredentialInternal(User user, Credential credential) { - if (user.IsOrganization()) + if (user is Organization) { throw new InvalidOperationException(Strings.OrganizationsCannotCreateCredentials); } diff --git a/src/NuGetGallery/Extensions/UserExtensions.cs b/src/NuGetGallery/Extensions/UserExtensions.cs index 64555f65e8..e3f5a8e763 100644 --- a/src/NuGetGallery/Extensions/UserExtensions.cs +++ b/src/NuGetGallery/Extensions/UserExtensions.cs @@ -37,15 +37,5 @@ public static Credential GetCurrentApiKeyCredential(this User user, IIdentity id return user.Credentials.FirstOrDefault(c => c.Value == apiKey); } - - /// - /// Determines if the User (account) belongs to an organization. - /// - /// User (account) instance. - /// True for organizations, false for users. - public static bool IsOrganization(this User account) - { - return account.Organization != null; - } } } \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.resx b/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.resx deleted file mode 100644 index 0c6e911590..0000000000 --- a/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 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 - - - dbo - - \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.Designer.cs b/src/NuGetGallery/Migrations/201710301857232_Organizations.Designer.cs similarity index 79% rename from src/NuGetGallery/Migrations/201710250430326_AddOrganizations.Designer.cs rename to src/NuGetGallery/Migrations/201710301857232_Organizations.Designer.cs index 73f2758b43..73601b65dd 100644 --- a/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.Designer.cs +++ b/src/NuGetGallery/Migrations/201710301857232_Organizations.Designer.cs @@ -7,13 +7,13 @@ namespace NuGetGallery.Migrations using System.Resources; [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] - public sealed partial class AddOrganizations : IMigrationMetadata + public sealed partial class Organizations : IMigrationMetadata { - private readonly ResourceManager Resources = new ResourceManager(typeof(AddOrganizations)); + private readonly ResourceManager Resources = new ResourceManager(typeof(Organizations)); string IMigrationMetadata.Id { - get { return "201710250430326_AddOrganizations"; } + get { return "201710301857232_Organizations"; } } string IMigrationMetadata.Source diff --git a/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.cs b/src/NuGetGallery/Migrations/201710301857232_Organizations.cs similarity index 91% rename from src/NuGetGallery/Migrations/201710250430326_AddOrganizations.cs rename to src/NuGetGallery/Migrations/201710301857232_Organizations.cs index 848a251776..c13d5ee25f 100644 --- a/src/NuGetGallery/Migrations/201710250430326_AddOrganizations.cs +++ b/src/NuGetGallery/Migrations/201710301857232_Organizations.cs @@ -3,7 +3,7 @@ namespace NuGetGallery.Migrations using System; using System.Data.Entity.Migrations; - public partial class AddOrganizations : DbMigration + public partial class Organizations : DbMigration { public override void Up() { @@ -16,8 +16,8 @@ public override void Up() IsAdmin = c.Boolean(nullable: false), }) .PrimaryKey(t => new { t.OrganizationKey, t.MemberKey }) + .ForeignKey("dbo.Organizations", t => t.OrganizationKey) .ForeignKey("dbo.Users", t => t.MemberKey, cascadeDelete: true) - .ForeignKey("dbo.Organizations", t => t.OrganizationKey, cascadeDelete: true) .Index(t => t.OrganizationKey) .Index(t => t.MemberKey); @@ -26,19 +26,17 @@ public override void Up() c => new { Key = c.Int(nullable: false), - AccountKey = c.Int(nullable: false), }) .PrimaryKey(t => t.Key) .ForeignKey("dbo.Users", t => t.Key) .Index(t => t.Key); - } public override void Down() { - DropForeignKey("dbo.Memberships", "OrganizationKey", "dbo.Organizations"); DropForeignKey("dbo.Organizations", "Key", "dbo.Users"); DropForeignKey("dbo.Memberships", "MemberKey", "dbo.Users"); + DropForeignKey("dbo.Memberships", "OrganizationKey", "dbo.Organizations"); DropIndex("dbo.Organizations", new[] { "Key" }); DropIndex("dbo.Memberships", new[] { "MemberKey" }); DropIndex("dbo.Memberships", new[] { "OrganizationKey" }); diff --git a/src/NuGetGallery/Migrations/201710301857232_Organizations.resx b/src/NuGetGallery/Migrations/201710301857232_Organizations.resx new file mode 100644 index 0000000000..51bf9fddab --- /dev/null +++ b/src/NuGetGallery/Migrations/201710301857232_Organizations.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index 42fb211e17..bfb7d5f7b1 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -777,9 +777,9 @@ - - - 201710250430326_AddOrganizations.cs + + + 201710301857232_Organizations.cs @@ -1860,8 +1860,8 @@ 201709202249402_AddPackageOwnershipRequestsPage.cs - - 201710250430326_AddOrganizations.cs + + 201710301857232_Organizations.cs diff --git a/src/NuGetGallery/Services/PackagePermissionsService.cs b/src/NuGetGallery/Services/PackagePermissionsService.cs index b07e3402b7..d420cd3243 100644 --- a/src/NuGetGallery/Services/PackagePermissionsService.cs +++ b/src/NuGetGallery/Services/PackagePermissionsService.cs @@ -166,8 +166,9 @@ private static IEnumerable GetPermissionLevels(IEnumerable u.Organization != null) - .SelectMany(u => u.Organization.Memberships) + .Where(o => o is Organization) + .Cast() + .SelectMany(o => o.Members) .Where(m => isUserMatch(m.Member)); if (matchingMembers.Any(m => m.IsAdmin)) diff --git a/tests/NuGetGallery.Facts/Framework/Fakes.cs b/tests/NuGetGallery.Facts/Framework/Fakes.cs index 55623e8156..78e8a2b8ad 100644 --- a/tests/NuGetGallery.Facts/Framework/Fakes.cs +++ b/tests/NuGetGallery.Facts/Framework/Fakes.cs @@ -39,14 +39,10 @@ public Fakes() } }; - Organization = new User("testOrganization") + Organization = new Organization("testOrganization") { Key = 41, EmailAddress = "confirmedOrganization@example.com", - Organization = new Organization() - { - Key = 1 - }, // invalid credentials for testing authentication constraints Credentials = new List { @@ -54,11 +50,11 @@ public Fakes() } }; - Organization.Organization.Memberships = new List() + Organization.Members = new List() { new Membership { - Organization = Organization.Organization, + Organization = Organization, Member = User, IsAdmin = true } @@ -117,7 +113,7 @@ public Fakes() public User User { get; } - public User Organization { get; } + public Organization Organization { get; } public User ShaUser { get; } diff --git a/tests/NuGetGallery.Facts/Services/PackagePermissionsServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackagePermissionsServiceFacts.cs index fd92adcde5..3117a92aaa 100644 --- a/tests/NuGetGallery.Facts/Services/PackagePermissionsServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackagePermissionsServiceFacts.cs @@ -88,12 +88,17 @@ public void ReturnsExpectedPermissionLevels(IEnumerable owners, User user, bool isAdmin) { - var organization = new Organization() { Memberships = new List() }; - var organizationOwner = new User("testorganization") { Key = _key++, Organization = organization }; - owners.Add(organizationOwner); - - var organizationMembership = new Membership() { Organization = organization, Member = user, IsAdmin = isAdmin }; - organization.Memberships.Add(organizationMembership); + var organization = new Organization(); + organization.Members = new[] + { + new Membership() + { + Organization = organization, + Member = user, + IsAdmin = isAdmin + } + }; + owners.Add(organization); } private void AssertPermissionLevels(IEnumerable owners, User user, IEnumerable expectedLevels) From 5c8184f544c72665cf4b6ef3a4fb34f48173b5d0 Mon Sep 17 00:00:00 2001 From: Scott Bommarito Date: Mon, 30 Oct 2017 15:20:15 -0700 Subject: [PATCH 15/16] Add ability to reflow hard-deletes from Admin area (#4899) --- .../Admin/Controllers/DeleteController.cs | 90 ++++++++++++++++++- .../ViewModels/HardDeleteReflowBulkRequest.cs | 14 +++ ...HardDeleteReflowBulkRequestConfirmation.cs | 12 +++ .../ViewModels/HardDeleteReflowRequest.cs | 16 ++++ .../Areas/Admin/Views/Delete/Index.cshtml | 4 + .../Areas/Admin/Views/Delete/Reflow.cshtml | 23 +++++ .../Admin/Views/Delete/_ReflowBulk.cshtml | 25 ++++++ .../Views/Delete/_ReflowBulkConfirm.cshtml | 30 +++++++ src/NuGetGallery/NuGetGallery.csproj | 6 ++ .../Services/IPackageDeleteService.cs | 1 + .../Services/PackageDeleteService.cs | 51 ++++++++++- .../Controllers/DeleteControllerFacts.cs | 9 +- .../Services/PackageDeleteServiceFacts.cs | 66 +++++++++++++- 13 files changed, 341 insertions(+), 6 deletions(-) create mode 100644 src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowBulkRequest.cs create mode 100644 src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowBulkRequestConfirmation.cs create mode 100644 src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowRequest.cs create mode 100644 src/NuGetGallery/Areas/Admin/Views/Delete/Reflow.cshtml create mode 100644 src/NuGetGallery/Areas/Admin/Views/Delete/_ReflowBulk.cshtml create mode 100644 src/NuGetGallery/Areas/Admin/Views/Delete/_ReflowBulkConfirm.cshtml diff --git a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs index f961d44ce9..03bcc8699a 100644 --- a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs +++ b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using System.Web.Mvc; -using System.Web.Routing; using NuGet.Versioning; using NuGetGallery.Areas.Admin.ViewModels; @@ -14,12 +14,19 @@ namespace NuGetGallery.Areas.Admin.Controllers public partial class DeleteController : AdminControllerBase { private readonly IPackageService _packageService; + private readonly IPackageDeleteService _packageDeleteService; + private readonly ITelemetryService _telemetryService; protected DeleteController() { } - public DeleteController(IPackageService packageService) + public DeleteController( + IPackageService packageService, + IPackageDeleteService packageDeleteService, + ITelemetryService telemetryService) { _packageService = packageService; + _packageDeleteService = packageDeleteService; + _telemetryService = telemetryService; } [HttpGet] @@ -122,5 +129,84 @@ private DeleteSearchResult CreateDeleteSearchResult(Package package) .ToList() }; } + + [HttpGet] + public virtual ActionResult Reflow() + { + return View(nameof(Reflow)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public virtual ActionResult ReflowBulk(HardDeleteReflowBulkRequest request) + { + if (string.IsNullOrWhiteSpace(request.BulkList)) + { + TempData["Message"] = "Must specify a list of hard-deleted packages to reflow in bulk!"; + + return View(nameof(Reflow)); + } + + var lines = request.BulkList.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + try + { + var requests = new List(); + + foreach (var line in lines) + { + var parts = line.Split(new char[0], StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length != 2) + { + throw new UserSafeException( + $"Couldn't parse the list of hard-deleted packages to reflow in bulk: could not split \"{line}\" into ID and version!"); + } + + requests.Add(new HardDeleteReflowRequest() { Id = parts[0], Version = parts[1] }); + } + + return View(nameof(Reflow), new HardDeleteReflowBulkRequestConfirmation() { Requests = requests }); + } + catch (Exception e) + { + _telemetryService.TraceException(e); + + TempData["Message"] = e.GetUserSafeMessage(); + + return View(nameof(Reflow)); + } + } + + [HttpPost] + [ValidateAntiForgeryToken] + public virtual async Task ReflowBulkConfirm(HardDeleteReflowBulkRequestConfirmation confirmation) + { + var failures = new List(); + + foreach (var request in confirmation.Requests) + { + try + { + await _packageDeleteService.ReflowHardDeletedPackageAsync(request.Id, request.Version); + } + catch (Exception e) + { + failures.Add($"Failed to reflow hard-deleted package {request.Id} {request.Version}: {e.GetUserSafeMessage()}"); + } + } + + if (failures.Any()) + { + TempData["Message"] = string.Join(" ", failures.ToArray()); + } + else + { + TempData["Message"] = + "Successfully reflowed all packages."; + } + + return View(nameof(Reflow)); + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowBulkRequest.cs b/src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowBulkRequest.cs new file mode 100644 index 0000000000..34072761d2 --- /dev/null +++ b/src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowBulkRequest.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; + +namespace NuGetGallery.Areas.Admin.ViewModels +{ + public class HardDeleteReflowBulkRequest + { + [Display(Name = "List")] + [Required(ErrorMessage = "You must provide a list of deleted packages to reflow.")] + public string BulkList { get; set; } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowBulkRequestConfirmation.cs b/src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowBulkRequestConfirmation.cs new file mode 100644 index 0000000000..72d01d5cfd --- /dev/null +++ b/src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowBulkRequestConfirmation.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace NuGetGallery.Areas.Admin.ViewModels +{ + public class HardDeleteReflowBulkRequestConfirmation + { + public IEnumerable Requests { get; set; } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowRequest.cs b/src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowRequest.cs new file mode 100644 index 0000000000..0e85d89223 --- /dev/null +++ b/src/NuGetGallery/Areas/Admin/ViewModels/HardDeleteReflowRequest.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; + +namespace NuGetGallery.Areas.Admin.ViewModels +{ + public class HardDeleteReflowRequest + { + [Required(ErrorMessage = "You must provide an ID for the deleted package to reflow.")] + public string Id { get; set; } + + [Required(ErrorMessage = "You must provide a version for the deleted package to reflow.")] + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Areas/Admin/Views/Delete/Index.cshtml b/src/NuGetGallery/Areas/Admin/Views/Delete/Index.cshtml index 31c5b09ace..50bd728ebc 100644 --- a/src/NuGetGallery/Areas/Admin/Views/Delete/Index.cshtml +++ b/src/NuGetGallery/Areas/Admin/Views/Delete/Index.cshtml @@ -10,6 +10,10 @@

    Delete packages

    +

    + If you would like to reflow a hard-deleted package, go to this page instead. +

    +

    diff --git a/src/NuGetGallery/Areas/Admin/Views/Delete/Reflow.cshtml b/src/NuGetGallery/Areas/Admin/Views/Delete/Reflow.cshtml new file mode 100644 index 0000000000..f7eb6bd271 --- /dev/null +++ b/src/NuGetGallery/Areas/Admin/Views/Delete/Reflow.cshtml @@ -0,0 +1,23 @@ +@model HardDeleteReflowBulkRequestConfirmation +@{ + ViewBag.Title = "Reflow deleted packages"; +} + +
    +
    +

    Reflow

    + +

    + Use this page to reflow a hard-deleted package that is no longer in the gallery. +

    + + @if (Model == null) + { + @Html.Partial("_ReflowBulk") + } + else + { + @Html.Partial("_ReflowBulkConfirm") + } +
    +
    \ No newline at end of file diff --git a/src/NuGetGallery/Areas/Admin/Views/Delete/_ReflowBulk.cshtml b/src/NuGetGallery/Areas/Admin/Views/Delete/_ReflowBulk.cshtml new file mode 100644 index 0000000000..90620eabba --- /dev/null +++ b/src/NuGetGallery/Areas/Admin/Views/Delete/_ReflowBulk.cshtml @@ -0,0 +1,25 @@ +@model HardDeleteReflowBulkRequest + +@using (Html.BeginForm("ReflowBulk", "Delete")) +{ + @Html.AntiForgeryToken() + +

    + Type a list of packages to delete in the form: +
    + id version +
    + id2 version2 +
    + id3 version3 +

    + +
    + @Html.TextAreaFor(m => m.BulkList, 10, 50, new { style = "width: 100%;" }) + @Html.ValidationMessageFor(m => m.BulkList) +
    + +
    + + +} \ No newline at end of file diff --git a/src/NuGetGallery/Areas/Admin/Views/Delete/_ReflowBulkConfirm.cshtml b/src/NuGetGallery/Areas/Admin/Views/Delete/_ReflowBulkConfirm.cshtml new file mode 100644 index 0000000000..cb2765582f --- /dev/null +++ b/src/NuGetGallery/Areas/Admin/Views/Delete/_ReflowBulkConfirm.cshtml @@ -0,0 +1,30 @@ +@model HardDeleteReflowBulkRequestConfirmation + +@using (Html.BeginForm("ReflowBulkConfirm", "Delete")) +{ + @Html.AntiForgeryToken() + +

    + You are trying to reflow the following hard-deleted packages: +

    + + var requestIndex = 0; + +
      + @foreach (var request in Model.Requests) + { +
    • + @request.Id @request.Version +
    • + + @Html.Hidden("Requests[" + requestIndex + "].Id", request.Id) + @Html.Hidden("Requests[" + requestIndex + "].Version", request.Version) + + requestIndex++; + } +
    + +
    + + +} diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index bfb7d5f7b1..75458f9c42 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -698,6 +698,9 @@ + + + @@ -1871,6 +1874,9 @@ + + + diff --git a/src/NuGetGallery/Services/IPackageDeleteService.cs b/src/NuGetGallery/Services/IPackageDeleteService.cs index f13ba2353b..de6ee93a46 100644 --- a/src/NuGetGallery/Services/IPackageDeleteService.cs +++ b/src/NuGetGallery/Services/IPackageDeleteService.cs @@ -10,5 +10,6 @@ public interface IPackageDeleteService { Task SoftDeletePackagesAsync(IEnumerable packages, User deletedBy, string reason, string signature); Task HardDeletePackagesAsync(IEnumerable packages, User deletedBy, string reason, string signature, bool deleteEmptyPackageRegistration); + Task ReflowHardDeletedPackageAsync(string id, string version); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/PackageDeleteService.cs b/src/NuGetGallery/Services/PackageDeleteService.cs index 4e340cd5c7..7bcf2a85b3 100644 --- a/src/NuGetGallery/Services/PackageDeleteService.cs +++ b/src/NuGetGallery/Services/PackageDeleteService.cs @@ -33,6 +33,7 @@ DELETE pr FROM PackageRegistrations AS pr END"; private readonly IEntityRepository _packageRepository; + private readonly IEntityRepository _packageRegistrationRepository; private readonly IEntityRepository _packageDeletesRepository; private readonly IEntitiesContext _entitiesContext; private readonly IPackageService _packageService; @@ -42,6 +43,7 @@ DELETE pr FROM PackageRegistrations AS pr public PackageDeleteService( IEntityRepository packageRepository, + IEntityRepository packageRegistrationRepository, IEntityRepository packageDeletesRepository, IEntitiesContext entitiesContext, IPackageService packageService, @@ -50,6 +52,7 @@ public PackageDeleteService( IAuditingService auditingService) { _packageRepository = packageRepository; + _packageRegistrationRepository = packageRegistrationRepository; _packageDeletesRepository = packageDeletesRepository; _entitiesContext = entitiesContext; _packageService = packageService; @@ -168,6 +171,52 @@ await ExecuteSqlCommandAsync(_entitiesContext.GetDatabase(), UpdateSearchIndex(); } + public Task ReflowHardDeletedPackageAsync(string id, string version) + { + if (string.IsNullOrEmpty(id)) + { + throw new UserSafeException("Must supply an ID for the hard-deleted package to reflow."); + } + + if (string.IsNullOrEmpty(version)) + { + throw new UserSafeException("Must supply a version for the hard-deleted package to reflow."); + } + + var normalizedId = id.ToLowerInvariant(); + if (!NuGetVersion.TryParse(version, out var normalizedVersion)) + { + throw new UserSafeException($"{version} is not a valid version string!"); + } + + var normalizedVersionString = normalizedVersion.ToNormalizedString(); + + var existingPackageRegistration = _packageRegistrationRepository.GetAll() + .SingleOrDefault(p => p.Id == normalizedId); + + if (existingPackageRegistration != null) + { + var existingPackage = _packageRepository.GetAll() + .Where(p => p.PackageRegistrationKey == existingPackageRegistration.Key) + .SingleOrDefault(p => p.NormalizedVersion == normalizedVersionString); + + if (existingPackage != null) + { + throw new UserSafeException($"The package {id} {normalizedVersion} exists! You can only reflow hard-deleted packages that do not exist."); + } + } + + var auditRecord = new PackageAuditRecord( + normalizedId, + normalizedVersionString, + hash: string.Empty, + packageRecord: null, + registrationRecord: null, + action: AuditedPackageAction.Delete, + reason: "reflow hard-deleted package"); + return _auditingService.SaveAuditRecordAsync(auditRecord); + } + protected virtual async Task ExecuteSqlCommandAsync(Database database, string sql, params object[] parameters) { await database.ExecuteSqlCommandAsync(sql, parameters); @@ -260,4 +309,4 @@ protected virtual PackageAuditRecord CreateAuditRecord(Package package, PackageR return new PackageAuditRecord(package, action, reason); } } -} \ No newline at end of file +} diff --git a/tests/NuGetGallery.Facts/Areas/Admin/Controllers/DeleteControllerFacts.cs b/tests/NuGetGallery.Facts/Areas/Admin/Controllers/DeleteControllerFacts.cs index 1ecd04b1af..d49506f863 100644 --- a/tests/NuGetGallery.Facts/Areas/Admin/Controllers/DeleteControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Areas/Admin/Controllers/DeleteControllerFacts.cs @@ -54,6 +54,8 @@ public abstract class FactsBase : TestContainer protected string _query; protected readonly List _packages; protected readonly Mock _packageService; + protected readonly Mock _packageDeleteService; + protected readonly Mock _telemetryService; protected readonly Mock _httpContextBase; protected readonly DeleteController _target; @@ -96,9 +98,14 @@ public FactsBase() _packageService .Setup(x => x.FindPackageRegistrationById(It.IsAny())) .Returns(() => new PackageRegistration { Packages = _packages }); + + _packageDeleteService = new Mock(); + + _telemetryService = new Mock(); + _httpContextBase = new Mock(); - _target = new DeleteController(_packageService.Object); + _target = new DeleteController(_packageService.Object, _packageDeleteService.Object, _telemetryService.Object); TestUtility.SetupHttpContextMockForUrlGeneration(_httpContextBase, _target); } diff --git a/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs index 6e0df18dcc..2655eb32e8 100644 --- a/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs @@ -5,6 +5,7 @@ using System.Data.Entity; using System.Data.SqlClient; using System.IO; +using System.Linq; using System.Threading.Tasks; using Moq; using NuGetGallery.Auditing; @@ -18,6 +19,7 @@ public class PackageDeleteServiceFacts private static IPackageDeleteService CreateService( Mock> packageRepository = null, + Mock> packageRegistrationRepository = null, Mock> packageDeletesRepository = null, Mock entitiesContext = null, Mock packageService = null, @@ -27,6 +29,7 @@ private static IPackageDeleteService CreateService( Action> setup = null) { packageRepository = packageRepository ?? new Mock>(); + packageRegistrationRepository = packageRegistrationRepository ?? new Mock>(); packageDeletesRepository = packageDeletesRepository ?? new Mock>(); var dbContext = new Mock(); @@ -41,6 +44,7 @@ private static IPackageDeleteService CreateService( var packageDeleteService = new Mock( packageRepository.Object, + packageRegistrationRepository.Object, packageDeletesRepository.Object, entitiesContext.Object, packageService.Object, @@ -63,8 +67,8 @@ public class TestPackageDeleteService { public PackageAuditRecord LastAuditRecord { get; set; } - public TestPackageDeleteService(IEntityRepository packageRepository, IEntityRepository packageDeletesRepository, IEntitiesContext entitiesContext, IPackageService packageService, IIndexingService indexingService, IPackageFileService packageFileService, IAuditingService auditingService) - : base(packageRepository, packageDeletesRepository, entitiesContext, packageService, indexingService, packageFileService, auditingService) + public TestPackageDeleteService(IEntityRepository packageRepository, IEntityRepository packageRegistrationRepository, IEntityRepository packageDeletesRepository, IEntitiesContext entitiesContext, IPackageService packageService, IIndexingService indexingService, IPackageFileService packageFileService, IAuditingService auditingService) + : base(packageRepository, packageRegistrationRepository, packageDeletesRepository, entitiesContext, packageService, indexingService, packageFileService, auditingService) { } @@ -559,5 +563,63 @@ public async Task WillCreateAuditRecordUsingAuditService() auditingService.Verify(x => x.SaveAuditRecordAsync(testService.LastAuditRecord)); } } + + public class TheReflowHardDeletedPackagesAsyncMethod + { + [Fact] + public Task FailsIfPackageExists() + { + var id = "a"; + var version = "1.0.0"; + return ReflowHardDeletedPackage(id, version, id, version, false); + } + + [Fact] + public Task SucceedsIfRegistrationExistsButNotPackage() + { + var id = "a"; + var version = "1.0.0"; + var existingVersion = "2.0.0"; + return ReflowHardDeletedPackage(id, version, id, existingVersion, true); + } + + [Fact] + public Task SucceedsIfRegistrationDoesNotExist() + { + var id = "a"; + var version = "1.0.0"; + var existingId = "b"; + var existingVersion = "2.0.0"; + return ReflowHardDeletedPackage(id, version, existingId, existingVersion, true); + } + + private async Task ReflowHardDeletedPackage(string id, string version, string existingId, string existingVersion, bool succeeds) + { + var packageRegistrationKey = 1; + var packageRegistration = new PackageRegistration { Key = packageRegistrationKey, Id = existingId }; + var package = new Package { PackageRegistrationKey = packageRegistrationKey, NormalizedVersion = existingVersion }; + + var packageRegistrationRepository = new Mock>(); + packageRegistrationRepository.Setup(x => x.GetAll()).Returns(new PackageRegistration[] { packageRegistration }.AsQueryable()); + + var packageRepository = new Mock>(); + packageRepository.Setup(x => x.GetAll()).Returns(new Package[] { package }.AsQueryable()); + + var auditingService = new Mock(); + + var service = CreateService(packageRepository: packageRepository, packageRegistrationRepository: packageRegistrationRepository, auditingService: auditingService); + + if (succeeds) + { + await service.ReflowHardDeletedPackageAsync(id, version); + } + else + { + await Assert.ThrowsAsync(() => service.ReflowHardDeletedPackageAsync(id, version)); + } + + auditingService.Verify(x => x.SaveAuditRecordAsync(It.IsAny()), succeeds ? Times.Once() : Times.Never()); + } + } } } From 49833afe3e7ee36dd0de3cc5f9262b27e0241f35 Mon Sep 17 00:00:00 2001 From: Scott Bommarito Date: Mon, 30 Oct 2017 16:09:45 -0700 Subject: [PATCH 16/16] Fix permission-related renaming issue (#4929) --- src/NuGetGallery/NuGetGallery.csproj | 2 +- src/NuGetGallery/Views/Shared/_ListPackage.cshtml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index 75458f9c42..041dc4d87e 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -2296,4 +2296,4 @@ copy "$(ProjectDir)..\Bootstrap\dist\js\bootstrap.js" "$(ProjectDir)Scripts\gallery" >NUL - \ No newline at end of file + diff --git a/src/NuGetGallery/Views/Shared/_ListPackage.cshtml b/src/NuGetGallery/Views/Shared/_ListPackage.cshtml index b60ba1b2d6..0e420311a0 100644 --- a/src/NuGetGallery/Views/Shared/_ListPackage.cshtml +++ b/src/NuGetGallery/Views/Shared/_ListPackage.cshtml @@ -77,12 +77,12 @@ } - @if (Model.HasPermission(User, Permission.Edit) || - Model.HasPermission(User, Permission.ManagePackageOwners) || - Model.HasPermission(User, Permission.Delete)) + @if (Model.HasPermission(User, PackageAction.Edit) || + Model.HasPermission(User, PackageAction.ManagePackageOwners) || + Model.HasPermission(User, PackageAction.Unlist)) {
      - @if (Model.HasPermission(User, Permission.Edit)) + @if (Model.HasPermission(User, PackageAction.Edit)) {
    • @@ -91,7 +91,7 @@
    • } - @if (Model.HasPermission(User, Permission.ManagePackageOwners)) + @if (Model.HasPermission(User, PackageAction.ManagePackageOwners)) {
    • @@ -100,7 +100,7 @@
    • } - @if (Model.HasPermission(User, Permission.Delete)) + @if (Model.HasPermission(User, PackageAction.Unlist)) {