diff --git a/.gitignore b/.gitignore index 91d94c1ce6..4d0750561a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,14 +17,21 @@ Packages.dgml *.sln.docstates # Build results - [Dd]ebug/ [Rr]elease/ x64/ -#build/ # We use this folder for our Build Scripts [Bb]in/ [Oo]bj/ +# Files created and used by our build scripts +build/ +tools/ +.nuget/CredentialProviderBundle.zip +.nuget/CredentialProvider.VSS.exe +.nuget/EULA_Microsoft Visual Studio Team Services Credential Provider.docx +.nuget/ThirdPartyNotices.txt +AssemblyInfo.g.cs + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* @@ -149,7 +156,7 @@ Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ -# Mac crap +# Mac .DS_Store # NuGetGallery Specific Ignores @@ -160,8 +167,6 @@ src/NuGetGallery/App_Data/Mail src/NuGetGallery/App_Data/Lucene !src/NuGetGallery/Views/Packages/ !src/NuGetGallery/Branding/Views/Packages/ -!src/NuGetGallery.Operations/Tasks/Backups/ -!src/NuGetGallery.Cloud/*Content/bin # Kudu build folder build/artifacts/ @@ -171,6 +176,14 @@ build/artifacts/ tests/Scripts/TestResults[*].trx src/NuGetGallery.Cloud/ecf/ *.trx +tests/functionaltests.*.xml +Results.?.xml # Vs2015 -.vs/config/applicationhost.config \ No newline at end of file +# The applicationhost.config is ignored, but already comitted on purpose +# Reason: The localtest.me setting needs to be configured in the -section. +# See ReadMe.md for more information +.vs/config/applicationhost.config +src/NuGetGallery/App_Data/Files/auditing/ +artifacts/ +.vs diff --git a/.nuget/packages.config b/.nuget/packages.config index 1b2db2ce5e..c126c2587e 100644 --- a/.nuget/packages.config +++ b/.nuget/packages.config @@ -1,4 +1,6 @@  - + + + \ No newline at end of file diff --git a/Changelog.md b/Changelog.md deleted file mode 100644 index 27a22058a1..0000000000 --- a/Changelog.md +++ /dev/null @@ -1,239 +0,0 @@ -#March 7, 2014 - -Fixed a bug that caused a limited number of users to see an error page when attempting to view the Manage My Packages page, as well as a couple other UI glitches. This release was coordinated with a new worker whose new jobs support our disaster recovery and failover plan. See [3.0.1](https://github.com/NuGet/NuGetGallery/issues?milestone=45&page=1&state=closed) for details. - - -#February 21, 2014 - -Changed the frequency of stats updates on the home page. See [#1795](hhttps://github.com/NuGet/NuGetGallery/pull/1795) for details on the code change. - - -#February 6, 2014 - -Restored aggregate statistics to the home page, added an error message when package edits fail repeatedly, fixed a spurious error when uploading packages with dependencies that have no targetFramework assigned, and made a number of other fixes. See [I7 - QA 1/6](https://github.com/NuGet/NuGetGallery/issues?milestone=39&page=1&state=closed) for details. - -#January 2, 2014 - -Improved our detection algorithm for packages in the WebMatrix custom feed, included additional validation for package upload using the Gallery website, and made the experience for validation errors in the contact pages more consistent. This iteration also included a number of other small user interface changes; see [I6 -12/06 QA - 01/02 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=38&page=1&state=closed) for details. - -#Dec 6, 2013 - -Added support for Microsoft account login. Also fixes a number of UI issues. See [I5 -11/14 QA - 12/06 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=37&page=1&state=closed) for details. - - -#Nov 14, 2013 - -Bug fixes in a number of UI components, removing offensive terms, and removing the FriendlyLicenseNames configuration setting. Also fixes endless cycle of re-prompting for credentials in the client. [I4 -11/1 QA - 11/14 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=36&page=1&state=closed) - -#Nov 1,2013 - -Bug fixes in new user registration, stats, search indexing and "Manage my packages" page. Complete list can be found here: [I3 -10/18 QA - 11/1 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=35&page=1&state=closed) - -#Oct 19,2013 - -### OAuth Phase 1 - -The back end changes to support OAuth in NuGet Gallery. - -### Other bug fixes - -Complete list can be found here: [10/04 - QA (10/18 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=34&page=1&state=closed) - -#Oct 9,2013 - -### Friendly License Names - -NuGet Gallery will now display the list of license names for a package in addition to simply providing a link to the license text. The big idea behind this feature is that it will aid in the decision making process over whether or not to use a package. -More details [here.](http://blog.nuget.org/20131011/friendly-license-names.html) - -### Simplified user registration - -New user registration workflow has been simplified with this deployment. Going forward, users don't have to confirm email for signing in. Email comfirmation can be done anytime before uploading a package. - -### Normalized package versions - -The package versions will be normalized in the package display page. For example, for a package with version "1.0", the version will be displayed as "1.0.0". -The normalized version will be displayed in the "Verify package details" page while uploading a new package. This avoids the issue where packages with version "1.1" and "1.1.0" co-exists in the Gallery. - -### Other bug fixes - -Other bug fixes in Glimpse integration and OData feed.Complete list can be found here: [09/20 - QA (10/09 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=33&state=closed) - - -#Sep 19, 2013 - -### Edit package and other bug fixes - -A bunch of fixes around Edit package and expandable search box. Complete list can be found here: [09/6 - QA (09/19 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=32&page=1&state=closed) - -#Sep 3, 2013 - -### Edit Package - -You can now edit the metadata related to your NuGet package from the NuGet Gallery before and after uploading the package. -More details can be found [here](http://blog.nuget.org/20130823/Introducing-Edit-Package.html) in our team blog. - -### Expandable Search box -The search box now gets auto-expanded whenever user tries to search for packages, making it easier to type in large search queries. - -### Other bug fixes - -Other changes include minor fixes in stats page, GetUpdates() API and updating NuGet.Core to 2.7-alpha.Complete list can be found here: [08/14 - QA (09/03 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=29) - -#Aug 1, 2013 - -### Dependency update - -The dependencies of nuget.org website like OData, NuGet.Core and Azure Storage have been updated to point to their latest versions respectively. - -### Other bug fixes - -Other changes include minor fixes in stats page, GetUpdates() API and email validation for new user registration. Complete list can be found here: [07/19 - QA (08/02 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=27) - -#July 19, 2013 - -### Nuget.org deployed on Azure Websites - -The nuget.org website is now deployed on Azure web sites instead of Azure cloud services. Expect a detailed blog post from the NuGet team on the steps involed in migration and key take aways. -A couple of bug fixes were made to enable this migration( to be compatible with Azure web sites). - -- Canonical domain name for nuget.org : nuget.org will now re-direct to www.nuget.org. - -- Lucence search index stored in Temp folder instead of AppData folder. - -### Improved statistics - -The [stats page](https://www.nuget.org/stats) now shows graphs for client usage and monthly download trends. -Also the stats for the individual packages now shows graphs for [downloads based on version](https://www.nuget.org/stats/packages/Newtonsoft.Json?groupby=Version) and "Install-Dependency" as an operation - which would help in indicating whether it is a direct install or install due to dependency. - -### Other bug fixes - -Other changes include [updated terms of use and privacy policy](https://www.nuget.org/policies/Privacy) and fixes to "Contact support" form. Complete list can be found here: [07/05 - QA (07/19 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=25&state=closed) - -#July 8, 2013 - -### Accessiblity bug fixes - -A bunch of accessiblity issue like sorting, highlighting and WCAG level A HTML 5 errors in the website are fixed. - -### Other bug fixes - -Other changes include code refactoring of the controllers for better testability, client side input validation for user registration and proper retrieval of tags from package file irrespective of the delimiter used. Complete list can be found here: [06/21 - QA (07/05 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=24&state=closed) - -# June 21, 2013 - -### Filtering in GetUpdates() API based on target framework - -The GetUpdates() in nuget.org API (V2) feed now allows filtering based on a specific target framework. - -### Admin page bug fixes - -A bunch of bug fixes related to the nuget.org Admin page (which shows up only for Administrator account) to modify and update database. - -### Other bug fixes - -Other bug fixes related to new user registration form and database schemna changes. Complete list can be found here: [06/10 - QA (06/20 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=23&state=closed) - -# June 7, 2013 - -### Bug fixes in Search - -Minor bug fixes in search to not show the version number of packages in search results and to support special characters in seeach queries. Now the search terms like "C++" ,"C#" should return precise results. - -### Other bug fixes - -Other bug fixes related to "Contact Support" form and unlisting packages. Complete list can be found here: [05/27 - QA (06/07 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=22&state=closed) - -# May 23, 2013 - -### Remove unlisted packages from search index - -When a package gets unlisted, it will be removed from the Lucene search index immediately. This is one of the frequent ask from users as they don't want their unlisted packages to show up in search. - -### Admin Page bug fixes - -Bunch of fixes around the Admin page (which will be visible only for administrative login). - -### Other minor bug fixes - -Other minor fixes like client side validation for user name/email. Complete list can be found here: [05/13 - QA (05/24 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=21) - -# May 13, 2013 - -### Front page enhancements - -The contents for [nuget.org](nuget.org) home page is dynamically pulled from blob storage. This will help us to make announcements about new releases, -warnings or alerts about outages in an easy and quick way. - -### Admin Page improvements - -A new admin page is added to the nuget.org website which lets the administrators (core NuGet team members) to view error logs, rebuilding Lucenece index and similar admin actions. - -### User created date - -The user created date will be now be stored along with the user data in the dastabase. This enables getting statistics around users like registrations per week. - -### Other minor bug fixes - -Other minor fixes around statistics and curated feed.Complete list can be found here: [04/29 - QA (05/10 - Production)](https://github.com/NuGet/NuGetGallery/issues?milestone=20) - - -# April 25, 2013 - -### Group by Client Name,Version and Operation for download stats -The [package download statistics page] (//nuget.org/stats/) now allows you to group the download details based on package version, client version, client name and operation. - -### WebMatrix curated feed performance improvements -The indexing of curated packages is optimized for performance so that search on a curated feed is on par with the search on regular feed. - -### Other minor bug fixes - -Complete list can be found here: [Production Deployment 4/25](https://github.com/NuGet/NuGetGallery/issues?milestone=19) - -# April 11, 2013 - -### Top 500 packages exposed in the feed - -The nuget.org API (V2) feed now exposes the top downloaded packages (over the last 6 weeks). This can accessed be via url [nuget.org/api/v2/stats/downloads](//nuget.org/api/v2/stats/downloads). At this time, the top 500 packages are shown by default and that is also the maximum number returned. - -You can limit the numbers of results using ?count in the query string. For example, [nuget.org/api/v2/stats/downloads?count=10](//nuget.org/api/v2/stats/downloads?count=10) would return the top 10 downloaded packages in last 6 weeks - with information like download count, gallery url and feed url for that package. - -The default and maximum count of 500 might change over time, so we recommend always specifying a count parameter if you are programmatically consuming this data. - -### Numeric rank for packages stats - -The [Statistics page](http://nuget.org/stats) now shows the numeric rank of the package (based on the download count). - -### Links to gravatar in profile page - -The profile editing page now includes help text and a link to gravatar making it easy for users to update their profile picture. - -### UserName optimization in DB (backend) - -The "Users" table is optimized to have "UserName" as index for performance enhancements. - -### Other minor bug fixes - -Complete list can be found here: [Production Deployment 4/12](https://github.com/NuGet/NuGetGallery/issues?milestone=18&state=closed) - -# March 28, 2013 - -### Support for MinClientVersion - -You can now upload packages with "[minclientVersion](http://nuget.codeplex.com/wikipage?title=NuGet%202.5%20list%20of%20features%20for%20Testing%20days%203%2f27%20to%203%2f29%20%2c%202013 )" to the NuGetGallery. - -The minclientVersion of the package will shown in the package home page right next to the package description. - -### Contacting support - -The "Report Abuse" page has been revamped to enable users to chose the specific issue with the package they are reporting. It also guides the user to differentiate between "Contact Owners" and "Report Abuse". - -### Improved package statistics - -The package statistics now shows the break down of downloads based on the NuGet client (like NuGet CommandLine 2.1, NuGet Package Manager console 2.2 and so on. It also shows the split of the type of download operation (like Install, Restore, Update). - -### Other minor bug fixes - -Complete list can be found here: [Production Deployment 3/28](https://github.com/NuGet/NuGetGallery/issues?milestone=17&state=closed) - - diff --git a/Deploy-StaticContent.ps1 b/Deploy-StaticContent.ps1 new file mode 100644 index 0000000000..76161fa92a --- /dev/null +++ b/Deploy-StaticContent.ps1 @@ -0,0 +1,24 @@ +[CmdletBinding(DefaultParameterSetName='RegularBuild')] +param ( + [string]$StorageAccountName, + [string]$StorageAccountKey, + [string]$Environment +) + +Write-Host "Uploading static $Environment gallery content to $StorageAccountName." + +[System.Reflection.Assembly]::LoadFrom("C:\Program Files\Microsoft SDKs\Azure\.NET SDK\v2.9\bin\Microsoft.WindowsAzure.StorageClient.dll") + +$account = [Microsoft.WindowsAzure.CloudStorageAccount]::Parse("DefaultEndpointsProtocol=https;AccountName=$StorageAccountName;AccountKey=$StorageAccountKey") +$client = [Microsoft.WindowsAzure.StorageClient.CloudStorageAccountStorageClientExtensions]::CreateCloudBlobClient($account) + +$files = Get-ChildItem ".\content\$Environment" +foreach ($file in $files) { + $blob = $client.GetBlockBlob("content/$file") + try { + $snappy = $blob.CreateSnapshot() + Write-Host "Created snapshot of existing 'content/$file'." + } catch {} + $blob.UploadFile($file.FullName) + Write-Host "Uploaded 'content/$file'." +} \ No newline at end of file diff --git a/NuGet.Cloud.targets b/NuGet.Cloud.targets deleted file mode 100644 index 03e1b3f7d8..0000000000 --- a/NuGet.Cloud.targets +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - $(CollectRoleFilesDependsOn); - CollectWorkerAppConfig - - - - - - - %(Filename)%(Extension).config - $(_WorkerRoleProject) - $(_WorkerRoleProjectName) - - - %(TargetPath) - $(_WorkerRoleProject) - $(_WorkerRoleProjectName) - - - - - - $(PublishDependsOn); - OctopusPackage - - - - - $(MSBuildProjectDirectory)\$(PublishDir.Trim('\\')) - "$(NuGetExePath)" pack "$(MSBuildProjectDirectory)\$(MSBuildProjectName).nuspec" -Properties "Configuration=$(Configuration);Platform=$(Platform);SemanticVersion=$(SemanticVersion);SimpleVersion=$(SimpleVersion);BuildMachine=$(BuildMachine);BuildUser=$(BuildUser);Branch=$(Branch);Commit=$(Commit);BuildDateUtc=$(BuildDateUtc)" -Version $(SemanticVersion) -NonInteractive -OutputDirectory "$(PackageOutputDir)" - - - - - \ No newline at end of file diff --git a/NuGet.config b/NuGet.config index 24b02f4ff3..6aac65a840 100644 --- a/NuGet.config +++ b/NuGet.config @@ -8,8 +8,9 @@ - - + + + diff --git a/NuGetGallery.sln b/NuGetGallery.sln index 62a206a333..7b4c0a65ab 100644 --- a/NuGetGallery.sln +++ b/NuGetGallery.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{96E4AFF8-D3A1-4102-ADCF-05F186F916A9}" ProjectSection(SolutionItems) = preProject @@ -10,25 +10,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{96E4AF EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1. Frontend", "1. Frontend", "{05998089-58F5-4A84-8C11-C5C6244A6F89}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2. Operations", "2. Operations", "{2ECA1159-9B9D-4D65-95AF-F14337FD3DA6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3. Tests", "3. Tests", "{39E54EC3-CBAA-453A-BE64-748FE1559A58}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0. Shared", "0. Shared", "{155100FF-524B-4CAF-93C6-A57478B3DBAD}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetGallery", "src\NuGetGallery\NuGetGallery.csproj", "{1DACF781-5CD0-4123-8BAC-CD385D864BE5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetGallery.Facts", "tests\NuGetGallery.Facts\NuGetGallery.Facts.csproj", "{FDC76BEF-3360-45AC-A13E-AE8F14D343D5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetGallery.Operations", "src\NuGetGallery.Operations\NuGetGallery.Operations.csproj", "{DBECF66B-8F2F-4B32-9143-E243BAFF12DF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "galops", "src\galops\galops.csproj", "{F240D1BC-BBFB-4F22-9DF8-3FDE36BFD665}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetGallery.Core", "src\NuGetGallery.Core\NuGetGallery.Core.csproj", "{097B2CDD-9623-4C34-93C2-D373D51F5B4E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetGallery.Core.Facts", "tests\NuGetGallery.Core.Facts\NuGetGallery.Core.Facts.csproj", "{8AC9E39E-366C-47E5-80AE-38E71CD31386}" EndProject -Project("{CC5FD16D-436D-48AD-A40C-5A424C6E3E79}") = "NuGetGallery.Cloud", "src\NuGetGallery.Cloud\NuGetGallery.Cloud.ccproj", "{0041ACA0-30EC-4554-8C7C-0AF810F3086F}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2. Tests", "2. Tests", "{39E54EC3-CBAA-453A-BE64-748FE1559A58}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0. Shared", "0. Shared", "{155100FF-524B-4CAF-93C6-A57478B3DBAD}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.Search.Client", "src\NuGet.Services.Search.Client\NuGet.Services.Search.Client.csproj", "{6931C2EE-E081-4518-9798-D34D83B35BF6}" EndProject @@ -46,14 +38,6 @@ Global {FDC76BEF-3360-45AC-A13E-AE8F14D343D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {FDC76BEF-3360-45AC-A13E-AE8F14D343D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {FDC76BEF-3360-45AC-A13E-AE8F14D343D5}.Release|Any CPU.Build.0 = Release|Any CPU - {DBECF66B-8F2F-4B32-9143-E243BAFF12DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DBECF66B-8F2F-4B32-9143-E243BAFF12DF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DBECF66B-8F2F-4B32-9143-E243BAFF12DF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DBECF66B-8F2F-4B32-9143-E243BAFF12DF}.Release|Any CPU.Build.0 = Release|Any CPU - {F240D1BC-BBFB-4F22-9DF8-3FDE36BFD665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F240D1BC-BBFB-4F22-9DF8-3FDE36BFD665}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F240D1BC-BBFB-4F22-9DF8-3FDE36BFD665}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F240D1BC-BBFB-4F22-9DF8-3FDE36BFD665}.Release|Any CPU.Build.0 = Release|Any CPU {097B2CDD-9623-4C34-93C2-D373D51F5B4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {097B2CDD-9623-4C34-93C2-D373D51F5B4E}.Debug|Any CPU.Build.0 = Debug|Any CPU {097B2CDD-9623-4C34-93C2-D373D51F5B4E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -62,10 +46,6 @@ Global {8AC9E39E-366C-47E5-80AE-38E71CD31386}.Debug|Any CPU.Build.0 = Debug|Any CPU {8AC9E39E-366C-47E5-80AE-38E71CD31386}.Release|Any CPU.ActiveCfg = Release|Any CPU {8AC9E39E-366C-47E5-80AE-38E71CD31386}.Release|Any CPU.Build.0 = Release|Any CPU - {0041ACA0-30EC-4554-8C7C-0AF810F3086F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0041ACA0-30EC-4554-8C7C-0AF810F3086F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0041ACA0-30EC-4554-8C7C-0AF810F3086F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0041ACA0-30EC-4554-8C7C-0AF810F3086F}.Release|Any CPU.Build.0 = Release|Any CPU {6931C2EE-E081-4518-9798-D34D83B35BF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6931C2EE-E081-4518-9798-D34D83B35BF6}.Debug|Any CPU.Build.0 = Debug|Any CPU {6931C2EE-E081-4518-9798-D34D83B35BF6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -77,11 +57,8 @@ Global GlobalSection(NestedProjects) = preSolution {1DACF781-5CD0-4123-8BAC-CD385D864BE5} = {05998089-58F5-4A84-8C11-C5C6244A6F89} {FDC76BEF-3360-45AC-A13E-AE8F14D343D5} = {39E54EC3-CBAA-453A-BE64-748FE1559A58} - {DBECF66B-8F2F-4B32-9143-E243BAFF12DF} = {2ECA1159-9B9D-4D65-95AF-F14337FD3DA6} - {F240D1BC-BBFB-4F22-9DF8-3FDE36BFD665} = {2ECA1159-9B9D-4D65-95AF-F14337FD3DA6} {097B2CDD-9623-4C34-93C2-D373D51F5B4E} = {155100FF-524B-4CAF-93C6-A57478B3DBAD} {8AC9E39E-366C-47E5-80AE-38E71CD31386} = {39E54EC3-CBAA-453A-BE64-748FE1559A58} - {0041ACA0-30EC-4554-8C7C-0AF810F3086F} = {05998089-58F5-4A84-8C11-C5C6244A6F89} {6931C2EE-E081-4518-9798-D34D83B35BF6} = {05998089-58F5-4A84-8C11-C5C6244A6F89} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index e4a302bf44..1ed59dfff8 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,15 @@ NuGet Gallery for Companies This is an implementation of the NuGet Gallery and API. This serves as the back-end and community website for the NuGet client. For information about the NuGet project, visit the [Home repository](https://github.com/nuget/home). +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + ## Build and Run the Gallery in (arbitrary number) easy steps 1. Prerequisites. Install these if you don't already have them: 1. Visual Studio 2015 - Custom install so that you may also install Microsoft SQL Server Data Tools. This will provide the LocalDB that Windows Azure SDK requires. 2. PowerShell 2.0 (comes with Windows 7+) 3. [NuGet](http://docs.nuget.org/docs/start-here/installing-nuget) - 4. [Windows Azure SDK](http://www.microsoft.com/windowsazure/sdk/) - Note that you may have to manually upgrade the ".Cloud" projects in the solution if a different SDK version is used. + 4. [Windows Azure SDK](http://www.microsoft.com/windowsazure/sdk/) 2. Clone it! ```git clone git@github.com:NuGet/NuGetGallery.git``` @@ -24,21 +26,39 @@ website for the NuGet client. For information about the NuGet project, visit the ``` 4. Set up the website in IIS Express! 1. We highly recommend using IIS Express. Use the [Web Platform Installer](http://microsoft.com/web) to install it if you don't have it already (it comes with recent versions of VS and WebMatrix though). Make sure to at least once run IIS Express as an administrator. - 2. In an ADMIN powershell prompt, run the `.\tools\Enable-LocalTestMe.ps1` file. It allows non-admins to host websites at: `http(s)://nuget.localtest.me`, it configures an IIS Express site at that URL and creates a self-signed SSL certificate. For more information on `localtest.me`, check out [readme.localtest.me](http://readme.localtest.me). + 2. In an ADMIN powershell prompt, run the `.\tools\Enable-LocalTestMe.ps1` file. It allows non-admins to host websites at: `http(s)://nuget.localtest.me`, it configures an IIS Express site at that URL and creates a self-signed SSL certificate. For more information on `localtest.me`, check out [readme.localtest.me](http://readme.localtest.me). However, because [Server Name Indication](https://en.wikipedia.org/wiki/Server_Name_Indication) is not supported in the Network Shell on versions of Windows before 8, you must have at least Windows 8 to run this script successfully. 3. If you're having trouble, go to the _Project Properties_ for the Website project, click on the _Web_ tab and change the URL to `localhost:port` where _port_ is some port number above 1024. - 4. When running the application using the Azure Compute emulator, you may have to edit the `.\src\NuGetGallery.Cloud\ServiceConfiguration.Local.cscfg` file and set the certificate thumbprint for the setting `SSLCertificate` to the certificate thumbprint of the generated `nuget.localtest.me` certificate from step 2. You can get a list of certificates and their thumbprints using PowerShell, running `Get-ChildItem -path cert:\LocalMachine\My`. - -5. Create the Database! - 1. Open Visual Studio 2015 - 2. Open the Package Manager Console window - 3. Ensure that the Default Project is set to `NuGetGallery` - 4. Open the NuGetGallery.sln solution from the root of this repository. ***Important:*** Make sure the Package Manager Console has been opened once before you open the solution. If the solution was already open, open the package manager console and then close and re-open the solution (from the file menu) - 5. Run the following command in the Package Manager Console: - ``` - Update-Database -StartUpProjectName NuGetGallery -ConfigurationTypeName MigrationsConfiguration - ``` -If this fails, you are likely to get more useful output by passing `-Debug` than `-Verbose`. +5. Create the Database! + + There are two ways you can create the databases. From Visual Studio 2015 or from the command line. + + 1. From Visual Studio 2015 + 1. Open Visual Studio 2015 + 2. Open the Package Manager Console window + 3. Ensure that the Default Project is set to `NuGetGallery` + 4. Open the NuGetGallery.sln solution from the root of this repository. ***Important:*** Make sure the Package Manager Console has been opened once before you open the solution. If the solution was already open, open the package manager console and then close and re-open the solution (from the file menu) + 5. Run the following command in the Package Manager Console: + + ``` powershell + Update-Database -StartUpProjectName NuGetGallery -ConfigurationTypeName MigrationsConfiguration + ``` + If this fails, you are likely to get more useful output by passing `-Debug` than `-Verbose`. + 2. From the command line. ***Important:*** You must have successfully built the Gallery (step 3) for this to succeed. + * Run `Update-Databases.ps1` in the `tools` folder to migrate the databases to the latest version. + * To Update both databases, Nuget Gallery and Support Request, run this command + ``` powershell + .\tools\Update-Databases.ps1 -MigrationTargets NugetGallery,NugetGallerySupportRequest + ``` + * To update only the Nuget Gallery DB, run this + ``` powershell + .\tools\Update-Databases.ps1 -MigrationTargets NugetGallery + ``` + * And to update only the Support Request DB, run this + ``` powershell + .\tools\Update-Databases.ps1 -MigrationTargets NugetGallerySupportRequest + ``` + * Additionally you can provide a `-NugetGallerySitePath` parameter to the `Update-Databases.ps1` script to indicate that you want to perform the migration on a site other than the one that is built with this repository. 6. When working with the gallery, e-mail messages are saved to the file system (under `~/App_Data`). * To change this to use an SMTP server, edit `src\NuGetGallery\Web.Config` and add a `Gallery.SmtpUri` setting. Its value should be an SMTP connection string, for example `smtp://user:password@smtpservername:25`. @@ -49,6 +69,20 @@ If this fails, you are likely to get more useful output by passing `-Debug` than That's it! You should now be able to press Ctrl-F5 to run the site! +Be aware that you might detect a change in the __applicationhost.config__: + +Unfortunately Visual Studio will replace the relative path with an absolute path. The committed applicationhost.config-file is currently the easiest way to setup the localtest.me-binding for IIS Express. + +However, you can force Git to ignore the change with this command: + + git update-index --assume-unchanged .vs/config/applicationhost.config + +You can undo this with this command: + + git update-index --no-assume-unchanged .vs/config/applicationhost.config + +This should help to prevent unwanted file commits. + ## Contribute If you find a bug with the gallery, please visit the [Issue tracker](https://github.com/NuGet/NuGetGallery/issues) and create an issue. If you're feeling generous, please search to see if the issue is already logged before creating a @@ -88,7 +122,8 @@ This is the Git workflow we're currently using: ### Setting up -1. Clone and checkout the following branches (to make sure local copies are made): 'master'. +1. Clone and checkout the following branches (to make sure local copies are made): ' +2. '. ### When starting a new feature/unit of work. @@ -97,16 +132,16 @@ This is the Git workflow we're currently using: This assumes you have no local commits that haven't yet been pushed (i.e., that you were previously up-to-date with origin). - git checkout master - git pull master + git checkout dev + git pull dev 2. __Create a topic branch to do your work.__ You must work in topic branches, in order to help us keep our features isolated and easily moved between branches. - Our policy is to start all topic branches off of the 'master' branch. + Our policy is to start all topic branches off of the 'dev' branch. Branch names should use the following format '[user]-[bugnumber]-[shortdescription]'. If there is no bug yet, create one and assign it to yourself! - git checkout master + git checkout dev git checkout -b anurse-123-makesuckless 3. __Do your work.__ @@ -126,19 +161,19 @@ This is the Git workflow we're currently using: 4. __Start a code review.__ Start a code review by pushing your branch up to GitHub (```git push origin anurse-123-makesuckless```) and - creating a Pull Request from your branch to ***master***. Wait for at least someone on the team to respond with: ":shipit:" (that's called the + creating a Pull Request from your branch to ***dev***. Wait for at least someone on the team to respond with: ":shipit:" (that's called the "Ship-It Squirrel" and you can put it in your own comments by typing ```:shipit:```). -5. __Merge your changes in to master.__ - Click the bright green "Merge" button on your pull request! **NOTE: DO NOT DELETE THE TOPIC BRANCH!!** +5. __Merge your changes in to dev.__ + Click the bright green "Merge" button on your pull request! Don't forget to delete the branch afterwards to keep our repo clean. If there isn't a bright green button... well, you'll have to do some more complicated merging: - git checkout master - git pull origin master + git checkout dev + git pull origin dev git merge anurse-123-makesuckless ... resolve conflicts ... - git push origin master + git push origin dev 6. __Be ready to guide your change through QA, Staging and Prod__ Your change will make its way through the QA, Staging and finally Prod branches as it's deployed to the various environments. Be prepared to fix additional bugs! diff --git a/Repository.props b/Repository.props deleted file mode 100644 index 8899d77824..0000000000 --- a/Repository.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 496af747f9..5fdec2b9e6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,11 +1,10 @@ -version: 3.0.{build} +version: 3.1.{build} configuration: Release os: Visual Studio 2015 build_script: - cmd: 'build.cmd /p:SimpleVersion=%APPVEYOR_BUILD_VERSION% /p:Branch=%APPVEYOR_REPO_BRANCH% /p:Commit=%APPVEYOR_REPO_COMMIT:~0,6%' artifacts: -- path: '**\app.publish\*.cspkg' -- path: '**\app.publish\*.nupkg' +- path: 'artifacts\*.nupkg' cache: - packages -> **\packages.config nuget: diff --git a/build.cmd b/build.cmd index e556ba1adc..dea8289527 100644 --- a/build.cmd +++ b/build.cmd @@ -1 +1 @@ -@msbuild build.msbuild /tv:14.0 /p:VisualStudioVersion=14.0 /p:ToolsVersion=14.0 %* +PowerShell.exe -NoProfile -ExecutionPolicy ByPass ".\buildandtest.ps1 -Configuration 'Release' -Verbose" \ No newline at end of file diff --git a/build.msbuild b/build.msbuild deleted file mode 100644 index 4a54dd8774..0000000000 --- a/build.msbuild +++ /dev/null @@ -1,80 +0,0 @@ - - - - 14.0 - 14.0 - - - $(MSBuildToolsPath)\Microsoft.Build.Tasks.v$(MSBuildToolsVersion).dll - $(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - $(MSBuildThisFileDirectory)\.nuget - $(NuGetExeDir)\nuget.exe - "$(NuGetExePath)" restore -Source "@(PackageSource)" "@(SolutionFile)" -NonInteractive - - - - - - - - - - - - - - - - - - <_NuGetServicesBuildVersion>$(Versions.Split(";")[0]) - - - - - - \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000000..587eae59bf --- /dev/null +++ b/build.ps1 @@ -0,0 +1,107 @@ +[CmdletBinding(DefaultParameterSetName='RegularBuild')] +param ( + [ValidateSet("debug", "release")] + [string]$Configuration = 'debug', + [int]$BuildNumber, + [switch]$SkipRestore, + [switch]$CleanCache, + [string]$SimpleVersion = '1.0.0', + [string]$SemanticVersion = '1.0.0-zlocal', + [string]$Branch, + [string]$CommitSHA, + [string]$BuildBranch = '1c479a7381ebbc0fe1fded765de70d513b8bd68e' +) + +# For TeamCity - If any issue occurs, this script fail the build. - By default, TeamCity returns an exit code of 0 for all powershell scripts, even if they fail +trap { + Write-Host "BUILD FAILED: $_" -ForegroundColor Red + Write-Host "ERROR DETAILS:" -ForegroundColor Red + Write-Host $_.Exception -ForegroundColor Red + Write-Host ("`r`n" * 3) + exit 1 +} + +if (-not (Test-Path "$PSScriptRoot/build")) { + New-Item -Path "$PSScriptRoot/build" -ItemType "directory" +} +wget -UseBasicParsing -Uri "https://raw.githubusercontent.com/NuGet/ServerCommon/$BuildBranch/build/init.ps1" -OutFile "$PSScriptRoot/build/init.ps1" +. "$PSScriptRoot/build/init.ps1" -BuildBranch "$BuildBranch" + +Function Clean-Tests { + [CmdletBinding()] + param() + + Trace-Log 'Cleaning test results' + + Remove-Item (Join-Path $PSScriptRoot "Results.*.xml") +} + +Write-Host ("`r`n" * 3) +Trace-Log ('=' * 60) + +$startTime = [DateTime]::UtcNow +if (-not $BuildNumber) { + $BuildNumber = Get-BuildNumber +} +Trace-Log "Build #$BuildNumber started at $startTime" + +$BuildErrors = @() + +Invoke-BuildStep 'Getting private build tools' { Install-PrivateBuildTools } ` + -ev +BuildErrors + +Invoke-BuildStep 'Cleaning test results' { Clean-Tests } ` + -ev +BuildErrors + +Invoke-BuildStep 'Installing NuGet.exe' { Install-NuGet } ` + -ev +BuildErrors + +Invoke-BuildStep 'Clearing package cache' { Clear-PackageCache } ` + -skip:(-not $CleanCache) ` + -ev +BuildErrors + +Invoke-BuildStep 'Clearing artifacts' { Clear-Artifacts } ` + -ev +BuildErrors + +Invoke-BuildStep 'Restoring solution packages' { ` + Install-SolutionPackages -path (Join-Path $PSScriptRoot ".nuget\packages.config") -output (Join-Path $PSScriptRoot "packages") -excludeversion } ` + -skip:$SkipRestore ` + -ev +BuildErrors + +Invoke-BuildStep 'Set version metadata in AssemblyInfo.cs' { + $Paths = ` + (Join-Path $PSScriptRoot "src\NuGetGallery\Properties\AssemblyInfo.g.cs"), ` + (Join-Path $PSScriptRoot "src\NuGetGallery.Core\Properties\AssemblyInfo.g.cs") + + Foreach ($Path in $Paths) { + Set-VersionInfo -Path $Path -Version $SimpleVersion -Branch $Branch -Commit $CommitSHA + } + } ` + -ev +BuildErrors + +Invoke-BuildStep 'Building solution' { + $SolutionPath = Join-Path $PSScriptRoot "NuGetGallery.sln" + Build-Solution $Configuration $BuildNumber -MSBuildVersion "14" $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 + } + +Trace-Log ('-' * 60) + +## Calculating Build time +$endTime = [DateTime]::UtcNow +Trace-Log "Build #$BuildNumber ended at $endTime" +Trace-Log "Time elapsed $(Format-ElapsedTime ($endTime - $startTime))" + +Trace-Log ('=' * 60) + +if ($BuildErrors) { + $ErrorLines = $BuildErrors | %{ ">>> $($_.Exception.Message)" } + Error-Log "Builds completed with $($BuildErrors.Count) error(s):`r`n$($ErrorLines -join "`r`n")" -Fatal +} + +Write-Host ("`r`n" * 3) \ No newline at end of file diff --git a/build/DeployFrontend.ps1 b/build/DeployFrontend.ps1 deleted file mode 100644 index 0548f1eac0..0000000000 --- a/build/DeployFrontend.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -function Set-AppSetting($x, [string]$name, [string]$value) { - $setting = $x.configuration.appSettings.add | where { $_.key -eq $name } - if($setting) { - $setting.value = $value - "Set $name = $value." - } else { - "Unknown App Setting: $name." - } -} - -# Gather deployment info -pushd $env:DEPLOYMENT_SOURCE -"In Deployment Source: $(Get-Location)" -$Commit = git rev-parse --short HEAD -$Branch = $env:branch -$Date = [TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([DateTimeOffset]::UtcNow, "Pacific Standard Time") -popd - -# Load web.config -$webConfigPath = Join-Path $env:DEPLOYMENT_TEMP "web.config" -if(!(Test-Path $webConfigPath)) { - throw "Web.config not found at $webConfigPath!" -} -$webConfig = [xml](cat $webConfigPath) -Set-AppSetting $webConfig "Gallery.ReleaseBranch" $Branch -Set-AppSetting $webConfig "Gallery.ReleaseSha" $Commit -Set-AppSetting $webConfig "Gallery.ReleaseTime" ($Date.ToString("yyyy-MM-dd hh:mm:ss tt") + " Pacific") -$webConfig.Save($webConfigPath) \ No newline at end of file diff --git a/build/NuGetGallery.xunit.targets b/build/NuGetGallery.xunit.targets deleted file mode 100644 index 9da6378bc4..0000000000 --- a/build/NuGetGallery.xunit.targets +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - $(BuildDependsOn); - RunFacts - - - - - - - - \ No newline at end of file diff --git a/build/README.md b/build/README.md deleted file mode 100644 index 6ae26659ab..0000000000 --- a/build/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Build Scripts - -Scripts used during the compilation of the NuGet Gallery frontend and backend. \ No newline at end of file diff --git a/buildandtest.ps1 b/buildandtest.ps1 new file mode 100644 index 0000000000..6962d44eeb --- /dev/null +++ b/buildandtest.ps1 @@ -0,0 +1,17 @@ +[CmdletBinding(DefaultParameterSetName='RegularBuild')] +param ( + [ValidateSet("debug", "release")] + [string]$Configuration = 'debug', + [int]$BuildNumber, + [switch]$SkipRestore, + [switch]$CleanCache, + [string]$SimpleVersion = '1.0.0', + [string]$SemanticVersion = '1.0.0-zlocal', + [string]$Branch, + [string]$CommitSHA +) + +$ScriptPath = Split-Path $MyInvocation.InvocationName + +& "$ScriptPath\build.ps1" -Configuration $Configuration -BuildNumber $BuildNumber -SkipRestore:$SkipRestore -CleanCache:$CleanCache -SimpleVersion "$SimpleVersion" -SemanticVersion "$SemanticVersion" -Branch "$Branch" -CommitSHA "$CommitSHA" +& "$ScriptPath\test.ps1" -Configuration $Configuration -BuildNumber $BuildNumber \ No newline at end of file diff --git a/content/DEV/Privacy-Policy.md b/content/DEV/Privacy-Policy.md index d7af89ebfa..df8990e70d 100644 --- a/content/DEV/Privacy-Policy.md +++ b/content/DEV/Privacy-Policy.md @@ -1,60 +1,60 @@ -# NuGet Website Privacy Statement - -The NuGet Website ([http://nuget.org](http://nuget.org)) (the "Website") is provided by the .NET Foundation (".NET Foundation") as a public service to our users. Your privacy is important to us. Our goal is to provide you with a personalized online experience that provides you with the information, resources, and services that are most relevant and helpful to you. This Privacy Statement has been written to describe the conditions under which the Website is being made available to you. This Privacy Statement describes, among other things, how data obtained during your visit to the Website may be collected and used. We strongly recommend that you read this Privacy Statement carefully. By using the Website, you agree to be bound by the terms of this Privacy Statement. If you do not accept the terms of this Privacy Statement, you must discontinue accessing or otherwise using the Website or any materials obtained from it. If you are dissatisfied with the Website, by all means contact us at info@nuget.org; otherwise, your only recourse is to discontinue using the Website. - -The process of maintaining a Website is an evolving one, and .NET Foundation may decide at some point in the future, without advance notice, to modify the terms of this Privacy Statement. Your use of the Website, or materials obtained from the Website, indicates your assent to this Privacy Statement at the time of such use. The effective Privacy Statement will be posted on the Website, and you should check upon every visit to the Website for any changes to this Privacy Statement. - -## Sites Covered by this Privacy Statement -This Privacy Statement applies to all .NET Foundation-maintained Web sites related to the NuGet Gallery, domains, information portals, and registries, as well as any other .NET Foundation websites that link to this Privacy Statement. - -## Children's Privacy - -We are committed to protecting the privacy needs of children, and we encourage parents and guardians to take an active role in their children's online activities and interests. .NET Foundation does not intentionally collect information from children under the age of 13, and .NET Foundation does not target the Website to children. - -## Links to Non-.NET-Foundation Web Sites -The Website may provide links to third-party websites for the convenience of our users. If you click on those links, you will leave the Website. .NET Foundation does not control these third-party websites and cannot represent that their policies and practices will be consistent with this Privacy Statement. For example, other websites may collect or use personal information about you in a manner different from that described in this document. Therefore, you should use other websites with caution, and you do so at your own risk. We encourage you to review the privacy policy of any third-party website before submitting personal information. - -## TYPES OF INFORMATION WE COLLECT - -### Non-Personal Information -Non-personal information is data about usage and service operation that is not directly associated with a specific personal identity. .NET Foundation may collect and analyze non-personal information to evaluate how visitors use the Website. - -### Aggregate Information -.NET Foundation may gather aggregate information, which refers to information your computer automatically provides to us and which cannot be tied back to you as a specific individual. Examples include referral data (the websites you visited just before and just after the Website), the Website pages viewed, time spent at the site, and Internet Protocol (IP) addresses. An IP address is a number that is automatically assigned to your computer whenever you access the Internet. For example, when you request a page from teh Website, our servers log your IP address to create aggregate reports on user demographics and traffic patterns and for purposes of system administration. - -### Log Files -Every time you request or download a file from the Website, .NET Foundation may store data about these events and your IP address in a log file. .NET Foundation may use this information to analyze trends, administer the Website, track users' movements, and gather broad demographic information for aggregate use or for other business purposes. - -### Cookies -The Website may use a feature of your browser to set a "cookie" on your computer. Cookies are small packets of information that a website stores on your computer. The Web site can then read the cookies whenever you visit the Website. We may use cookies in a number of ways, such as to save an authentication token so that you don't have to enter your username and password each time you visit the Website, to deliver content specific to your interests and to track the pages you've visited on the Website. These cookies allow us to use the information we collect to customize your Website experience so that your visit to the Website is as relevant and as valuable to you as possible. - -Most browser software can be set up to handle cookies. You may modify your browser preference to provide you with choices relating to cookies. You have the choice to accept all cookies, to be notified when a cookie is set or to reject all cookies. If you choose to reject cookies, certain functions and conveniences of the Website may not work properly, and you may be unable to use those Website services that require registration in order to participate, or you will have to re-enter your username and password each time you visit the Website. Most browsers offer instructions on how to reset the browser to reject cookies in the "Help" section of the toolbar. We do not link non-personal information from cookies to personally identifiable information without your permission. - -### Web Beacons -The Website also may use Web beacons to collect non-personal information about your use of the Website. The information collected by web beacons allows us to statistically monitor how many people are using the Website, how many people open our emails, and for what purposes these actions are being taken. Our web beacons are not used to track your activity outside of the Website. .NET Foundation does not link non-personal information from web beacons to personally identifiable information without your permission. - -### Personal Information -Personal information is information that is associated with your name or personal identity. .NET Foundation uses personal information to better understand your needs and interests and to provide you with better service. On some Website pages, you may be able to view information about NuGet packages, download NuGet packages, provide feedback, submit information into registries, register for notifications, or upload NuGet packages. The types of personal information you provide to us on these pages may include name, address, phone number, email address, user IDs, passwords, or other information. - -## HOW WE USE YOUR INFORMATION -.NET Foundation may use non-personal data that is aggregated for reporting about the Website usability, performance, and effectiveness. It may be used to improve the experience, usability, and content of the Website. - -### Information Sharing -We do not sell, rent, or lease any individual's personal information or lists of email addresses to anyone for marketing purposes, and we take commercially reasonable steps to maintain the security of this information. However, .NET Foundation reserves the right to supply any such information to any organization into which .NET Foundation may merge in the future or to which it may make any transfer in order to enable a third party to continue part or all of .NET Foundation's mission. We also reserve the right to release personal information to protect our systems or business, when we reasonably believe you to be in violation of our Terms of Use or if we reasonably believe you to have initiated or participated in any illegal activity. In addition, please be aware that in certain circumstances, .NET Foundation may be obligated to release your personal information pursuant to judicial or other government subpoenas, warrants, or other orders. Certain NuGet repositories on the Website may require you to agree to a third party's legal terms in order to upload or access NuGet packages. We may provide your personal information to the third party if you accept such legal terms. - -In keeping with our open process, .NET Foundation may maintain publicly accessible archives for Website activities. For example, submitting the report abuse form may result in your description of the abuse becoming part of the publically accessible archives. In all such cases, we will ensure to the greatest degree possible, that personal information is protected. - -Please remember that any information (including personal information) that you disclose in public areas of the Website, such as NuGet package uploads, discussions, and social networking features, becomes public information that others may collect, circulate, and use. Because we cannot and do not control the acts of others, you should exercise caution when deciding to disclose information about yourself or others in public forums such as these. - -Given the international scope of .NET Foundation, personal information may be visible to persons outside your country of residence, including to persons in countries that your own country's privacy laws and regulations deem deficient in ensuring an adequate level of protection for such information. If you are unsure whether this privacy statement is in conflict with applicable local rules, you should not submit your information. If you are located within the European Union, you should note that your information will be transferred to the United States, which is deemed by the European Union to have inadequate data protection. Nevertheless, in accordance with local laws implementing European Union Directive 95/46/EC of 24 October 1995 on the protection of individuals with regard to the processing of personal data and on the free movement of such data ("EU Privacy Directive"), individuals located in countries outside of the United States of America who submit personal information do thereby consent to the general use of such information as provided in this Privacy Statement and to its transfer to and/or storage in the United States of America. - -If you do not want your personal information collected and used by .NET Foundation, please do not visit the Website or download or upload NuGet packages. - -### Security -.NET Foundation makes reasonable efforts to protect personal information by users of the Website, including using firewalls and other security measures on its servers. No server, however, is 100% secure, and you should take this into account when submitting personal or confidential information about yourself on the Website. Much of the personal information is used in conjunction with participation-level services such as sharing and exchanging NuGet packages, so some types of personal information such as your name, company affiliation, and email address will be visible to other participants and to the public. .NET Foundation assumes no liability for the interception, alteration, or misuse of the information you provide. You alone are responsible for maintaining the secrecy of your personal information. Please use care when you access the Website and provide personal information. - -### Opting Out -From time to time .NET Foundation may email you electronic newsletters, announcements, surveys or other information. If you prefer not to receive any or all of these communications, you may opt out by following the directions provided within the electronic newsletters and announcements. - -### Contacting Us -Questions about this Privacy Statement can be directed to info@nuget.org. \ No newline at end of file +# NuGet Website Privacy Statement + +The NuGet Website ([http://nuget.org](http://nuget.org)) (the "Website") is provided by the .NET Foundation (".NET Foundation") as a public service to our users. Your privacy is important to us. Our goal is to provide you with a personalized online experience that provides you with the information, resources, and services that are most relevant and helpful to you. This Privacy Statement has been written to describe the conditions under which the Website is being made available to you. This Privacy Statement describes, among other things, how data obtained during your visit to the Website may be collected and used. We strongly recommend that you read this Privacy Statement carefully. By using the Website, you agree to be bound by the terms of this Privacy Statement. If you do not accept the terms of this Privacy Statement, you must discontinue accessing or otherwise using the Website or any materials obtained from it. If you are dissatisfied with the Website, by all means contact us at info@nuget.org; otherwise, your only recourse is to discontinue using the Website. + +The process of maintaining a Website is an evolving one, and .NET Foundation may decide at some point in the future, without advance notice, to modify the terms of this Privacy Statement. Your use of the Website, or materials obtained from the Website, indicates your assent to this Privacy Statement at the time of such use. The effective Privacy Statement will be posted on the Website, and you should check upon every visit to the Website for any changes to this Privacy Statement. + +## Sites Covered by this Privacy Statement +This Privacy Statement applies to all .NET Foundation-maintained Web sites related to the NuGet Gallery, domains, information portals, and registries, as well as any other .NET Foundation websites that link to this Privacy Statement. + +## Children's Privacy + +We are committed to protecting the privacy needs of children, and we encourage parents and guardians to take an active role in their children's online activities and interests. .NET Foundation does not intentionally collect information from children under the age of 13, and .NET Foundation does not target the Website to children. + +## Links to Non-.NET-Foundation Web Sites +The Website may provide links to third-party websites for the convenience of our users. If you click on those links, you will leave the Website. .NET Foundation does not control these third-party websites and cannot represent that their policies and practices will be consistent with this Privacy Statement. For example, other websites may collect or use personal information about you in a manner different from that described in this document. Therefore, you should use other websites with caution, and you do so at your own risk. We encourage you to review the privacy policy of any third-party website before submitting personal information. + +## TYPES OF INFORMATION WE COLLECT + +### Non-Personal Information +Non-personal information is data about usage and service operation that is not directly associated with a specific personal identity. .NET Foundation may collect and analyze non-personal information to evaluate how visitors use the Website. + +### Aggregate Information +.NET Foundation may gather aggregate information, which refers to information your computer automatically provides to us and which cannot be tied back to you as a specific individual. Examples include referral data (the websites you visited just before and just after the Website), the Website pages viewed, time spent at the site, and Internet Protocol (IP) addresses. An IP address is a number that is automatically assigned to your computer whenever you access the Internet. For example, when you request a page from the Website, our servers log your IP address to create aggregate reports on user demographics and traffic patterns and for purposes of system administration. + +### Log Files +Every time you request or download a file from the Website, .NET Foundation may store data about these events and your IP address in a log file. .NET Foundation may use this information to analyze trends, administer the Website, track users' movements, and gather broad demographic information for aggregate use or for other business purposes. + +### Cookies +The Website may use a feature of your browser to set a "cookie" on your computer. Cookies are small packets of information that a website stores on your computer. The Web site can then read the cookies whenever you visit the Website. We may use cookies in a number of ways, such as to save an authentication token so that you don't have to enter your username and password each time you visit the Website, to deliver content specific to your interests and to track the pages you've visited on the Website. These cookies allow us to use the information we collect to customize your Website experience so that your visit to the Website is as relevant and as valuable to you as possible. + +Most browser software can be set up to handle cookies. You may modify your browser preference to provide you with choices relating to cookies. You have the choice to accept all cookies, to be notified when a cookie is set or to reject all cookies. If you choose to reject cookies, certain functions and conveniences of the Website may not work properly, and you may be unable to use those Website services that require registration in order to participate, or you will have to re-enter your username and password each time you visit the Website. Most browsers offer instructions on how to reset the browser to reject cookies in the "Help" section of the toolbar. We do not link non-personal information from cookies to personally identifiable information without your permission. + +### Web Beacons +The Website also may use Web beacons to collect non-personal information about your use of the Website. The information collected by web beacons allows us to statistically monitor how many people are using the Website, how many people open our emails, and for what purposes these actions are being taken. Our web beacons are not used to track your activity outside of the Website. .NET Foundation does not link non-personal information from web beacons to personally identifiable information without your permission. + +### Personal Information +Personal information is information that is associated with your name or personal identity. .NET Foundation uses personal information to better understand your needs and interests and to provide you with better service. On some Website pages, you may be able to view information about NuGet packages, download NuGet packages, provide feedback, submit information into registries, register for notifications, or upload NuGet packages. The types of personal information you provide to us on these pages may include name, address, phone number, email address, user IDs, passwords, or other information. + +## HOW WE USE YOUR INFORMATION +.NET Foundation may use non-personal data that is aggregated for reporting about the Website usability, performance, and effectiveness. It may be used to improve the experience, usability, and content of the Website. + +### Information Sharing +We do not sell, rent, or lease any individual's personal information or lists of email addresses to anyone for marketing purposes, and we take commercially reasonable steps to maintain the security of this information. However, .NET Foundation reserves the right to supply any such information to any organization into which .NET Foundation may merge in the future or to which it may make any transfer in order to enable a third party to continue part or all of .NET Foundation's mission. We also reserve the right to release personal information to protect our systems or business, when we reasonably believe you to be in violation of our Terms of Use or if we reasonably believe you to have initiated or participated in any illegal activity. In addition, please be aware that in certain circumstances, .NET Foundation may be obligated to release your personal information pursuant to judicial or other government subpoenas, warrants, or other orders. Certain NuGet repositories on the Website may require you to agree to a third party's legal terms in order to upload or access NuGet packages. We may provide your personal information to the third party if you accept such legal terms. + +In keeping with our open process, .NET Foundation may maintain publicly accessible archives for Website activities. For example, submitting the report abuse form may result in your description of the abuse becoming part of the publicly accessible archives. In all such cases, we will ensure to the greatest degree possible, that personal information is protected. + +Please remember that any information (including personal information) that you disclose in public areas of the Website, such as NuGet package uploads, discussions, and social networking features, becomes public information that others may collect, circulate, and use. Because we cannot and do not control the acts of others, you should exercise caution when deciding to disclose information about yourself or others in public forums such as these. + +Given the international scope of .NET Foundation, personal information may be visible to persons outside your country of residence, including to persons in countries that your own country's privacy laws and regulations deem deficient in ensuring an adequate level of protection for such information. If you are unsure whether this privacy statement is in conflict with applicable local rules, you should not submit your information. If you are located within the European Union, you should note that your information will be transferred to the United States, which is deemed by the European Union to have inadequate data protection. Nevertheless, in accordance with local laws implementing European Union Directive 95/46/EC of 24 October 1995 on the protection of individuals with regard to the processing of personal data and on the free movement of such data ("EU Privacy Directive"), individuals located in countries outside of the United States of America who submit personal information do thereby consent to the general use of such information as provided in this Privacy Statement and to its transfer to and/or storage in the United States of America. + +If you do not want your personal information collected and used by .NET Foundation, please do not visit the Website or download or upload NuGet packages. + +### Security +.NET Foundation makes reasonable efforts to protect personal information by users of the Website, including using firewalls and other security measures on its servers. No server, however, is 100% secure, and you should take this into account when submitting personal or confidential information about yourself on the Website. Much of the personal information is used in conjunction with participation-level services such as sharing and exchanging NuGet packages, so some types of personal information such as your name, company affiliation, and email address will be visible to other participants and to the public. .NET Foundation assumes no liability for the interception, alteration, or misuse of the information you provide. You alone are responsible for maintaining the secrecy of your personal information. Please use care when you access the Website and provide personal information. + +### Opting Out +From time to time .NET Foundation may email you electronic newsletters, announcements, surveys or other information. If you prefer not to receive any or all of these communications, you may opt out by following the directions provided within the electronic newsletters and announcements. + +### Contacting Us +Questions about this Privacy Statement can be directed to info@nuget.org. diff --git a/content/INT/Privacy-Policy.md b/content/INT/Privacy-Policy.md index 1537eb6ef8..1a034abc10 100644 --- a/content/INT/Privacy-Policy.md +++ b/content/INT/Privacy-Policy.md @@ -41,7 +41,7 @@ Outercurve may use non-personal data that is aggregated for reporting about the ### Information Sharing Outercurve does not sell, rent, or lease any individual's personal information or lists of email addresses to anyone for marketing purposes, and we take commercially reasonable steps to maintain the security of this information. However, Outercurve reserves the right to supply any such information to any organization into which Outercurve may merge in the future or to which it may make any transfer in order to enable a third party to continue part or all of the Council's mission. We also reserve the right to release personal information to protect our systems or business, when we reasonably believe you to be in violation of our Terms of Use or if we reasonably believe you to have initiated or participated in any illegal activity. In addition, please be aware that in certain circumstances, Outercurve may be obligated to release your personal information pursuant to judicial or other government subpoenas, warrants, or other orders. -In keeping with our open process, Outercurve may maintain publicly accessible archives for Web site activities. For example, submitting the report abuse form may result in your description of the abuse becoming part of the publically accessible archives. In all such cases, we will ensure to the greatest degree possible, that personal information is protected. +In keeping with our open process, Outercurve may maintain publicly accessible archives for Web site activities. For example, submitting the report abuse form may result in your description of the abuse becoming part of the publicly accessible archives. In all such cases, we will ensure to the greatest degree possible, that personal information is protected. Please remember that any information (including personal information) that you disclose in public areas of the Web site, such as NuGet package uploads, discussions, and social networking features, becomes public information that others may collect, circulate, and use. Because we cannot and do not control the acts of others, you should exercise caution when deciding to disclose information about yourself or others in public forums such as these. @@ -56,4 +56,4 @@ Outercurve makes every effort to protect personal information by users of the We From time to time Outercurve may email you electronic newsletters, announcements, surveys or other information. If you prefer not to receive any or all of these communications, you may opt out by following the directions provided within the electronic newsletters and announcements. ### Contacting Us -Questions about this Privacy Statement can be directed to info@nuget.org. \ No newline at end of file +Questions about this Privacy Statement can be directed to info@nuget.org. diff --git a/content/PROD/Home.html b/content/PROD/Home.html index f3eda15c32..d05f851a08 100644 --- a/content/PROD/Home.html +++ b/content/PROD/Home.html @@ -31,13 +31,13 @@

What is NuGet?

Latest NuGet Releases

- NuGet 3.4.3 for Visual Studio 2015 was released on April 22nd, 2016. NuGet 2.8.7 for Visual Studio 2013 was released on July 27th 2015. Upgrade now using the Visual Studio Extension Manager. + NuGet 3.5 for Visual Studio 2015 was released on October 27th, 2016. NuGet 2.12 for Visual Studio 2013 was released on June 27th 2016. Download these releases from http://nuget.org/downloads.

- For details about what's in the 3.4.3 release, read the release notes. + For details about what's in the 3.5 release, read the release notes.

- For details about what's in the 2.8.7 release, read the release notes. + For details about what's in the 2.12 release, read the release notes.

diff --git a/content/PROD/Privacy-Policy.md b/content/PROD/Privacy-Policy.md index a2a37c85ab..79d86a09c4 100644 --- a/content/PROD/Privacy-Policy.md +++ b/content/PROD/Privacy-Policy.md @@ -20,7 +20,7 @@ The Website may provide links to third-party websites for the convenience of our Non-personal information is data about usage and service operation that is not directly associated with a specific personal identity. .NET Foundation may collect and analyze non-personal information to evaluate how visitors use the Website. ### Aggregate Information -.NET Foundation may gather aggregate information, which refers to information your computer automatically provides to us and which cannot be tied back to you as a specific individual. Examples include referral data (the websites you visited just before and just after the Website), the Website pages viewed, time spent at the site, and Internet Protocol (IP) addresses. An IP address is a number that is automatically assigned to your computer whenever you access the Internet. For example, when you request a page from teh Website, our servers log your IP address to create aggregate reports on user demographics and traffic patterns and for purposes of system administration. +.NET Foundation may gather aggregate information, which refers to information your computer automatically provides to us and which cannot be tied back to you as a specific individual. Examples include referral data (the websites you visited just before and just after the Website), the Website pages viewed, time spent at the site, and Internet Protocol (IP) addresses. An IP address is a number that is automatically assigned to your computer whenever you access the Internet. For example, when you request a page from the Website, our servers log your IP address to create aggregate reports on user demographics and traffic patterns and for purposes of system administration. ### Log Files Every time you request or download a file from the Website, .NET Foundation may store data about these events and your IP address in a log file. .NET Foundation may use this information to analyze trends, administer the Website, track users' movements, and gather broad demographic information for aggregate use or for other business purposes. @@ -42,7 +42,7 @@ Personal information is information that is associated with your name or persona ### Information Sharing We do not sell, rent, or lease any individual's personal information or lists of email addresses to anyone for marketing purposes, and we take commercially reasonable steps to maintain the security of this information. However, .NET Foundation reserves the right to supply any such information to any organization into which .NET Foundation may merge in the future or to which it may make any transfer in order to enable a third party to continue part or all of .NET Foundation's mission. We also reserve the right to release personal information to protect our systems or business, when we reasonably believe you to be in violation of our Terms of Use or if we reasonably believe you to have initiated or participated in any illegal activity. In addition, please be aware that in certain circumstances, .NET Foundation may be obligated to release your personal information pursuant to judicial or other government subpoenas, warrants, or other orders. Certain NuGet repositories on the Website may require you to agree to a third party's legal terms in order to upload or access NuGet packages. We may provide your personal information to the third party if you accept such legal terms. -In keeping with our open process, .NET Foundation may maintain publicly accessible archives for Website activities. For example, submitting the report abuse form may result in your description of the abuse becoming part of the publically accessible archives. In all such cases, we will ensure to the greatest degree possible, that personal information is protected. +In keeping with our open process, .NET Foundation may maintain publicly accessible archives for Website activities. For example, submitting the report abuse form may result in your description of the abuse becoming part of the publicly accessible archives. In all such cases, we will ensure to the greatest degree possible, that personal information is protected. Please remember that any information (including personal information) that you disclose in public areas of the Website, such as NuGet package uploads, discussions, and social networking features, becomes public information that others may collect, circulate, and use. Because we cannot and do not control the acts of others, you should exercise caution when deciding to disclose information about yourself or others in public forums such as these. @@ -57,4 +57,4 @@ If you do not want your personal information collected and used by .NET Foundati From time to time .NET Foundation may email you electronic newsletters, announcements, surveys or other information. If you prefer not to receive any or all of these communications, you may opt out by following the directions provided within the electronic newsletters and announcements. ### Contacting Us -Questions about this Privacy Statement can be directed to info@nuget.org. \ No newline at end of file +Questions about this Privacy Statement can be directed to info@nuget.org. diff --git a/content/PROD/Team.json b/content/PROD/Team.json index 137eb66fa0..d7d40444ea 100644 --- a/content/PROD/Team.json +++ b/content/PROD/Team.json @@ -24,5 +24,28 @@ "sblom", "zhili1208", "xavierdecoster", - "maartenba" + "maartenba", + "scottbommarito", + "joelverhagen", + "shishirx34", + "ryuyu", + "skofman1", + "chenriksson", + "cristinamanum", + "alpaix", + "anangaur", + "diverdan92", + "DoRonMotter", + "drewgillies", + "dtivel", + "ericstj", + "jainaashish", + "jasonmalinowski", + "jonwchu", + "karann-msft", + "mishra14", + "nkolev92", + "rohit21agrawal", + "rrelyea", + "yishaigalatzer" ] \ No newline at end of file diff --git a/ops/COPYRIGHT.md b/ops/COPYRIGHT.md deleted file mode 100644 index 0cbaef5a72..0000000000 --- a/ops/COPYRIGHT.md +++ /dev/null @@ -1,13 +0,0 @@ - Copyright 2015 .NET Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/ops/Environments.sample.xml b/ops/Environments.sample.xml deleted file mode 100644 index 4dbba5eeca..0000000000 --- a/ops/Environments.sample.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/ops/LICENSE.md b/ops/LICENSE.md deleted file mode 100644 index bba5ab557a..0000000000 --- a/ops/LICENSE.md +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/ops/Modules/NuGetOps/NuGetOps.csproj b/ops/Modules/NuGetOps/NuGetOps.csproj deleted file mode 100644 index 4ab9937cd8..0000000000 --- a/ops/Modules/NuGetOps/NuGetOps.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - {0DB976EC-BB58-471A-AC68-1F92CD6343C2} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ops/Modules/NuGetOps/NuGetOps.psm1 b/ops/Modules/NuGetOps/NuGetOps.psm1 deleted file mode 100644 index 566caa085d..0000000000 --- a/ops/Modules/NuGetOps/NuGetOps.psm1 +++ /dev/null @@ -1,228 +0,0 @@ -. "$PsScriptRoot\_EnvironmentLoaders.ps1" - -$Global:RepoRoot = (Convert-Path "$PsScriptRoot\..\..\..") -$Global:OpsRoot = (Convert-Path "$PsScriptRoot\..\..") -$Global:NuGetOpsDefinition = $env:NUGET_OPS_DEFINITION - -$GalOpsRoot = Join-Path $RepoRoot "src\galops" - -$CurrentDeployment = $null -$CurrentEnvironment = $null -Export-ModuleMember -Variable CurrentDeployment, CurrentEnvironment - -# Extract Ops NuGetOpsVersion -$NuGetOpsVersion = - cat (Join-Path $RepoRoot "src\CommonAssemblyInfo.cs") | - where { $_ -match "\[assembly:\s+AssemblyInformationalVersion\(`"(?[^`"]*)`"\)\]" } | - foreach { $matches["ver"] } - -# Find the Azure SDK -$SDKParent = "$env:ProgramFiles\Microsoft SDKs\Windows Azure\.NET SDK" -$Global:AzureSDKRoot = $null; -if(Test-Path $SDKParent) { - # Pick the latest - $AzureSDKRoot = (dir $SDKParent | sort Name -desc | select -first 1).FullName -} - -# Find the NuGet Internal repo -$Global:ConfigRoot = $env:NUGET_CONFIG_ROOT -$guessedRoot = Join-Path (Split-Path -Parent $Global:RepoRoot) "NuGetMicrosoft\NuGetInternal\Config" -if(!$Global:ConfigRoot) { - Write-Host "NUGET_CONFIG_ROOT not set, searching for repo..." - $Global:ConfigRoot = $guessedRoot -} - -if(!(Test-Path $Global:ConfigRoot)) { - $Global:ConfigRoot = $null; - Write-Warning "Could not find NuGet Configuration Files Path." - Write-Warning "If you put it in $guessedRoot, we'll find it automatically" - Write-Warning "Otherwise, set the NUGET_CONFIG_ROOT environment variable to the root path" - Write-Warning "If you are not deploying NuGet.org, set NUGET_CONFIG_ROOT to a path that contains CSCFG files for your service" -} -if($Global:ConfigRoot -and (@(dir "$Global:ConfigRoot\*.cscfg").Length -eq 0)) { - Write-Warning "Found no CSCFG files in $($Global:ConfigRoot)" -} - -if(!$AzureSDKRoot) { - Write-Warning "Couldn't find the Azure SDK. Some commands may not work." -} else { - Write-Host "Using Azure SDK at: $AzureSDKRoot" -} - -$accounts = @(Get-AzureAccount) -if($accounts.Length -eq 0) { - Write-Warning "No Azure Accounts found. Run Add-AzureAccount to configure your Azure account." -} - -# Try to load V3 Environments -$Global:ServiceModel = Get-V3Environments -NuGetOpsDefinition $NuGetOpsDefinition -if(!$Global:ServiceModel) { - $Global:ServiceModel = Get-V2Environments -NuGetOpsDefinition $NuGetOpsDefinition -} -$Global:Environments = $ServiceModel.Environments -$Global:Subscriptions = $ServiceModel.Subscriptions - -function Get-Environment([switch]$ListAvailable) { - if($ListAvailable) { - @($Environments.Keys | ForEach-Object { - if(Test-Environment $_) { - "* $_" - } else { - " $_" - } - }) - } else { - if(!$CurrentEnvironment) { - $null; - } else { - $CurrentEnvironment.Name - } - } -} -Export-ModuleMember -Function Get-Environment - -function Test-Environment([Parameter(Mandatory=$true)][String]$Environment, [Switch]$Exists) { - if($Exists) { - return $Environments.ContainsKey($Environment) - } else { - [String]::Equals((Get-Environment), $Environment, "OrdinalIgnoreCase"); - } -} -Export-ModuleMember -Function Test-Environment - -function _IsProduction { - $CurrentEnvironment -and ($CurrentEnvironment.Protected -eq "true") -} - -function _RefreshGitColors { - $global:GitPromptSettings = New-Object PSObject -Property @{ - DefaultForegroundColor = $Host.UI.RawUI.ForegroundColor - - BeforeText = ' [' - BeforeForegroundColor = [ConsoleColor]::Yellow - BeforeBackgroundColor = $Host.UI.RawUI.BackgroundColor - DelimText = ' |' - DelimForegroundColor = [ConsoleColor]::Yellow - DelimBackgroundColor = $Host.UI.RawUI.BackgroundColor - - AfterText = ']' - AfterForegroundColor = [ConsoleColor]::Yellow - AfterBackgroundColor = $Host.UI.RawUI.BackgroundColor - - BranchForegroundColor = [ConsoleColor]::Cyan - BranchBackgroundColor = $Host.UI.RawUI.BackgroundColor - BranchAheadForegroundColor = [ConsoleColor]::Green - BranchAheadBackgroundColor = $Host.UI.RawUI.BackgroundColor - BranchBehindForegroundColor = [ConsoleColor]::Red - BranchBehindBackgroundColor = $Host.UI.RawUI.BackgroundColor - BranchBehindAndAheadForegroundColor = [ConsoleColor]::Yellow - BranchBehindAndAheadBackgroundColor = $Host.UI.RawUI.BackgroundColor - - BeforeIndexText = "" - BeforeIndexForegroundColor= [ConsoleColor]::DarkGreen - BeforeIndexBackgroundColor= $Host.UI.RawUI.BackgroundColor - - IndexForegroundColor = [ConsoleColor]::DarkGreen - IndexBackgroundColor = $Host.UI.RawUI.BackgroundColor - - WorkingForegroundColor = [ConsoleColor]::DarkRed - WorkingBackgroundColor = $Host.UI.RawUI.BackgroundColor - - UntrackedText = ' !' - UntrackedForegroundColor = [ConsoleColor]::DarkRed - UntrackedBackgroundColor = $Host.UI.RawUI.BackgroundColor - - ShowStatusWhenZero = $true - - AutoRefreshIndex = $true - - EnablePromptStatus = !$GitMissing - EnableFileStatus = $true - RepositoriesInWhichToDisableFileStatus = @( ) # Array of repository paths - - Debug = $false - } - if(_IsProduction) { - $GitPromptSettings.WorkingForegroundColor = [ConsoleColor]::Yellow - $GitPromptSettings.UntrackedForegroundColor = [ConsoleColor]::Yellow - $GitPromptSettings.IndexForegroundColor = [ConsoleColor]::Cyan - } -} - -function env([string]$Name) { - if([String]::IsNullOrEmpty($Name)) { - Get-Environment -ListAvailable - } else { - Set-Environment -Name $Name - } -} -Export-ModuleMember -Function env - -$Global:GalOpsExe = join-path $GalOpsRoot "bin\Debug\galops.exe" -if(!(Test-Path $GalOpsExe)) { - $answer = Read-Host "Gallery ops exe not built. Build it now? (Y/n)" - if([String]::IsNullOrEmpty($answer) -or $answer.Equals("y", "OrdinalIgnoreCase") -or $answer.Equals("yes", "OrdinalIgnoreCase")) { - pushd $RepoRoot - Write-Host "Building GalOps.exe..." - & msbuild /v:m | Out-Host - popd - } else { - Write-Host -Background Yellow -Foreground Black "Warning: Do not execute gallery ops tasks until you have built the GalOps.exe executable" - } -} - -# Load Private Functions -dir $PsScriptRoot\Private\*.ps1 | foreach { - . $_ -} - -# Load Public Functions -dir $PsScriptRoot\Public\*.ps1 | foreach { - . $_ - Export-ModuleMember -Function "$([IO.Path]::GetFileNameWithoutExtension($_.Name))" -} - - - -#Clear-Host -Write-Host -BackgroundColor Blue -ForegroundColor White @" - _____ _____ _ _____ _____ _____ -| | |_ _| __|___| | | | | __| -| | | | | | | | | - | | | | |__|__ | -|_|___|___|_____|___|_| |_____|__| |_____| - -"@ -Write-Host -ForegroundColor Black -BackgroundColor Yellow "Welcome to the NuGet Operations Console (v$NuGetOpsVersion)" - -if($Environments.Count -eq 0) { - Write-Warning "No environments are available, the console will not function correctly.`r`nSee https://github.com/NuGet/NuGetOperations/wiki/Setting-up-the-Operations-Console for more info" -} -if(!(Test-Path "$env:ProgramFiles\Microsoft SDKs\Windows Azure\.NET SDK\")) { - Write-Warning "Couldn't find the Azure .NET SDK. Some operations may not work without it." -} - -function Write-NuGetOpsPrompt() { - $envName = "" - if($CurrentEnvironment) { $env = $CurrentEnvironment.Name; } - $host.UI.RawUI.WindowTitle = "NuGet Operations Console v$NuGetOpsVersion [Environment: $env]" - - Write-Host -noNewLine "$(Get-Location)" - - $realLASTEXITCODE = $LASTEXITCODE - - # Reset color, which can be messed up by Enable-GitColors - $Host.UI.RawUI.ForegroundColor = $GitPromptSettings.DefaultForegroundColor - - Write-VcsStatus - - $global:LASTEXITCODE = $realLASTEXITCODE - Write-Host - Write-Host -noNewline "[env:" - if(_IsProduction) { - Write-Host -noNewLine -foregroundColor Yellow $env - } else { - Write-Host -noNewLine -foregroundColor Magenta $env - } - return "]> " -} -Export-ModuleMember -Function Write-NuGetOpsPrompt \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Private/DeployWebsite.ps1 b/ops/Modules/NuGetOps/Private/DeployWebsite.ps1 deleted file mode 100644 index cf2a7a85c7..0000000000 --- a/ops/Modules/NuGetOps/Private/DeployWebsite.ps1 +++ /dev/null @@ -1,45 +0,0 @@ -function DeployWebsite($Service, $Package) { - $MSDeployKey = 'HKLM:\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\3' - if(!(Test-Path $MSDeployKey)) { - throw "Could not find MSDeploy. Use Web Platform Installer to install the 'Web Deployment Tool' and re-run this command" - } - $InstallPath = (Get-ItemProperty $MSDeployKey).InstallPath - if(!$InstallPath -or !(Test-Path $InstallPath)) { - throw "Could not find MSDeploy. Use Web Platform Installer to install the 'Web Deployment Tool' and re-run this command" - } - - $MSDeploy = Join-Path $InstallPath "msdeploy.exe" - if(!(Test-Path $MSDeploy)) { - throw "Could not find MSDeploy. Use Web Platform Installer to install the 'Web Deployment Tool' and re-run this command" - } - - $Site = RunInSubscription $Service.Environment.Subscription.Name { - Write-Host "Downloading Site configuration for $($Service.ID)" - Get-AzureWebsite $Service.ID - } - - if(!$Site) { - throw "Failed to load site: $($Service.ID)" - } - - # MSDeploy Settings - $UserName = "`$$($Site.Name)" - $Password = $Site.PublishingPassword - - # HACK: Hack up the SelfLink to point at the publish endpoint - $subdomain = $Site.SelfLink.Host.Split(".")[0] - $PublishUrl = "https://$subdomain.publish.azurewebsites.windows.net:443/msdeploy.axd?Site=$($Site.Name)" - - # DEPLOY! - Write-Host "Deploying package to $PublishUrl for $($Site.Name)" - - $arguments = [string[]]@( - "-verb:sync", - "-source:package='$Package'", - "-dest:auto,computerName='$PublishUrl',userName='$UserName',password='$Password',authtype='Basic',includeAcls='False'", - "-setParam:name='IIS Web Application Name',value='$($Site.Name)'") - - Write-Verbose "msdeploy $arguments" - #&$msdeploy @arguments - Start-Process $msdeploy -ArgumentList $arguments -NoNewWindow -Wait -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Private/EnsureService.ps1 b/ops/Modules/NuGetOps/Private/EnsureService.ps1 deleted file mode 100644 index e85db6f50f..0000000000 --- a/ops/Modules/NuGetOps/Private/EnsureService.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -function EnsureService($Service) { - if($Service -is [string]) { - if(!$CurrentEnvironment) { - throw "This command requires an environment" - } - $Service = Get-NuGetService $Service -ForceSingle - } - $Service -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Private/GetDeployment.ps1 b/ops/Modules/NuGetOps/Private/GetDeployment.ps1 deleted file mode 100644 index 975f9938de..0000000000 --- a/ops/Modules/NuGetOps/Private/GetDeployment.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -function GetDeployment($Service, $Slot = "production") { - if(!$Service.ID -or !$Service.Environment) { - throw "Invalid Service object provided" - } - - if(!$Service.Environment.Subscription) { - throw "No Subscription is available for this environment. Do you have access?" - } - - if($Service.Type -eq "Website") { - if($Slot -ne "production") { - Write-Warning "Websites do not support multiple slots, ignoring -Slot parameter" - } - RunInSubscription $Service.Environment.Subscription.Name { - Get-AzureWebsite -Name $Service.ID - } - } elseif($Service.Type -eq "CloudService") { - RunInSubscription $Service.Environment.Subscription.Name { - Get-AzureDeployment -ServiceName $Service.ID -Slot $Slot - } - } -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Private/ParseConfigurationSettings.ps1 b/ops/Modules/NuGetOps/Private/ParseConfigurationSettings.ps1 deleted file mode 100644 index 4005749ca8..0000000000 --- a/ops/Modules/NuGetOps/Private/ParseConfigurationSettings.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -function ParseConfigurationSettings($file) { - $xml = [xml](cat $file) - $role = $xml.ServiceConfiguration.Role | Select-Object -First 1 - - $hash = @{} - $role.ConfigurationSettings.Setting | ForEach-Object { - $hash[$_.name] = $_.value - } - $hash -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Private/RunInSubscription.ps1 b/ops/Modules/NuGetOps/Private/RunInSubscription.ps1 deleted file mode 100644 index 8b34ec8449..0000000000 --- a/ops/Modules/NuGetOps/Private/RunInSubscription.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -function RunInSubscription($name, [scriptblock]$scriptblock) { - $oldSub = Get-AzureSubscription | Where-Object { $_.IsDefault } - if(!$oldSub -or ($oldSub.SubscriptionName -ne $name)) { - Select-AzureSubscription $name - } - $scriptblock.Invoke(); - if($oldSub -and ($oldSub.SubscriptionName -ne $name)) { - Select-AzureSubscription $oldSub.SubscriptionName - } -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Clear-Environment.ps1 b/ops/Modules/NuGetOps/Public/Clear-Environment.ps1 deleted file mode 100644 index 738e495a56..0000000000 --- a/ops/Modules/NuGetOps/Public/Clear-Environment.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -<# -.SYNOPSIS -Clears the active NuGet Environment -#> -function Clear-Environment { - if($Global:OldBgColor) { - $Host.UI.RawUI.BackgroundColor = $Global:OldBgColor - del variable:\OldBgColor - } - $prod = _IsProduction - _RefreshGitColors - if($prod) { - Clear-Host - } - del variable:\CurrentEnvironment - del variable:\CurrentDeployment -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Copy-AppSettings.ps1 b/ops/Modules/NuGetOps/Public/Copy-AppSettings.ps1 deleted file mode 100644 index 6bb900a988..0000000000 --- a/ops/Modules/NuGetOps/Public/Copy-AppSettings.ps1 +++ /dev/null @@ -1,75 +0,0 @@ -<# -.SYNOPSIS -Copies app settings from one service to another - -.DESCRIPTION - -.PARAMETER From -The service to copy from - -.PARAMETER To -The service to copy to -#> -function Copy-AppSettings { - [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="high")] - param( - [Parameter(Mandatory=$true, Position=0)][string]$From, - [Parameter(Mandatory=$true, Position=1)][string]$To - ) - if(!$CurrentEnvironment) { - throw "This command requires an environment" - } - - # Get the services - $FromService = Get-NuGetService $From -ForceSingle - $ToService = Get-NuGetService $To -ForceSingle - if(!$FromService -or !$ToService) { - return - } - - Write-Host "Downloading configuration for the source service '$($FromService.ID)'" - $FromDeployment = GetDeployment $FromService - $FromSettings = Get-NuGetServiceConfiguration $FromDeployment - - Write-Host "Downloading configuration for the destination service '$($ToService.ID)'" - $ToDeployment = GetDeployment $ToService - $ToSettings = Get-NuGetServiceConfiguration $ToDeployment - - $Changes = @(); - $FromSettings.Keys | where { !$_.StartsWith("Microsoft.") } | foreach { - if($ToSettings[$_] -ne $FromSettings[$_]) { - $change = New-Object PSCustomObject; - Add-Member -NotePropertyMembers @{ - "Key"=$_; - "From"=$ToSettings[$_]; - "To"=$FromSettings[$_]; - } -InputObject $change - $Changes += $change - } - } - - if($Changes.Length -eq 0) { - Write-Host "No settings to update!" - } - else { - $Changes | Format-Table Key,From,To - if($PsCmdlet.ShouldProcess($ToService.ID, "Apply the above settings")) { - $Changes | ForEach { - $ToSettings[$_.Key] = $_.To - } - Set-NuGetServiceConfiguration $ToService $ToSettings - } - } - -<# - $FromSettings | foreach { - if($ToSettings[$_] -ne $FromSettings[$_]) { - Write-Verbose "Updating $_" - $ToSettings[$_] = $FromSettings[$_] - } - } - - Write-Host "New Settings" - $ToSettings - #> -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Enable-AzurePowerShell.ps1 b/ops/Modules/NuGetOps/Public/Enable-AzurePowerShell.ps1 deleted file mode 100644 index c236578bc6..0000000000 --- a/ops/Modules/NuGetOps/Public/Enable-AzurePowerShell.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -<# -.SYNOPSIS -Sets up the Azure PowerShell Module for accessing a set of subscriptions -#> -function Enable-AzurePowerShell { - throw "No longer needed! Use Add-AzureAccount to configure your Azure account." -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Get-AzureManagementCertificate.ps1 b/ops/Modules/NuGetOps/Public/Get-AzureManagementCertificate.ps1 deleted file mode 100644 index 317a630595..0000000000 --- a/ops/Modules/NuGetOps/Public/Get-AzureManagementCertificate.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -<# -.SYNOPSIS -Gets the remote desktop certificate for the current environment if one is installed - -.DESCRIPTION -#> -function Get-AzureManagementCertificate { - param([Parameter(Mandatory=$false, Position=0)][string]$SubscriptionName) - - if(!$SubscriptionName) { - if(!$CurrentEnvironment) { - throw "This command requires an environment or a subscription name" - } - $SubscriptionName = $CurrentEnvironment.Subscription.Name - } - - $subName = $SubscriptionName.Replace(" ", "") - $CertPrefix = "CN=Azure-$subName-*" - - dir "cert:\CurrentUser\My" | where { $_.Subject -like $CertPrefix } | select -first 1 -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Get-Configuration.ps1 b/ops/Modules/NuGetOps/Public/Get-Configuration.ps1 deleted file mode 100644 index a522c8d55a..0000000000 --- a/ops/Modules/NuGetOps/Public/Get-Configuration.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -<# -.SYNOPSIS -Lists the available configuration files for a service - -.PARAMETER Type -The type of service to list configurations for -#> -function Get-Configuration { - [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="high")] - param( - [Parameter(Mandatory=$false, Position=0)][string]$Type - ) - - if(!$ConfigRoot) { - return; - } - - dir "$ConfigRoot\*.cscfg" | ForEach-Object { - $match = [Regex]::Match($_.Name, "(?.*)\.(?.*)\.cscfg") - if($match.Success) { - $cfg = New-Object PSCustomObject - Add-Member -InputObject $cfg -NotePropertyMembers @{ - "Type" = $match.Groups["type"].Value; - "Environment" = $match.Groups["env"].Value; - "File" = $_.FullName - } - $cfg - } - } | Where-Object { - (([String]::IsNullOrEmpty($Type)) -or ($Type -eq $_.Type)) -and - (($CurrentEnvironment -eq $null) -or ($CurrentEnvironment.Name -eq $_.Environment)) - } -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Get-ConfigurationSettings.ps1 b/ops/Modules/NuGetOps/Public/Get-ConfigurationSettings.ps1 deleted file mode 100644 index f95b8c2543..0000000000 --- a/ops/Modules/NuGetOps/Public/Get-ConfigurationSettings.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -<# -.SYNOPSIS -Lists the available configuration files for a service - -.PARAMETER Type -The type of service to list configurations settings for -#> -function Get-ConfigurationSettings { - [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="high")] - param( - [Parameter(Mandatory=$true, Position=1)][string]$Type - ) - - if(!$ConfigRoot) { - return; - } - - if(!$CurrentEnvironment) { - throw "Requires a current environment" - } - - $Config = Get-Configuration -Type $Type | Select-Object -First 1 - - ParseConfigurationSettings $Config.File -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Get-NuGetService.ps1 b/ops/Modules/NuGetOps/Public/Get-NuGetService.ps1 deleted file mode 100644 index 1f0179479a..0000000000 --- a/ops/Modules/NuGetOps/Public/Get-NuGetService.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -<# -.SYNOPSIS -Gets the service with the specified name/ID or ones that match that substring - -.PARAMETER Thumbprint -The Thumbprint of the certificate to use to encrypt. MUST BE INSTALLED. -#> -function Get-NuGetService { - param( - [Parameter(Mandatory=$false, Position=0)][string]$Name, - [Parameter(Mandatory=$false)][switch]$ForceSingle) - - if(!$CurrentEnvironment) { - throw "This command requires an environment" - } - - $candidates = @($CurrentEnvironment.Services) - if($Name) { - $candidates = @($candidates | where { ($_.Name -like "*$Name*") -or ($_.ID -eq "*$Name*") }) - } - - $exactMatch = @($candidates | where { ($_.Name -eq $Name) -or ($_.ID -eq $Name) }) - if($exactMatch.Length -eq 1) { - $exactMatch[0] - } - else { - if($ForceSingle) { - if($candidates.Length -eq 1) { - $candidates[0] - } elseif($candidates.Length -gt 1) { - throw "Multiple matches for $Name found: $candidates" - } - } else { - $candidates - } - } -} -Set-Alias -Name svc -Value Get-NuGetService -Export-ModuleMember -Alias svc \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Get-NuGetServiceConfiguration.ps1 b/ops/Modules/NuGetOps/Public/Get-NuGetServiceConfiguration.ps1 deleted file mode 100644 index 46783d975f..0000000000 --- a/ops/Modules/NuGetOps/Public/Get-NuGetServiceConfiguration.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -<# -.SYNOPSIS -Gets the config settings for the specified service - -.PARAMETER Service -The service to get configuration for -#> -function Get-NuGetServiceConfiguration { - param( - [Parameter(Mandatory=$true, Position=0)]$Service) - - $dep = $null; - $type = $null; - $Service = EnsureService $Service - - if($Service.AppSettings) { - # Website deployment - $type = "Website" - $dep = $Service - } elseif($Service.Configuration) { - # Cloud Service deployment - $type = "CloudService" - $dep = $Service - } elseif(!$Service.ID -or !$Service.Environment) { - throw "Unknown service object" - } else { - Write-Host "Downloading config for $($Service.ID)..." - $type = $Service.Type - $dep = GetDeployment $Service - } - - - if($type -eq "Website") { - $settings = $dep.AppSettings - $dep.ConnectionStrings | foreach { - $settings[$_.Name] = $_.ConnectionString - } - $settings - } else { - $x = [xml]($dep.Configuration); - $table = @{}; - $role = $x.ServiceConfiguration.Role | select -first 1 - Write-Host "Using config for role '$($role.name)'" - $role.ConfigurationSettings.Setting | foreach { - $table.Add($_.name, $_.value) - } - $table - } -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Get-RandomPassword.ps1 b/ops/Modules/NuGetOps/Public/Get-RandomPassword.ps1 deleted file mode 100644 index c33dc8ffbb..0000000000 --- a/ops/Modules/NuGetOps/Public/Get-RandomPassword.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -<# -.SYNOPSIS -Returns a random, timestamped, password -#> -function Get-RandomPassword { - # Base64-encode the Guid to add some additional characters - [DateTime]::Now.ToString("MMMddyy") + "!" + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([Guid]::NewGuid().ToString())) -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Get-RemoteDesktopCertificate.ps1 b/ops/Modules/NuGetOps/Public/Get-RemoteDesktopCertificate.ps1 deleted file mode 100644 index 12449bf1bf..0000000000 --- a/ops/Modules/NuGetOps/Public/Get-RemoteDesktopCertificate.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -<# -.SYNOPSIS -Gets the remote desktop certificate for the current environment if one is installed - -.DESCRIPTION -#> -function Get-RemoteDesktopCertificate { - if(!$CurrentEnvironment) { - throw "This command requires an environment" - } - - $CertificateName = "nuget-$($CurrentEnvironment.Name)" - - dir cert:\CurrentUser\My | where { $_.FriendlyName -eq $CertificateName } | select -first 1 -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Get-RemoteDesktopPassword.ps1 b/ops/Modules/NuGetOps/Public/Get-RemoteDesktopPassword.ps1 deleted file mode 100644 index 97e48a56e1..0000000000 --- a/ops/Modules/NuGetOps/Public/Get-RemoteDesktopPassword.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -<# -.SYNOPSIS -Returns a random, timestamped, password AND an encrypted password suitable for use as a Remote Desktop password - -.PARAMETER Thumbprint -The Thumbprint of the certificate to use to encrypt. If not specified, the default one for this environment will be used, if present. Certificate MUST BE INSTALLED. -#> -function Get-RemoteDesktopPassword { - param( - [Parameter(Mandatory=$false)][string]$Thumbprint) - - if(!$AzureSDKRoot) { - throw "This command requires the Azure .NET SDK" - } - - if(!$Thumbprint) { - $CertificateName = "nuget-$($CurrentEnvironment.Name)" - $cert = Get-RemoteDesktopCertificate - if(!$cert) { - throw "Environment RDP certificate is not installed. Download the '$CertificateName' certificate from the secret store and install it" - } - $Thumbprint = $cert.Thumbprint - } - - $plainText = Get-RandomPassword - $cipherText = [String]::Concat( - (echo $plainText | - & "$AzureSDKRoot\bin\csencrypt.exe" Encrypt-Password -Thumbprint $Thumbprint | - select -skip 5)) - return @{ - "PlainText" = $plainText; - "CipherText" = $cipherText; - } -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Invoke-GalleryOperations.ps1 b/ops/Modules/NuGetOps/Public/Invoke-GalleryOperations.ps1 deleted file mode 100644 index d384455dbb..0000000000 --- a/ops/Modules/NuGetOps/Public/Invoke-GalleryOperations.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -function Invoke-GalleryOperations() { - param([switch]$WhatIf) - - if(!(Test-Path $GalOpsExe)) { - Write-Warning "Gallery Ops Runner has not been built, build it and try your command again." - return; - } - - $tmpfile = $null - if($CurrentDeployment) { - # Write a temp file with config data - $tmpfile = [IO.Path]::GetTempFileName() - $CurrentDeployment.Backend.Configuration | Out-File -Encoding UTF8 -FilePath $tmpfile - } - - # Fill Environment Variables - $oldConfig = $null - if(Test-Path "env:\NUGET_SERVICE_CONFIG") { - $oldConfig = $env:NUGET_SERVICE_CONFIG - } - $env:NUGET_SERVICE_CONFIG = $tmpfile - - Write-Host $env:NUGET_SERVICE_CONFIG - #& $GalOpsExe @args - - if($tmpfile -and (Test-Path $tmpfile)) { - del $tmpfile - if($oldConfig) { - $env:NUGET_SERVICE_CONFIG = $oldConfig - } else { - del env:\NUGET_SERVICE_CONFIG - } - } -} -Set-Alias -Name galops -Value Invoke-GalleryOperations -Export-ModuleMember -Alias galops \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/New-AzureManagementCertificate.ps1 b/ops/Modules/NuGetOps/Public/New-AzureManagementCertificate.ps1 deleted file mode 100644 index e92ee749d9..0000000000 --- a/ops/Modules/NuGetOps/Public/New-AzureManagementCertificate.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -<# -.SYNOPSIS -Creates an Azure Management Certificate -#> -function New-AzureManagementCertificate { - param([switch]$Force) - if(!$Force) { - throw "No longer needed! Use Add-AzureAccount to configure your Azure account. If you know you need to run this command, use -Force" - } - - if(!$CurrentEnvironment -or !$CurrentEnvironment.Subscription) { - throw "Requires an active environment" - } - - $subName = $CurrentEnvironment.Subscription.Name.Replace(" ", "") - $NamePrefix = "Azure-$subName-$([Environment]::UserName)-on-$([Environment]::MachineName)-" - if(@(dir cert:\CurrentUser\My | where { $_.Subject -like "CN=$NamePrefix*, O=Azure, OU=$($CurrentEnvironment.Subscription.Id)" }).Length -gt 0) { - throw "A cert is already registered in the store for this (subscription, user, machine) triple" - } - - $CommonName = "$($NamePrefix)at-$([DateTime]::UtcNow.ToString("yyyy-MM-dd"))-utc" - $Name = "CN=$CommonName,O=Azure,OU=$($CurrentEnvironment.Subscription.Id)" - - Write-Host "Generating Certificate..." - $FileName = Join-Path (Convert-Path .) "$CommonName.cer" - $PfxFileName = Join-Path (Convert-Path .) "$CommonName.pfx" - if(Test-Path $FileName) { - if($Force) { - del $FileName - } else { - throw "There is already a cert at $FileName. Delete it or move it before running this command, or specify the -Force argument to have this script replace it." - } - } - if(Test-Path $PfxFileName) { - if($Force) { - del $PfxFileName - } else { - throw "There is already a cert at $PfxFileName. Delete it or move it before running this command, or specify the -Force argument to have this script replace it." - } - } - makecert -sky exchange -r -n "$Name" -pe -a sha1 -len 2048 -ss My $FileName - - # Get the Thumbprint and find the private key in the store - $FileName = (Convert-Path $FileName) - Write-Host "Certificate created. Public Key is at $FileName" - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $FileName - $CertificateThumbprint = $cert.Thumbprint - - $cert = get-item "cert:\CurrentUser\My\$CertificateThumbprint" - $CertData = $cert.Export("Pkcs12", [String]::Empty); - [IO.File]::WriteAllBytes($PfxFileName, $CertData) -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/New-NuGetFrontendDeployment.ps1 b/ops/Modules/NuGetOps/Public/New-NuGetFrontendDeployment.ps1 deleted file mode 100644 index f3852958f6..0000000000 --- a/ops/Modules/NuGetOps/Public/New-NuGetFrontendDeployment.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -<# -.SYNOPSIS -Creates a new NuGet Frontend Deployment - -.PARAMETER Service -The service to deploy the frontend to -#> -function New-NuGetFrontendDeployment { - param( - [Parameter(Mandatory=$true, Position=0)]$Service, - [Parameter(Mandatory=$true)][string]$Package) - - $Service = EnsureService $Service - - if(!$Service.ID -or !$Service.Environment) { - throw "Invalid Service object provided" - } - - if(!$Service.Environment.Subscription) { - throw "No Subscription is available for this environment. Do you have access?" - } - - if(!(Test-Path $Package)) { - throw "Could not find package $Package" - } - $Package = Convert-Path $Package - - $ext = [IO.Path]::GetExtension($Package) - - if($Service.Type -eq "CloudService") { - if($ext -ne ".cspkg") { - throw "Expected a CSPKG package!" - } - throw "Not yet implemented!" - } elseif($Service.Type -eq "Website") { - if($ext -ne ".zip") { - throw "Expected a ZIP package!" - } - DeployWebsite $Service $Package - } -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/New-RemoteDesktopCertificate.ps1 b/ops/Modules/NuGetOps/Public/New-RemoteDesktopCertificate.ps1 deleted file mode 100644 index 9f88943000..0000000000 --- a/ops/Modules/NuGetOps/Public/New-RemoteDesktopCertificate.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -<# -.SYNOPSIS -Creates a new remote desktop certificate for the current environment - -.DESCRIPTION -#> -function New-RemoteDesktopCertificate { - if(!$CurrentEnvironment) { - throw "This command requires an environment" - } - - if(!$AzureSDKRoot) { - throw "This command requires the Azure .NET SDK" - } - - $CertificateName = "nuget-$($CurrentEnvironment.Name)" - - $existingCert = Get-RemoteDesktopCertificate - if($existingCert) { - throw "There is already a certificate with the friendly name $CertificateName. Please delete it first." - } - - $Thumbprint = & "$AzureSDKRoot\bin\csencrypt.exe" new-passwordencryptioncertificate -FriendlyName $CertificateName | - where { $_ -match "Thumbprint\s+:\s(.*)" } | - foreach { $matches[1] } - - Write-Host "Created Remote Desktop Certificate $CertificateName with thumbprint $Thumbprint" -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Reset-GalleryRepo.ps1 b/ops/Modules/NuGetOps/Public/Reset-GalleryRepo.ps1 deleted file mode 100644 index 56e3f661a9..0000000000 --- a/ops/Modules/NuGetOps/Public/Reset-GalleryRepo.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -<# -.SYNOPSIS -Resets the specified Gallery Repository - -.PARAMETER RepositoryPath -The path to the NuGetGallery repository - -.PARAMETER RepositoryURL -The URL to clone the gallery from (optional, defaults to the NuGet/NuGetGallery repo on GitHub) -#> -function Reset-GalleryRepo { - param( - [string]$RepositoryPath = $null, - [string]$RepositoryURL = "https://github.com/NuGet/NuGetGallery.git" - ) - if([String]::IsNullOrEmpty($RepositoryPath)) { - $RepositoryPath = Join-Path $OpsRoot "NuGetGallery" - } - if(Test-Path $RepositoryPath) { - Write-Host "Removing old Gallery clone..." - del $RepositoryPath -Force -Recurse - } - Write-Host "Cloning gallery..." - git clone $RepositoryURL $RepositoryPath -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Set-Environment.ps1 b/ops/Modules/NuGetOps/Public/Set-Environment.ps1 deleted file mode 100644 index c0c7204d49..0000000000 --- a/ops/Modules/NuGetOps/Public/Set-Environment.ps1 +++ /dev/null @@ -1,109 +0,0 @@ -<# -.SYNOPSIS -Sets the active NuGet Environment - -.DESCRIPTION -This command has two different behaviors. - -If given the "-Name" argument, it searches the Environments.xml file located -at $EnvironmentsList (or the NUGET_OPS_ENVIRONMENTS environment variable) for an environment with the specified -name, and loads that configuration data. Note that the contents of this file are cached, so if you change it, you will -need to use Exit-NuGetOps and Enter-NuGetOps to reload the operations console. - -If given the "-ServiceName", "-WorkerName" and "-Subscription" arguments, it creates an ad-hoc environment based on the services you entered. -The name given to that service is the value given as the ServiceName paramter. Setting the "-NonProduction" switch disables extra checks which are -normally put in place for production environments. This version of the command should only be used in the rare occasions you are unable to connect -to the file share or location where the Environments.xml file is stored. - -.PARAMETER Name -The name of an environment defined in Environments.xml - -.PARAMETER Frontend -The name of an Azure Web Site which is present in one of the subscriptions you have already registered on this machine and contains the NuGetGallery frontend. - -.PARAMETER Backend -The name of an Azure Cloud Service which is present in one of the subscriptions you have already registered on this machine and contains the NuGetGallery backend. - -.PARAMETER Subscription -The name of the Azure Subscription containing the services named in Frontend/Backend. - -.PARAMETER NonProduction -Add this flag to disable extra checks relating to production environments - -#> -function Set-Environment { - param( - [Parameter(Mandatory=$true, ParameterSetName="FromList")][string]$Name, - [Parameter(Mandatory=$true, ParameterSetName="AdHoc")][string]$Frontend, - [Parameter(Mandatory=$true, ParameterSetName="AdHoc")][string]$Backend, - [Parameter(Mandatory=$true, ParameterSetName="AdHoc")][string]$Subscription, - [Parameter(Mandatory=$false, ParameterSetName="AdHoc")][switch]$NonProduction - ) - - if($PsCmdlet.ParameterSetName -eq "FromList") { - # Find the key - $key = @($Environments.Keys | Where { $_ -like "$Name*" }) - if($key.Length -eq 0) { - throw "Unknown Environment $Name" - } elseif($key.Length -gt 1) { - throw "Ambiguous Environment Name: $Name. Did you mean one of these?: $key" - } - $Global:CurrentEnvironment = $Environments[$key] - } elseif($PsCmdlet.ParameterSetName -eq "AdHoc") { - # Build an environment object - $Global:CurrentEnvironment = New-Object PSCustomObject - Add-Member -NotePropertyMembers @{ - Version = 0.2; - Name = $ServiceName; - Protected = !$NonProduction; - Frontend = $Frontend; - Backend = $Backend; - Subscription = $Subscription - } -InputObject $Global:CurrentEnvironment - } else { - throw "Unknown Parameter Set: $($PsCmdlet.ParameterSetName)" - } - - Write-Host "Setting Current Environment to $($CurrentEnvironment.Name)" - - # Check for the subscription - $subName = $CurrentEnvironment.Subscription - if($subName -isnot [string]) { - $subName = $subName.Name; - } - - Select-AzureSubscription $subName; - - if(_IsProduction) { - $Global:OldBgColor = $Host.UI.RawUI.BackgroundColor - $Host.UI.RawUI.BackgroundColor = "DarkRed" - _RefreshGitColors - Write-Warning "You are attached to the PRODUCTION Environment. Use caution!" - } else { - if($Global:OldBgColor) { - $Host.UI.RawUI.BackgroundColor = $Global:OldBgColor - del variable:\OldBgColor - } - _RefreshGitColors - } - - del env:\NUCMD_* - - # Load environment variables - $env:NUCMD_ENVIRONMENT = $CurrentEnvironment.Name; - $env:NUCMD_SUBSCRIPTION_ID = $CurrentEnvironment.Subscription.Id - $env:NUCMD_SUBSCRIPTION_NAME = $CurrentEnvironment.Subscription.Name - - # Search for a certificate - $cert = Get-AzureManagementCertificate $CurrentEnvironment.Subscription.Name - if($cert) { - $env:NUCMD_SUBSCRIPTION_THUMBPRINT = $cert.Thumbprint - } - - # Build a service map - $str = ""; - $CurrentEnvironment.Services.Values | where { $_.Datacenter -eq 0 } | foreach { - $str += "$($_.Kind)|$($_.Uri);" - } - $env:NUCMD_SERVICE_MAP = $str -} diff --git a/ops/Modules/NuGetOps/Public/Set-NuGetServiceConfiguration.ps1 b/ops/Modules/NuGetOps/Public/Set-NuGetServiceConfiguration.ps1 deleted file mode 100644 index 5e5a6f83cc..0000000000 --- a/ops/Modules/NuGetOps/Public/Set-NuGetServiceConfiguration.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -<# -.SYNOPSIS -Sets the config settings for the specified service - -.PARAMETER Service -The service to get configuration for -#> -function Set-NuGetServiceConfiguration { - param( - [Parameter(Mandatory=$true, Position=0)]$Service, - [Parameter(Mandatory=$true, Position=1)][hashtable]$Settings) - - $dep = $null; - if($Service -is [string]) { - if(!$CurrentEnvironment) { - throw "This command requires an environment" - } - $Service = Get-NuGetService $Service -ForceSingle - } - - if(!$Service.ID -or !$Service.Environment) { - throw "Invalid Service object provided" - } - - if(!$Service.Environment.Subscription) { - throw "No Subscription is available for this environment. Do you have access?" - } - - if($Service.Type -eq "Website") { - Write-Host "Saving settings for $($Service.ID)..." - - RunInSubscription $Service.Environment.Subscription.Name { - # HACK: Gallery.SqlServer is a connection string! - $cstr = $Settings["Gallery.SqlServer"] - if($cstr) { - $Settings.Remove("Gallery.SqlServer") - $cs = New-Object Microsoft.WindowsAzure.Commands.Utilities.Websites.Services.WebEntities.ConnStringInfo - $cs.Name = "Gallery.SqlServer" - $cs.ConnectionString = $cstr - $cs.Type = "SQLAzure" - Set-AzureWebsite -Name $Service.ID -AppSettings $Settings -ConnectionStrings $cs - } else { - Set-AzureWebsite -Name $Service.ID -AppSettings $Settings - } - } - } else { - throw "Cannot write settings to a Cloud Service. Apply the settings to a CSCFG file and upload that instead." - } -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/Public/Test-Symbols.ps1 b/ops/Modules/NuGetOps/Public/Test-Symbols.ps1 deleted file mode 100644 index ddeff11b6e..0000000000 --- a/ops/Modules/NuGetOps/Public/Test-Symbols.ps1 +++ /dev/null @@ -1,81 +0,0 @@ -<# -.SYNOPSIS -Tests that the NuGet Symbols exist on the specified path - -.PARAMETER ReleaseShare -A share containing the NuGet Binaries - -.PARAMETER SymbolServer -A specific symbol server to test - -.PARAMETER PublicOnly -Test only the public Microsoft symbol server - -.PARAMETER InternalOnly -Test only the internal Microsoft symbol server -#> -function Test-Symbols { - param( - [Parameter(Mandatory=$true)][string]$ReleaseShare, - [Parameter(ParameterSetName="SpecificServer")][string]$SymbolServer, - [Parameter(ParameterSetName="MicrosoftServers")][switch]$PublicOnly, - [Parameter(ParameterSetName="MicrosoftServers")][switch]$InternalOnly - ) - - $path = $ReleaseShare - if(!(Test-Path $path)) { - $path = "\\nuget\Releases\$ReleaseShare" - } - if(!(Test-Path $path)) { - throw "Could not find release share $ReleaseShare. Checked $ReleaseShare, $path"; - } - - if($PsCmdlet.ParameterSetName -eq "SpecificServer") { - Write-Host -Foreground Black -Background Yellow "*********************************************" - Write-Host -Foreground Black -Background Yellow "Testing Custom Symbol Server: $SymbolServer" - Write-Host -Foreground Black -Background Yellow "*********************************************" - symchk /s $SymbolServer /r $path /op - if($lastexitcode -ne 0) { - Write-Host -Foreground White -Background Red "****************************" - Write-Host -Foreground White -Background Red "Some Symbols were not found." - Write-Host -Foreground White -Background Red "****************************" - } else { - Write-Host -Foreground Black -Background Green "***********************" - Write-Host -Foreground Black -Background Green "All symbols were found." - Write-Host -Foreground Black -Background Green "***********************" - } - } - elseif($PsCmdlet.ParameterSetName -eq "MicrosoftServers") { - if(!$PublicOnly) { - Write-Host -Foreground Black -Background Yellow "*********************************************" - Write-Host -Foreground Black -Background Yellow "Testing Internal Symbol Server: http://symweb" - Write-Host -Foreground Black -Background Yellow "*********************************************" - symchk /s http://symweb /r $path /op - if($lastexitcode -ne 0) { - Write-Host -Foreground White -Background Red "****************************" - Write-Host -Foreground White -Background Red "Some Symbols were not found." - Write-Host -Foreground White -Background Red "****************************" - } else { - Write-Host -Foreground Black -Background Green "***********************" - Write-Host -Foreground Black -Background Green "All symbols were found." - Write-Host -Foreground Black -Background Green "***********************" - } - } - - if(!$InternalOnly) { - Write-Host -Foreground Black -Background Yellow "************************************************************************" - Write-Host -Foreground Black -Background Yellow "Testing Public Symbol Server: http://msdl.microsoft.com/download/symbols" - Write-Host -Foreground Black -Background Yellow "************************************************************************" - symchk /s http://msdl.microsoft.com/download/symbols /r $path /op - if($lastexitcode -ne 0) { - Write-Host -Foreground White -Background Red "****************************" - Write-Host -Foreground White -Background Red "Some Symbols were not found." - Write-Host -Foreground White -Background Red "****************************" - } else { - Write-Host -Foreground Black -Background Green "***********************" - Write-Host -Foreground Black -Background Green "All symbols were found." - Write-Host -Foreground Black -Background Green "***********************" - } - } - } -} \ No newline at end of file diff --git a/ops/Modules/NuGetOps/_EnvironmentLoaders.ps1 b/ops/Modules/NuGetOps/_EnvironmentLoaders.ps1 deleted file mode 100644 index 476768f4f7..0000000000 --- a/ops/Modules/NuGetOps/_EnvironmentLoaders.ps1 +++ /dev/null @@ -1,125 +0,0 @@ -function Get-Subscriptions($NuGetOpsDefinition) { - $Subscriptions = @{}; - $SubscriptionsList = Join-Path $NuGetOpsDefinition "Subscriptions.xml" - if(Test-Path $SubscriptionsList) { - $x = [xml](cat $SubscriptionsList) - $x.subscriptions.subscription | ForEach-Object { - # Get the subscription object - $sub = $null; - if($accounts.Length -gt 0) { - $sub = Get-AzureSubscription $_.name - } - if($sub -eq $null) { - Write-Warning "Could not find subscription $_ in Subscriptions.xml. Do you have access to it?" - } - - $Subscriptions[$_.name] = New-Object PSCustomObject - Add-Member -NotePropertyMembers @{ - Version = $NuGetOpsVersion; - Id = $_.id; - Name = $_.name; - Subscription = $sub; - } -InputObject $Subscriptions[$_.name] - } - } else { - Write-Warning "Subscriptions list not found at $SubscriptionsList. No Subscriptions will be available." - } - - $Subscriptions -} - -function Get-V3Environments($NuGetOpsDefinition) { - $Environments = @{}; - $Subscriptions = $null; - - $EnvironmentsList = Join-Path $NuGetOpsDefinition "Environments.v3.xml" - if(!(Test-Path $EnvironmentsList)) { - return - } - - if($NuGetOpsDefinition -and (Test-Path $NuGetOpsDefinition)) { - $Subscriptions = Get-Subscriptions -NuGetOpsDefinition $NuGetOpsDefinition - - $x = [xml](cat $EnvironmentsList); - $x.environments.environment | ForEach-Object { - $env = New-Object PSCustomObject - $sub = $Subscriptions[$_.subscription] - - $services = @{}; - $_.service | ForEach-Object { - $svc = New-Object PSCustomObject - Add-Member -NotePropertyMembers @{ - Kind = $_.kind; - Type = $_.type; - Name = $_.name; - ID = $_.InnerText; - Environment = $env; - Uri = $_.uri; - Datacenter = $_.dc; - } -InputObject $svc - $services[$svc.Name] = $svc - }; - - Add-Member -NotePropertyMembers @{ - Version = 3; - Name = $_.name; - Subscription = $sub; - Protected = $_.protected -and ([String]::Equals($_.protected, "true", "OrdinalIgnoreCase")); - Services = $services; - } -InputObject $env - $Environments[$_.name] = $env - } - } - - $ret = New-Object PSCustomObject - Add-Member -InputObject $ret -NotePropertyMembers @{ - "Version"=3; - "Environments"=$Environments; - "Subscriptions"=$Subscriptions - } - $ret -} - -function Get-V2Environments($NuGetOpsDefinition) { - $Environments = @{}; - $Subscriptions = @{}; - - if($NuGetOpsDefinition -and (Test-Path $NuGetOpsDefinition)) { - $EnvironmentsList = Join-Path $NuGetOpsDefinition "Environments.xml" - if(Test-Path $EnvironmentsList) { - $x = [xml](cat $EnvironmentsList) - $Environments = @{}; - $x.environments.environment | ForEach-Object { - $Environments[$_.name] = New-Object PSCustomObject - Add-Member -NotePropertyMembers @{ - Version = $NuGetOpsVersion; - Name = $_.name; - Protected = $_.protected -and ([String]::Equals($_.protected, "true", "OrdinalIgnoreCase")); - Frontend = $_.frontend; - Backend = $_.backend; - Subscription = $_.subscription - Type = $_.type - } -InputObject $Environments[$_.name] - } - } else { - Write-Warning "Environments list not found at $EnvironmentsList. No Environments will be available." - } - - $Subscriptions = Get-Subscriptions -NuGetOpsDefinition $NuGetOpsDefinition - - $Environments.Keys | foreach { - $subName = $Environments[$_].Subscription - if($Subscriptions[$subName] -ne $null) { - $Environments[$_].Subscription = $Subscriptions[$subName]; - } - } - } - - $ret = New-Object PSCustomObject - Add-Member -InputObject $ret -NotePropertyMembers @{ - "Version"=2; - "Environments"=$Environments; - "Subscriptions"=$Subscriptions - } - $ret -} \ No newline at end of file diff --git a/ops/Modules/PS-CmdInterop/PS-CmdInterop.nuspec b/ops/Modules/PS-CmdInterop/PS-CmdInterop.nuspec deleted file mode 100644 index 200fbce4ef..0000000000 --- a/ops/Modules/PS-CmdInterop/PS-CmdInterop.nuspec +++ /dev/null @@ -1,11 +0,0 @@ - - - - PS-CmdInterop - 1.0 - Andrew Nurse - Andrew Nurse - false - Provides the Invoke-CmdScript cmdlet, which invokes a CMD script and copies it's environment variables in to the current session. - - \ No newline at end of file diff --git a/ops/Modules/PS-CmdInterop/PS-CmdInterop.psd1 b/ops/Modules/PS-CmdInterop/PS-CmdInterop.psd1 deleted file mode 100644 index edc036d239..0000000000 Binary files a/ops/Modules/PS-CmdInterop/PS-CmdInterop.psd1 and /dev/null differ diff --git a/ops/Modules/PS-CmdInterop/PS-CmdInterop.psm1 b/ops/Modules/PS-CmdInterop/PS-CmdInterop.psm1 deleted file mode 100644 index f242152552..0000000000 --- a/ops/Modules/PS-CmdInterop/PS-CmdInterop.psm1 +++ /dev/null @@ -1,40 +0,0 @@ -<# - .Synopsis - Invokes the specified CMD-shell script and imports it's environment - .Description - Invoking a CMD-shell script from PowerShell normally causes a new CMD.exe - process to be created and the script run within it. The problem with this - is that the environment variables set by the script are lost when this process - terminates and control is returned to PowerShell. This cmdlet copies the - environment variables over to the PowerShell environment. - .Parameter Script - The script to run - .Parameter Parameters - The parameters to pass to the script - .Example - Invoke-CmdScript ScriptWhichSetsVars.cmd /a /b /c - .Inputs - System.String - .Outputs - Nothing -#> -function Invoke-CmdScript { - # Adapted from http://www.leeholmes.com/blog/2006/05/11/nothing-solves-everything-%E2%80%93-powershell-and-other-technologies/ - param( - [Parameter(Mandatory=$true, ValueFromPipeline=$true)][string]$Script, - [Parameter(Mandatory=$false, ValueFromRemainingArguments=$true)][string]$Parameters - ) - - $tempFile = [System.IO.Path]::GetTempFileName() - - cmd /c " `"$Script`" $Parameters && set > `"$tempFile`" " - - Get-Content $tempFile | ForEach-Object { - if($_ -match "^(?.*?)=(?.*)$") { - Set-Content "env:\$($matches['var'])" $matches['val'] - } - } - - Remove-Item $tempFile -} -Export-ModuleMember -Function Invoke-CmdScript \ No newline at end of file diff --git a/ops/Modules/PS-VsVars/PS-VsVars.nuspec b/ops/Modules/PS-VsVars/PS-VsVars.nuspec deleted file mode 100644 index b3f13a2c65..0000000000 --- a/ops/Modules/PS-VsVars/PS-VsVars.nuspec +++ /dev/null @@ -1,14 +0,0 @@ - - - - PS-VsVars - 1.0 - Andrew Nurse - Andrew Nurse - false - Provides the Import-VsVars cmdlet, which imports Visual Studio environment into the current powershell session. - - - - - \ No newline at end of file diff --git a/ops/Modules/PS-VsVars/PS-VsVars.psd1 b/ops/Modules/PS-VsVars/PS-VsVars.psd1 deleted file mode 100644 index 9f3e873d79..0000000000 Binary files a/ops/Modules/PS-VsVars/PS-VsVars.psd1 and /dev/null differ diff --git a/ops/Modules/PS-VsVars/PS-VsVars.psm1 b/ops/Modules/PS-VsVars/PS-VsVars.psm1 deleted file mode 100644 index 1858319aff..0000000000 --- a/ops/Modules/PS-VsVars/PS-VsVars.psm1 +++ /dev/null @@ -1,120 +0,0 @@ -$VisualStudioVersions = @{} - -$SearchPath = "Software\Microsoft\VisualStudio" -if($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { - $SearchPath = "Software\Wow6432Node\Microsoft\VisualStudio" -} - -Get-ChildItem "HKLM:\$SearchPath" | - Where-Object { - ($_.Name -match "\d+\.\d+") -and - (![String]::IsNullOrEmpty((Get-ItemProperty "HKLM:\$SearchPath\$($_.PSChildName)").InstallDir)) - } | ForEach-Object { - $regPath = "HKLM:\$SearchPath\$($_.PSChildName)" - - # Gather VS data - $installDir = (Get-ItemProperty $regPath).InstallDir - - $vsVars = $null; - if(Test-Path "$installDir\..\..\VC\vcvarsall.bat") { - $vsVars = Convert-Path "$installDir\..\..\VC\vcvarsall.bat" - } - $devenv = $null; - if(Test-Path "$installDir\devenv.exe") { - $devenv = Convert-Path "$installDir\devenv.exe" - } - - # Make a VSInfo object - $vsInfo = New-Object PSCustomObject - Add-Member -InputObject $vsInfo -NotePropertyMembers @{ - "Version" = $_.PSChildName; - "RegistryRoot" = $_; - "InstallDir" = $installDir; - "VsVarsPath" = $vsVars; - "DevEnv" = $devenv; - } - - # Add it to the dictionary - $VisualStudioVersions[$_.PSChildName] = $vsInfo - } - -$latestVerWithVars = $VisualStudioVersions.Keys | sort -desc | where { $VisualStudioVersions[$_].VsVarsPath -ne $null } | select -first 1 -$LatestVisualStudioVersion = $VisualStudioVersions[$latestVerWithVars] -Export-ModuleMember -Variable $VisualStudioVersions,$LatestVisualStudioVersion - -function Import-VsVars { - param( - [Parameter(Mandatory=$false)][string]$VsVersion = $null, - [Parameter(Mandatory=$false)][string]$VsVarsPath = $null, - [Parameter(Mandatory=$false)][string]$Architecture = $env:PROCESSOR_ARCHITECTURE - ) - - if([String]::IsNullOrEmpty($VsVarsPath)) { - Write-Debug "Finding vcvarsall.bat automatically..." - - if([String]::IsNullOrEmpty($VsVersion)) { - Write-Debug "Finding most recent Visual Studio version..." - $VsVersion = $LatestVisualStudioVersion.Version - } - - if([String]::IsNullOrEmpty($VsVersion)) { - "No Visual Studio Environments found" - } else { - $Vs = $VisualStudioVersions[$VsVersion] - Write-Debug "Found VS $($Vs.Version) in $($Vs.InstallDir)" - $VsVarsPath = $Vs.VsVarsPath - } - } - if(![String]::IsNullOrEmpty($VsVarsPath) -and (Test-Path $VsVarsPath)) { - # Run the cmd script - Write-Debug "Invoking: `"$VsVarsPath`" $Architecture" - Invoke-CmdScript "$VsVarsPath" $Architecture - "Imported Visual Studio $VsVersion Environment into current shell" - } -} -Export-ModuleMember -Function Import-VsVars - -function Get-DevEnv { - param( - [Parameter(Mandatory=$false, Position=1)][string]$Version) - $Vs = $LatestVisualStudioVersion; - if($Version) { - $Vs = $VisualStudioVersions[$Version] - } - if(!$Vs) { - if($Version) { - throw "Could not find visual studio $Version!" - } else { - throw "Could not find any visual studio version!" - } - } - $Vs.DevEnv -} -Export-ModuleMember -Function Get-DevEnv - -function Invoke-VisualStudio { - param( - [Parameter(Mandatory=$false, Position=0)][string]$Solution, - [Parameter(Mandatory=$false, Position=1)][string]$Version) - - if([String]::IsNullOrEmpty($Solution)) { - $Solution = "*.sln" - } - elseif(!$Solution.EndsWith(".sln")) { - $Solution = $Solution + "*.sln"; - } - - if(!(Test-Path $Solution)) { - throw "Could not find any matches for: $Solution" - } - $slns = @(dir $Solution) - if($slns.Length -gt 1) { - $names = [String]::Join(",", @($slns | foreach { $_.Name })) - throw "Ambiguous matches for $($Solution): $names"; - } - - $devenv = Get-DevEnv -Version $Version - &$devenv $slns[0]; -} -Set-Alias -Name vs -Value Invoke-VisualStudio -Export-ModuleMember -Function Invoke-VisualStudio -Alias vs \ No newline at end of file diff --git a/ops/Modules/README.md b/ops/Modules/README.md deleted file mode 100644 index 9be1735ff0..0000000000 --- a/ops/Modules/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Operations PowerShell Modules - -PowerShell Modules used by the Operations Console \ No newline at end of file diff --git a/ops/NuGet-Ops-Icon.ico b/ops/NuGet-Ops-Icon.ico deleted file mode 100644 index 932b374015..0000000000 Binary files a/ops/NuGet-Ops-Icon.ico and /dev/null differ diff --git a/ops/README.md b/ops/README.md deleted file mode 100644 index da41135f31..0000000000 --- a/ops/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# NuGet Gallery Operations Toolkit - -This is a set of operations tools designed for the official NuGet.org site. Its **sole** purpose is to make it easier for the NuGet Gallery team to maintain the site. This is **not** a general purpose operations toolkit and is not designed to work with every NuGetGallery installation. It works for us and we thought it might help those of you with private galleries, so we have open-sourced it. We'll do our best to help you use and configure it, but making this user-friendly is not our top priority :). We are very unlikely to take Pull Requests that don't directly enhance our workflows, but please do feel free to make and maintain forks for your own purposes and contribute back general-purpose changes that you think we might find helpful as well. - -## What's inside -This repo contains NuGet Gallery Operations tools. These tools include: - -1. A PowerShell Console Environment for working with deployed versions of the NuGet Gallery diff --git a/ops/Scripts/Build-AzurePackage.ps1 b/ops/Scripts/Build-AzurePackage.ps1 deleted file mode 100644 index 8b42ece5ec..0000000000 --- a/ops/Scripts/Build-AzurePackage.ps1 +++ /dev/null @@ -1,72 +0,0 @@ -param( - [Parameter(Mandatory=$false)][string]$ReleaseSha, - [Parameter(Mandatory=$false)][string]$ReleaseBranch, - [Parameter(Mandatory=$false)][string]$AzureSdkPath, - [Parameter(Mandatory=$false)][string]$Configuration = "Release", - [Parameter(Mandatory=$false)][switch]$ForEmulator) - -$AzureToolsRoot = "$env:ProgramFiles\Microsoft SDKs\Windows Azure\" - -# Common functions. If we have more scripts, move it to an _Common.ps1 like NuGetGallery has -function Get-AzureSdkPath { - param($azureSdkPath) - if(!$azureSdkPath) { - (dir "$AzureToolsRoot\.NET SDK" | sort Name -desc | select -first 1).FullName - } else { - $azureSdkPath - } -} - -# The script itself - -$MyPath = split-path $MyInvocation.MyCommand.Path -$RepositoryRoot = resolve-path (join-path $MyPath "..") -$WorkerPath = Join-Path $RepositoryRoot "Source\NuGetGallery.Operations.Worker" -$OutputFolder = Join-Path $RepositoryRoot "_AzurePackage"; -$StagingFolder = Join-Path $RepositoryRoot "_PackageStage"; - -$BuildOutput = Join-Path $WorkerPath "bin\Release" -if(!(Test-Path "$BuildOutput\NuGetGallery.Operations.Worker.dll")) { - throw "Worker is not built in $Configuration mode. Please build the solution first" -} - -if(Test-Path $OutputFolder) { - del -Recurse -Force $OutputFolder -} -if(Test-Path $StagingFolder) { - del -Recurse -Force $StagingFolder -} - -mkdir $StagingFolder | out-null -cp $BuildOutput\* $StagingFolder - -# Build the name -if(!$ReleaseSha) { - $ReleaseSha = (& git rev-parse --short HEAD) -} elseif($ReleaseSha.Length -gt 10) { - $ReleaseSha = $ReleaseSha.Substring(0, 10) -} -if(!$ReleaseBranch) { - $ReleaseBranch = (& git name-rev --name-only HEAD) -} -$PackageFile = Join-Path $OutputFolder "NuGetOperations_$($ReleaseSha)_$ReleaseBranch.cspkg" - -# Package! -$copyOnlyFlag = ""; -if($ForEmulator) { - $copyOnlyFlag = "/copyOnly" -} - -mkdir $OutputFolder | out-null - -$AzureSdkPath = Get-AzureSdkPath $AzureSdkPath -if(!$AzureSdkPath -or !(Test-Path $AzureSdkPath)) { - throw "Azure SDK not found. Please specify the path to the Azure SDK in the AzureSdkPath parameter." -} -$RoleName = "NuGetGallery.Operations.Worker" -& "$AzureSdkPath\bin\cspack.exe" $copyOnlyFlag "$MyPath\Worker.csdef" /out:"$PackageFile" /role:"$RoleName;$StagingFolder" /rolePropertiesFile:"$RoleName;$MyPath\NuGetOperations.RoleProperties.txt" - -write-host "Azure package and configuration dropped to $OutputFolder." -write-host "" - -Exit 0 \ No newline at end of file diff --git a/ops/Scripts/Curate-Windows8Packages.ps1 b/ops/Scripts/Curate-Windows8Packages.ps1 deleted file mode 100644 index b106c20a3b..0000000000 --- a/ops/Scripts/Curate-Windows8Packages.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -param( - $connectionString = $env:NUGET_GALLERY_SQL_AZURE_CONNECTION_STRING -) - -$scriptDir = (Split-Path -parent $MyInvocation.MyCommand.Definition) -. $scriptDir\Require-Param.ps1 - -$continue = read-host "Curate Windows8 packages on $($connectionString)? (y/n)" -if ($continue -ne "y") { - write-host "Did not answer 'y'; exiting." - exit 1 -} - -require-param -value $connectionString -paramName "connectionString" - -$galopsExe = join-path $scriptDir "..\OpsExe\bin\Debug\galops.exe" - -& "$galopsExe" /task:curatewindows8packages /connectionstring:$connectionString \ No newline at end of file diff --git a/ops/Scripts/Enter-NuGetOps.ps1 b/ops/Scripts/Enter-NuGetOps.ps1 deleted file mode 100644 index 20f3f5822a..0000000000 --- a/ops/Scripts/Enter-NuGetOps.ps1 +++ /dev/null @@ -1,76 +0,0 @@ -<# -.SYNOPSIS -Enters the NuGet Operations Console -#> - -function Get-AvailableModule($name) { - (Get-Module -ListAvailable $name | - Sort -Desc Version | - Select -First 1) -} - -$MsftDomainNames = @("REDMOND","FAREAST","NORTHAMERICA","NTDEV") - -$OpsProfile = $MyInvocation.MyCommand.Path -$root = (Split-Path -Parent (Split-Path -Parent $OpsProfile)) -$OpsModules = Join-Path $root "Modules" -$env:PSModulePath = "$env:PSModulePath;$OpsModules" - -if($EnvironmentList) { - $env:NUGET_OPS_DEFINITION = $EnvironmentList; -} - -if(!$env:NUGET_OPS_DEFINITION) { - $msftNuGetShare = "\\nuget\nuget\Share\Environments" - # Defaults for Microsoft CorpNet. If you're outside CorpNet, you'll have to VPN in. Of course, if you're hosting your own gallery, you have to build your own scripts :P - if([Environment]::UserDomainName -and ($MsftDomainNames -contains [Environment]::UserDomainName) -and (Test-Path $msftNuGetShare)) { - $env:NUGET_OPS_DEFINITION = $msftNuGetShare - } - else { - Write-Warning "NUGET_OPS_DEFINITION is not set. Set it to a path containing an Environments.xml and a Subscriptions.xml file" - } -} - -$env:WinSDKRoot = "$(cat "env:\ProgramFiles(x86)")\Windows Kits\8.0" - -$env:PATH = "$root;$env:PATH;$env:WinSDKRoot\bin\x86;$env:WinSDKRoot\Debuggers\x86" - -function LoadOrReloadModule($name) { - if(Get-Module $name) { - Write-Host "Module $name already loaded, reloading." - Remove-Module $name -Force - } - Import-Module $name -} - -LoadOrReloadModule PS-CmdInterop -LoadOrReloadModule PS-VsVars - -Import-VsVars -Architecture x86 - -if(!(Get-Module posh-git)) { - if((Get-AvailableModule posh-git) -eq $null) { - Write-Warning "Posh-Git not found, it is recommended you install it!" - } else { - Import-Module posh-git - } -} else { - Write-Host "Module posh-git already loaded, can't reload" -} - -$azMod = Get-AvailableModule Azure -if(($azMod -eq $null) -or ($azMod.Version -lt (New-Object Version "0.7.0"))) { - throw "NuGet Operations requires Azure PowerShell 0.7 or higher!" -} - -LoadOrReloadModule Azure -LoadOrReloadModule NuGetOps - -$Global:_OldPrompt = $function:prompt; -function Global:prompt { - if(Get-Module NuGetOps) { - return Write-NuGetOpsPrompt - } else { - return $oldprompt.InvokeReturnAsIs() - } -} \ No newline at end of file diff --git a/ops/Scripts/Exit-NuGetOps.ps1 b/ops/Scripts/Exit-NuGetOps.ps1 deleted file mode 100644 index 2629f83d6c..0000000000 --- a/ops/Scripts/Exit-NuGetOps.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -<# -.SYNOPSIS -Exits the NuGet Operations Console and restores variables and prompts to their previous values -#> - -$save = $env:NUGET_OPS_DEFINITION -del env:\NUGET_* -if($save) { - $env:NUGET_OPS_DEFINTION = $save -} -Clear-Environment -Remove-Module NuGetOps -Write-Host "Note: Only the NuGetOps module has been removed. The Azure module, etc. are still imported" -Set-Content function:\prompt $_OldPrompt \ No newline at end of file diff --git a/ops/Scripts/Export-PackageStatistics.ps1 b/ops/Scripts/Export-PackageStatistics.ps1 deleted file mode 100644 index 8c6ca69ee8..0000000000 --- a/ops/Scripts/Export-PackageStatistics.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -param( - $connectionString = $env:NUGET_GALLERY_SQL_AZURE_CONNECTION_STRING, - $storageName = $env:NUGET_GALLERY_AZURE_STORAGE_ACCOUNT_NAME, - $storageKey = $env:NUGET_GALLERY_AZURE_STORAGE_ACCESS_KEY -) - -$scriptDir = (Split-Path -parent $MyInvocation.MyCommand.Definition) -. $scriptDir\Require-Param.ps1 - -require-param -value $connectionString -paramName "connectionString" -require-param -value $storageName -paramName "storageName" -require-param -value $storageKey -paramName "storageKey" - -$galopsExe = join-path $scriptDir "..\OpsExe\bin\Debug\galops.exe" - -& "$galopsExe" /task:pps /connectionstring:$connectionString /storagename:$storageName /storagekey:$storageKey \ No newline at end of file diff --git a/ops/Scripts/README.md b/ops/Scripts/README.md deleted file mode 100644 index 31a82b428b..0000000000 --- a/ops/Scripts/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Operations Scripts - -These scripts are tools used when performing maintainance on the NuGet Gallery \ No newline at end of file diff --git a/ops/Scripts/clean-old-apikeypasswordcolumns.sql b/ops/Scripts/clean-old-apikeypasswordcolumns.sql deleted file mode 100644 index d5b9a84baf..0000000000 --- a/ops/Scripts/clean-old-apikeypasswordcolumns.sql +++ /dev/null @@ -1,33 +0,0 @@ --- RUNNING THIS SCRIPT --- 1. Connect to the database --- 2. Execute the script --- 3. Save the output table for your records - -DECLARE @affected TABLE( - username nvarchar(255), - field nvarchar(255), - how nvarchar(255) -) - -INSERT INTO @affected(username, field, how) -SELECT u.Username as username, 'ApiKey' AS field, 'OLD API key lost' as how -FROM Users u -INNER JOIN Credentials c ON c.Type = 'apikey.v1' AND u.[Key] = c.UserKey -WHERE LOWER(u.ApiKey) != c.Value - -INSERT INTO @affected(username, field, how) -SELECT u.Username as username, 'Password' AS field, 'OLD SHA1 password lost' as how -FROM Users u -INNER JOIN Credentials c ON c.Type = 'password.sha1' AND u.[Key] = c.UserKey AND u.PasswordHashAlgorithm = 'SHA1' -WHERE u.HashedPassword != c.Value - -INSERT INTO @affected(username, field, how) -SELECT u.Username as username, 'Password' AS field, 'OLD PBKDF2 password lost' as how -FROM Users u -INNER JOIN Credentials c ON c.Type = 'password.pbkdf2' AND u.[Key] = c.UserKey AND u.PasswordHashAlgorithm = 'PBKDF2' -WHERE u.HashedPassword != c.Value - -SELECT * FROM @affected - -ALTER TABLE Users ALTER COLUMN ApiKey uniqueidentifier NULL -ALTER TABLE Users ALTER COLUMN PasswordHashAlgorithm nvarchar(max) NULL \ No newline at end of file diff --git a/ops/Scripts/remove-duplicate-curated-packages.sql b/ops/Scripts/remove-duplicate-curated-packages.sql deleted file mode 100644 index 5fb1edf31c..0000000000 --- a/ops/Scripts/remove-duplicate-curated-packages.sql +++ /dev/null @@ -1,14 +0,0 @@ -/* DELETE duplicate CuratedPackages from the database, keeping just one of each (unique id = feed + package keys). - FAVOR Keeping the entries that were manually added, or were added with notes - - USEFUL FOR: Doing the CuratedFeed unique index migration that will prevent further duplication (we were seeing 40% of packages were dupes) -*/ - -WITH NumberedRows -AS -( - SELECT Row_number() OVER - (PARTITION BY CuratedFeedKey, PackageRegistrationKey ORDER BY CuratedFeedKey, PackageRegistrationKey, AutomaticallyCurated, Notes DESC) - RowId, * from CuratedPackages -) -DELETE FROM NumberedRows WHERE RowId > 1 diff --git a/ops/Scripts/remove-duplicate-usernames.sql b/ops/Scripts/remove-duplicate-usernames.sql deleted file mode 100644 index d1f0c5e458..0000000000 --- a/ops/Scripts/remove-duplicate-usernames.sql +++ /dev/null @@ -1,16 +0,0 @@ -/* DELETES Users with duplicated Usernames from the database. DOESNT DELETE the first User with a particular Username. - - Useful for: we used to not have a unique username cosntraint, and the user registration process can therefore - create duplicate users by a race condition. This scripts must be run before the migration which adds the UNIQUE - constraint (index) on the User Username column. - */ - -WITH NumberedRows -AS -( - SELECT Row_number() OVER - (PARTITION BY Username ORDER BY Username, [Key] ASC) - RowId, * from Users -) - -DELETE FROM NumberedRows WHERE RowId > 1 \ No newline at end of file diff --git a/ops/Subscriptions.sample.xml b/ops/Subscriptions.sample.xml deleted file mode 100644 index 8bb7ddfd08..0000000000 --- a/ops/Subscriptions.sample.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/Backend.ruleset b/src/Backend.ruleset deleted file mode 100644 index c0f1b27ff3..0000000000 --- a/src/Backend.ruleset +++ /dev/null @@ -1,208 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/FrontendSDL.ruleset b/src/FrontendSDL.ruleset new file mode 100644 index 0000000000..4da63b3d9e --- /dev/null +++ b/src/FrontendSDL.ruleset @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/NuGet.Services.Search.Client/Client/RetryingHttpClientWrapper.cs b/src/NuGet.Services.Search.Client/Client/RetryingHttpClientWrapper.cs index e768d49c13..86c4488431 100644 --- a/src/NuGet.Services.Search.Client/Client/RetryingHttpClientWrapper.cs +++ b/src/NuGet.Services.Search.Client/Client/RetryingHttpClientWrapper.cs @@ -67,7 +67,7 @@ private async Task GetWithRetry(IEnumerable endpoints, // Create requests queue var tasks = CreateRequestQueue(healthyEndpoints, httpClient, cancellationTokenSource); - // When the first succesful task comes in, return it. If no succesfull tasks are returned, throw an AggregateException. + // When the first successful task comes in, return it. If no successful tasks are returned, throw an AggregateException. var exceptions = new List(); var taskList = tasks.ToList(); diff --git a/src/NuGet.Services.Search.Client/Correlation/WebApiCorrelationHandler.cs b/src/NuGet.Services.Search.Client/Correlation/WebApiCorrelationHandler.cs index 129aba1acc..eaf9888826 100644 --- a/src/NuGet.Services.Search.Client/Correlation/WebApiCorrelationHandler.cs +++ b/src/NuGet.Services.Search.Client/Correlation/WebApiCorrelationHandler.cs @@ -67,7 +67,7 @@ private void SetResponseCorrelationId(HttpRequestMessage request, HttpResponseMe if (response != null) { // Do not allow overriding the header - if any code wants to set the correlation id - // it shoud set the request property instead. + // it should set the request property instead. if (response.Headers.Contains(CorrelationIdHttpHeaderName)) { response.Headers.Remove(CorrelationIdHttpHeaderName); diff --git a/src/NuGet.Services.Search.Client/NuGet.Services.Search.Client.csproj b/src/NuGet.Services.Search.Client/NuGet.Services.Search.Client.csproj index ea788eadac..ab7c69ccc6 100644 --- a/src/NuGet.Services.Search.Client/NuGet.Services.Search.Client.csproj +++ b/src/NuGet.Services.Search.Client/NuGet.Services.Search.Client.csproj @@ -22,6 +22,7 @@ DEBUG;TRACE prompt 4 + Sdl7.0.ruleset pdbonly @@ -32,9 +33,8 @@ 4 - - ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - True + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll False diff --git a/src/NuGet.Services.Search.Client/app.config b/src/NuGet.Services.Search.Client/app.config index 8ae69102b0..c61b949f0c 100644 --- a/src/NuGet.Services.Search.Client/app.config +++ b/src/NuGet.Services.Search.Client/app.config @@ -7,8 +7,8 @@ - - + + diff --git a/src/NuGet.Services.Search.Client/packages.config b/src/NuGet.Services.Search.Client/packages.config index e314dd6012..9c566a4ceb 100644 --- a/src/NuGet.Services.Search.Client/packages.config +++ b/src/NuGet.Services.Search.Client/packages.config @@ -5,6 +5,6 @@ - + \ No newline at end of file diff --git a/src/NuGetGallery.Cloud/DeployToAzure.ps1 b/src/NuGetGallery.Cloud/DeployToAzure.ps1 deleted file mode 100644 index 2c0a5f8fe1..0000000000 --- a/src/NuGetGallery.Cloud/DeployToAzure.ps1 +++ /dev/null @@ -1,177 +0,0 @@ -## Octopus Azure deployment script, version 1.0 -## -------------------------------------------------------------------------------------- -## -## This script is used to control how we deploy packages to Windows Azure. -## -## When the script is run, the correct Azure subscription will ALREADY be selected, -## and we'll have loaded the neccessary management certificates. The Azure PowerShell module -## will also be loaded. -## -## If you want to customize the Azure deployment process, simply copy this script into -## your NuGet package as DeployToAzure.ps1. Octopus will invoke it instead of the default -## script. -## -## The script will be passed the following parameters in addition to the normal Octopus -## variables passed to any PowerShell script. -## -## $OctopusAzureSubscriptionId // The subscription ID GUID -## $OctopusAzureSubscriptionName // The random name of the temporary Azure subscription record -## $OctopusAzureServiceName // The name of your cloud service -## $OctopusAzureStorageAccountName // The name of your storage account -## $OctopusAzureSlot // The name of the slot to deploy to (Staging or Production) -## $OctopusAzurePackageUri // URI to the .cspkg file in Azure Blob Storage to deploy -## $OctopusAzureConfigurationFile // The name of the Azure cloud service configuration file to use -## $OctopusAzureDeploymentLabel // The label to use for deployment -## $OctopusAzureSwapIfPossible // "True" if we should attempt to "swap" deployments rather than a new deployment - -function CreateOrUpdate() -{ - $releaseNumber = $OctopusParameters["Octopus.Release.Number"] - $OctopusAzureDeploymentLabel = $releaseNumber + " (" + ([DateTime]::Now.ToString("dd MMM yyyy @ HHmm")) + ")" - Write-Host "Deploying `"$OctopusAzureDeploymentLabel`"" - - # Parse out the environment name - if($OctopusAzureServiceName -notmatch "nuget-(?[A-Za-z]+)-\d+-[A-Z0-9a-z]+") - { - throw "Azure Service Name is invalid: $OctopusAzureServiceName" - } - $environment = $matches["env"] - - # Locate the config file - $config = Join-Path $env:NuDeployCode "Deployment\Config\$environment\$OctopusAzureServiceName.cscfg" - if(!(Test-Path $config)) - { - throw "Missing Deployment Config File! Expected it at: $config. Check the NuDeployCodeRoot environment variable on your Tentacle!" - } - - # Copy it over the current one - Write-Host "Copying $config to $OctopusAzureConfigurationFile" - Copy-Item $config $OctopusAzureConfigurationFile -Force - - # Get the Current Deployment - $deployment = Get-AzureDeployment -ServiceName $OctopusAzureServiceName -Slot $OctopusAzureSlot -ErrorVariable a -ErrorAction silentlycontinue - - if (($a[0] -ne $null) -or ($deployment.Name -eq $null)) - { - CreateNewDeployment - return - } - - if (($OctopusAzureSwapIfPossible -eq $true) -and ($OctopusAzureSlot -eq "Production")) - { - Write-Host "Checking whether a swap is possible" - $staging = Get-AzureDeployment -ServiceName $OctopusAzureServiceName -Slot "Staging" -ErrorVariable a -ErrorAction silentlycontinue - if (($a[0] -ne $null) -or ($staging.Name -eq $null)) - { - Write-Host "Nothing is deployed in staging" - } - else - { - Write-Host ("Current staging deployment: " + $staging.Label) - - # Parse the release number out - $splat = $staging.Label.Split(); - if(($splat.Length -gt 0) -and ($staging.Label.Split()[0] -eq $releaseNumber)) - { - # We can swap! The existing deployment label matches this release! - SwapDeployment - return - } - } - } - - # Get the current number of instances and poke it in to the config if we're updating an existing deployment - # (Octopus can do this automatically but we are already messing with CSCFG :)) - Write-Host "Reading existing Instance Count for $($deployment.ServiceName)" - $xml = [xml](cat $OctopusAzureConfigurationFile); - $deployment.RolesConfiguration.Keys | ForEach { - # Find the role node affected - $roleName = $_ - $roleXml = $xml.ServiceConfiguration.Role | where {$_.name -eq $roleName} | select -first 1 - - # Put the current value in the xml - $instanceCount = $deployment.RolesConfiguration[$roleName].InstanceCount; - Write-Host " Setting $roleName instance count to $instanceCount" - $roleXml.Instances.count = $instanceCount.ToString() - } - Write-Host "Saving config file..." - $xml.Save($OctopusAzureConfigurationFile) - UpdateDeployment -} - -function SwapDeployment() -{ - Write-Host "Swapping the staging environment to production" - Move-AzureDeployment -ServiceName $OctopusAzureServiceName -} - -function UpdateDeployment($deployment) -{ - Write-Host "A deployment already exists in $OctopusAzureServiceName for slot $OctopusAzureSlot. Upgrading deployment..." - Set-AzureDeployment -Upgrade -ServiceName $OctopusAzureServiceName -Package $OctopusAzurePackageUri -Configuration $OctopusAzureConfigurationFile -Slot $OctopusAzureSlot -Mode Simultaneous -label $OctopusAzureDeploymentLabel -Force -} - -function CreateNewDeployment() -{ - Write-Host "Creating a new deployment..." - New-AzureDeployment -Slot $OctopusAzureSlot -Package $OctopusAzurePackageUri -Configuration $OctopusAzureConfigurationFile -label $OctopusAzureDeploymentLabel -ServiceName $OctopusAzureServiceName -} - -function WaitForComplete() -{ - $dep = Get-AzureDeployment -ServiceName $OctopusAzureServiceName -Slot $OctopusAzureSlot - - $ready = $false - while(!$ready) { - Write-Host "Checking if deployment is ready yet" - $ready = $true - $dep.RoleInstanceList | ForEach-Object { - Write-Host " $($_.InstanceName) = $($_.InstanceStatus)" - if($_.InstanceStatus -ne "ReadyRole") { - $ready = $false - } - } - if(!$ready) { - Write-Host "Sleeping for 10 seconds..." - Start-Sleep -Seconds 10 - $dep = Get-AzureDeployment -ServiceName $OctopusAzureServiceName -Slot $OctopusAzureSlot - } - } - - $completeDeploymentID = $dep.DeploymentId - Write-Host "Deployment complete; Deployment ID: $completeDeploymentID" -} - -function ConfigureDiagnostics([string]$roleName) -{ - # Locate the diagnostics config file - $config = Join-Path (Split-Path $OctopusAzureConfigurationFile) "Extensions\PaasDiagnostics.$roleName.PubConfig.xml" - if(!(Test-Path $config)) - { - throw "Missing Diagnostics Config File! Expected it at: $config. Is it missing from the OctopusDeploy NuGet package?" - } - - $xml = [xml](cat $OctopusAzureConfigurationFile) - $roleXml = $xml.ServiceConfiguration.Role | where {$_.name -eq $roleName} | select -First 1 - $diagnosticsConfigurationSetting = $roleXml.ConfigurationSettings.Setting | where {$_.name -eq "Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString"} | select -First 1 - - $storageContext = New-AzureStorageContext -ConnectionString $diagnosticsConfigurationSetting.value - - Write-Host "Configuring diagnostics for '$OctopusAzureServiceName' (role: $roleName, slot: $OctopusAzureSlot)..." - - $extension = Get-AzureServiceExtension -ServiceName $OctopusAzureServiceName -Slot $OctopusAzureSlot -ExtensionName 'PaaSDiagnostics' -ProviderNamespace 'Microsoft.Azure.Diagnostics' -ErrorAction SilentlyContinue -ErrorVariable errorVariable - if (!($?)) { - Write-Host "Error occurred getting extension. Details: $errorVariable" - } elseif ($extension -ne $null) { - Write-Host "Diagnostics already configured. Skipping." - } else { - Remove-AzureServiceDiagnosticsExtension -ServiceName $OctopusAzureServiceName -Slot $OctopusAzureSlot -ErrorAction SilentlyContinue -ErrorVariable errorVariable - Set-AzureServiceDiagnosticsExtension -ServiceName $OctopusAzureServiceName -Slot $OctopusAzureSlot -DiagnosticsConfigurationPath $config -StorageContext $storageContext -Role $roleName -Verbose - } - - Write-Host "Configured diagnostics for role $roleName." -} - -CreateOrUpdate -WaitForComplete -ConfigureDiagnostics -RoleName "NuGetGallery" diff --git a/src/NuGetGallery.Cloud/NuGetGallery.Cloud.ccproj b/src/NuGetGallery.Cloud/NuGetGallery.Cloud.ccproj deleted file mode 100644 index 6e02662b12..0000000000 --- a/src/NuGetGallery.Cloud/NuGetGallery.Cloud.ccproj +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - Debug - AnyCPU - 2.9 - 0041aca0-30ec-4554-8c7c-0af810f3086f - Library - Properties - NuGetGallery.Cloud - NuGetGallery.Cloud - True - NuGetGallery.Cloud - False - Local - False - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - NuGetGallery - {1dacf781-5cd0-4123-8bac-cd385d864be5} - True - Web - NuGetGallery - True - - - - - - - - - Content - - - Content - - - Content - - - Content - - - Content - - - Content - - - - - - - - 10.0 - $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Windows Azure Tools\2.9\ - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Cloud/NuGetGallery.Cloud.nuspec b/src/NuGetGallery.Cloud/NuGetGallery.Cloud.nuspec deleted file mode 100644 index f85556602c..0000000000 --- a/src/NuGetGallery.Cloud/NuGetGallery.Cloud.nuspec +++ /dev/null @@ -1,22 +0,0 @@ - - - - NuGetGallery.Cloud - 0.0.0 - .NET Foundation - .NET Foundation - https://github.com/NuGet/NuGetGallery/blob/master/LICENSE - https://github.com/NuGet/NuGetGallery - false - Deployment package for the NuGet V2 Gallery Service. - - Built by $BuildUser$ on $BuildMachine$ at $BuildDateUtc$ from $Commit$ on branch '$Branch$'. - - - - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/InstallDotNet452.cmd b/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/InstallDotNet452.cmd deleted file mode 100644 index 921722dca9..0000000000 --- a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/InstallDotNet452.cmd +++ /dev/null @@ -1,13 +0,0 @@ -REM Install .NET Framework -set timehour=%time:~0,2% -set timestamp=%date:~-4,4%%date:~-10,2%%date:~-7,2%-%timehour: =0%%time:~3,2% -set startuptasklog=startuptasklog-%timestamp%.txt -set netfxinstallerlog = NetFXInstallerLog-%timestamp% -echo Logfile generated at: %startuptasklog% >> %startuptasklog% -echo Checking if .NET 4.5.2 is installed >> %startuptasklog% -reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" /v Release | Find "0x5cbf5" -if %ERRORLEVEL%== 0 goto end -echo Installing .NET 4.5.2. Log: %netfxinstallerlog% >> %startuptasklog% -start /wait %~dp0NDP452-KB2901954-Web.exe /q /serialdownload /log %netfxinstallerlog% -:end -echo Completed: %date:~-4,4%%date:~-10,2%%date:~-7,2%-%timehour: =0%%time:~3,2% >> %startuptasklog% \ No newline at end of file diff --git a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/NDP452-KB2901954-Web.exe b/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/NDP452-KB2901954-Web.exe deleted file mode 100644 index 60e3fb97f5..0000000000 Binary files a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/NDP452-KB2901954-Web.exe and /dev/null differ diff --git a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/SslConfig.cmd b/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/SslConfig.cmd deleted file mode 100644 index 41eb5a0363..0000000000 --- a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/SslConfig.cmd +++ /dev/null @@ -1,2 +0,0 @@ -powershell -NoProfile -ExecutionPolicy Unrestricted -File "%~dp0SslConfig.ps1" >> startup.log 2>> startup.err -exit /b 0 \ No newline at end of file diff --git a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/SslConfig.ps1 b/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/SslConfig.ps1 deleted file mode 100644 index 9bb76a5b3c..0000000000 --- a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/SslConfig.ps1 +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) Andre N. Klingsheim. See https://nwebsec.codeplex.com/license for license information. - -param([bool]$allowReboot = $false) - -Function UpdateRegistryPath($path){ - - if(test-path $path){ - return $false - } - write-Host "Creating registry path: $path" - md $path - return $true -} - -Function UpdateRegistryKey($path, $propertyName, $value, $propertyType){ - $property = Get-ItemProperty -Path $path -Name $propertyName -ErrorAction SilentlyContinue - - if($property){ - if($property.$propertyName -eq $value){ - return $false - } - Write-Host "Updating registry key $path $propertyName $value" - Set-ItemProperty -path $path -name $propertyName -value $value - return $true - } - Write-Host "Creating registry key $path $propertyName $value" - New-ItemProperty -path $path -name $propertyName -value $value -PropertyType $propertyType - return $true -} - -$date = Get-Date -write-output "---- NWebsec.AzureStartupTasks - TLS hardening - $date ----" -write-output "Checking for registry keys, updating as necessary" -write-output "" - - -$preferredCipherSuites = "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384_P256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384_P384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256_P256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA_P256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA_P384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA_P256,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384_P384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256_P256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384_P384,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256_P256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA_P256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA_P384,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA_P256,TLS_DHE_DSS_WITH_AES_256_CBC_SHA256,TLS_DHE_DSS_WITH_AES_128_CBC_SHA256,TLS_DHE_DSS_WITH_AES_256_CBC_SHA,TLS_DHE_DSS_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_3DES_EDE_CBC_SHA,TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA" -$rebootRequired = $false - -# Disable SSL 2.0 -write-output "**** Making sure SSL 2.0 is disabled ****" -$rebootRequired = (UpdateRegistryPath "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Server") -Or $rebootRequired -$rebootRequired = (UpdateRegistryKey "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Server" "Enabled" 0 "DWord") -Or $rebootRequired - -# Disable SSL 3.0 -write-output "**** Making sure SSL 3.0 is disabled ****" -$rebootRequired = (UpdateRegistryPath "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0") -Or $rebootRequired -$rebootRequired = (UpdateRegistryPath "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server") -Or $rebootRequired -$rebootRequired = (UpdateRegistryKey "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server" "Enabled" 0 "DWord") -Or $rebootRequired - -# Enable TLS 1.1 -write-output "**** Making sure TLS 1.1 is enabled ****" -$rebootRequired = (UpdateRegistryPath "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1") -Or $rebootRequired -$rebootRequired = (UpdateRegistryPath "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server") -Or $rebootRequired -$rebootRequired = (UpdateRegistryKey "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server" "DisabledByDefault" 0 "DWord") -Or $rebootRequired -$rebootRequired = (UpdateRegistryPath "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client") -Or $rebootRequired -$rebootRequired = (UpdateRegistryKey "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client" "DisabledByDefault" 0 "DWord") -Or $rebootRequired - - -# Enable TSL 1.2 -write-output "**** Making sure TLS 1.2 is enabled ****" -$rebootRequired = (UpdateRegistryPath "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2") -Or $rebootRequired -$rebootRequired = (UpdateRegistryPath "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server") -Or $rebootRequired -$rebootRequired = (UpdateRegistryKey "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server" "DisabledByDefault" 0 "DWord") -Or $rebootRequired -$rebootRequired = (UpdateRegistryPath "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client") -Or $rebootRequired -$rebootRequired = (UpdateRegistryKey "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client" "DisabledByDefault" 0 "DWord") -Or $rebootRequired - -# Protocol versions done, set preferred cipher suites -write-output "**** Making sure preferred cipher suites are set ****" -$rebootRequired = (UpdateRegistryKey "HKLM:\SOFTWARE\Policies\Microsoft\Cryptography\Configuration\SSL\00010002" "Functions" $preferredCipherSuites "String") -Or $rebootRequired - -if($rebootRequired){ - if($allowReboot){ - write-output "Registry was updated, rebooting..." - write-output "---- NWebsec.AzureStartupTasks - TLS hardening Completed - $date ----" - shutdown /r /t 0 - }else{ - write-output "Registry was updated, reboot is required for changes to take effect." - } -}else{ -write-output "Registry keys were ok, exiting." -} -write-output "---- NWebsec.AzureStartupTasks - TLS hardening Completed - $date ----" \ No newline at end of file diff --git a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/Startup.cmd b/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/Startup.cmd deleted file mode 100644 index f09f44f076..0000000000 --- a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/Startup.cmd +++ /dev/null @@ -1,2 +0,0 @@ -powershell -NoProfile -ExecutionPolicy Unrestricted -File "%~dp0Startup.ps1" >> startup.log 2>> startup.err -exit /b 0 \ No newline at end of file diff --git a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/Startup.ps1 b/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/Startup.ps1 deleted file mode 100644 index 294252a731..0000000000 --- a/src/NuGetGallery.Cloud/NuGetGalleryContent/bin/Startup.ps1 +++ /dev/null @@ -1,89 +0,0 @@ -# Find IIS -$iisRoot = Join-Path $env:windir "system32\inetsrv" -if(Test-Path "HKLM:\Software\Microsoft\IISExpress") { - $iisRoot = (Get-ItemProperty ((dir HKLM:\Software\Microsoft\IISExpress | sort -desc | select -first 1).PSPath)).InstallPath; -} - -$appcmd = Join-Path $iisRoot "appcmd.exe" -if(!(Test-Path $appcmd)) { - throw "Could not find AppCmd!" -} - -# Enable Dynamic Compression of OData feed -&$appcmd set config /section:urlCompression /doDynamicCompression:True /commit:apphost -&$appcmd set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/json; charset=utf-8',enabled='True']" /commit:apphost -&$appcmd set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/xml; charset=utf-8',enabled='True']" /commit:apphost -&$appcmd set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/xml',enabled='True']" /commit:apphost -&$appcmd set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/atom%u002bxml; charset=utf-8',enabled='True']" /commit:apphost -&$appcmd set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/atom%u002bxml',enabled='True']" /commit:apphost -&$appcmd set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/atom%u002bxml; type=feed; charset=utf-8',enabled='True']" /commit:apphost -&$appcmd set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/atom%u002bxml; type=feed',enabled='True']" /commit:apphost - - -# Customize Logging -&$appcmd set config -section:system.applicationHost/sites /siteDefaults.logFile.enabled:"True" /commit:apphost -&$appcmd set config -section:system.applicationHost/sites /siteDefaults.logFile.logFormat:"W3C" /commit:apphost -&$appcmd set config -section:system.applicationHost/sites /siteDefaults.logFile.period:"Hourly" /commit:apphost -&$appcmd set config -section:system.applicationHost/sites /siteDefaults.logFile.logExtFileFlags:"Date,Time,TimeTaken,BytesRecv,BytesSent,ComputerName,HttpStatus,HttpSubStatus,Win32Status,ProtocolVersion,ServerIP,ServerPort,Method,Host,UriStem,UriQuery,UserAgent" - - -# Increase the number of available IIS threads for high performance applications -# Uses the recommended values from http://msdn.microsoft.com/en-us/library/ms998549.aspx#scalenetchapt06_topic8 -# Assumes running on two cores (medium instance on Azure) -&$appcmd set config /commit:MACHINE -section:processModel -maxWorkerThreads:100 -&$appcmd set config /commit:MACHINE -section:processModel -minWorkerThreads:50 -&$appcmd set config /commit:MACHINE -section:processModel -minIoThreads:50 -&$appcmd set config /commit:MACHINE -section:processModel -maxIoThreads:100 - -# Adjust the maximum number of connections per core for all IP addresses -&$appcmd set config /commit:MACHINE -section:connectionManagement /+["address='*',maxconnection='240'"] - - -# Configure IP Restrictions - -# Install the feature -Import-Module ServerManager -Add-WindowsFeature Web-IP-Security - -# Clear them -do { - $str = &$appcmd set config -section:system.webServer/security/ipSecurity /-"[@start]" /commit:apphost - $str -} while(!$str.Contains("ERROR")) - -# Read the new list -[Reflection.Assembly]::LoadWithPartialName("Microsoft.WindowsAzure.ServiceRuntime"); -$setting = [Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetConfigurationSettingValue("Startup.BlockedIPs"); -$ips = $setting.Split(","); - -# Save the new lists -$ips | where { ![String]::IsNullOrEmpty($_) } | foreach { - $parts = $_.Split(":") - $ip = $parts[0] - if($parts.Length -gt 1) { - $subnet = $parts[1] - &$appcmd set config -section:system.webServer/security/ipSecurity /+"[ipAddress='$ip',subnetMask='$subnet',allowed='False']" /commit:apphost - } - else { - &$appcmd set config -section:system.webServer/security/ipSecurity /+"[ipAddress='$ip',allowed='False']" /commit:apphost - } -} - -# Configure secondary SSL bindings -$setting = [Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetConfigurationSettingValue("Startup.AdditionalSSL"); -$additionalSSL = $setting.Split(","); # e.g. foo.bar:443:thumbprint,bar.baz:443:FEDCBA - -# Register additional SSL bindings -$sites = [xml](&$appcmd list sites /xml) -$defaultSite = $sites.appcmd.SITE[0].Attributes[0].Value.ToString() -$additionalSSL | where { ![String]::IsNullOrEmpty($_) } | foreach { - $parts = $_.Split(":")` - - $hostname = $parts[0] - $port = $parts[1] - $thumbprint = $parts[2] - - echo Adding binding to site $defaultSite for URL https://$hostname`:$port with SNI certificate $thumbprint - &$appcmd set site /site.name:"$defaultSite" /+"bindings.[protocol='https',bindingInformation='*:$port`:$hostname',sslFlags='1']" /commit:apphost - netsh http add sslcert hostnameport=$hostname`:$port certhash=$thumbprint appid='{4dc3e181-e14b-4a21-b022-59fc669b0914}' certstorename=MY -} diff --git a/src/NuGetGallery.Cloud/NuGetGalleryContent/diagnostics.wadcfgx b/src/NuGetGallery.Cloud/NuGetGalleryContent/diagnostics.wadcfgx deleted file mode 100644 index 52cd829162..0000000000 --- a/src/NuGetGallery.Cloud/NuGetGalleryContent/diagnostics.wadcfgx +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - \ No newline at end of file diff --git a/src/NuGetGallery.Cloud/ServiceConfiguration.Cloud.cscfg b/src/NuGetGallery.Cloud/ServiceConfiguration.Cloud.cscfg deleted file mode 100644 index 56f68d033c..0000000000 --- a/src/NuGetGallery.Cloud/ServiceConfiguration.Cloud.cscfg +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Cloud/ServiceConfiguration.Local.cscfg b/src/NuGetGallery.Cloud/ServiceConfiguration.Local.cscfg deleted file mode 100644 index 8518619cd0..0000000000 --- a/src/NuGetGallery.Cloud/ServiceConfiguration.Local.cscfg +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Cloud/ServiceDefinition.csdef b/src/NuGetGallery.Cloud/ServiceDefinition.csdef deleted file mode 100644 index 31f1e31456..0000000000 --- a/src/NuGetGallery.Cloud/ServiceDefinition.csdef +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/NuGetGallery.Core/Auditing/AggregateAuditingService.cs b/src/NuGetGallery.Core/Auditing/AggregateAuditingService.cs new file mode 100644 index 0000000000..acb687f37e --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/AggregateAuditingService.cs @@ -0,0 +1,52 @@ +// 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.Threading.Tasks; + +namespace NuGetGallery.Auditing +{ + /// + /// An auditing service that aggregates multiple auditing services. + /// + public sealed class AggregateAuditingService : IAuditingService + { + private readonly IAuditingService[] _services; + + /// + /// Instantiates a new instance. + /// + /// An enumerable of instances. + /// Thrown if is null. + public AggregateAuditingService(IEnumerable services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + _services = services.ToArray(); + } + + /// + /// Persists the audit record to storage. + /// + /// An audit record. + /// A that represents the asynchronous save operation. + /// Thrown if is null. + public async Task SaveAuditRecordAsync(AuditRecord record) + { + if (record == null) + { + throw new ArgumentNullException(nameof(record)); + } + + var tasks = _services.Select(service => service.SaveAuditRecordAsync(record)) + .ToArray(); + + await Task.WhenAll(tasks); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/AuditActor.cs b/src/NuGetGallery.Core/Auditing/AuditActor.cs index 56bd44ed36..af7ab1d6f3 100644 --- a/src/NuGetGallery.Core/Auditing/AuditActor.cs +++ b/src/NuGetGallery.Core/Auditing/AuditActor.cs @@ -1,13 +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; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; -using System.Text; using System.Threading.Tasks; +using System.Web; namespace NuGetGallery.Auditing { @@ -23,24 +24,73 @@ public class AuditActor public AuditActor(string machineName, string machineIP, string userName, string authenticationType, DateTime timeStampUtc) : this(machineName, machineIP, userName, authenticationType, timeStampUtc, null) { } + public AuditActor(string machineName, string machineIP, string userName, string authenticationType, DateTime timeStampUtc, AuditActor onBehalfOf) { MachineName = machineName; + MachineIP = machineIP; UserName = userName; AuthenticationType = authenticationType; TimestampUtc = timeStampUtc; OnBehalfOf = onBehalfOf; } - public static Task GetCurrentMachineActor() + public static Task GetAspNetOnBehalfOfAsync() + { + // Use HttpContext to build an actor representing the user performing the action + var context = HttpContext.Current; + if (context == null) + { + return Task.FromResult(null); + } + + return GetAspNetOnBehalfOfAsync(new HttpContextWrapper(context)); + } + + public static Task GetAspNetOnBehalfOfAsync(HttpContextBase context) + { + // Try to identify the client IP using various server variables + var clientIpAddress = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"]; + if (string.IsNullOrEmpty(clientIpAddress)) // Try REMOTE_ADDR server variable + { + clientIpAddress = context.Request.ServerVariables["REMOTE_ADDR"]; + } + + if (string.IsNullOrEmpty(clientIpAddress)) // Try UserHostAddress property + { + clientIpAddress = context.Request.UserHostAddress; + } + + if (!string.IsNullOrEmpty(clientIpAddress) && clientIpAddress.IndexOf(".", StringComparison.Ordinal) > 0) + { + clientIpAddress = clientIpAddress.Substring(0, clientIpAddress.LastIndexOf(".", StringComparison.Ordinal)) + ".0"; + } + + string user = null; + string authType = null; + if (context.User != null) + { + user = context.User.Identity.Name; + authType = context.User.Identity.AuthenticationType; + } + + return Task.FromResult(new AuditActor( + null, + clientIpAddress, + user, + authType, + DateTime.UtcNow)); + } + + public static Task GetCurrentMachineActorAsync() { - return GetCurrentMachineActor(null); + return GetCurrentMachineActorAsync(null); } - public static async Task GetCurrentMachineActor(AuditActor onBehalfOf) + public static async Task GetCurrentMachineActorAsync(AuditActor onBehalfOf) { // Try to get local IP - string ipAddress = await GetLocalIP(); + string ipAddress = await GetLocalIpAddressAsync(); return new AuditActor( Environment.MachineName, @@ -51,7 +101,7 @@ public static async Task GetCurrentMachineActor(AuditActor onBehalfO onBehalfOf); } - public static async Task GetLocalIP() + public static async Task GetLocalIpAddressAsync() { string ipAddress = null; if (NetworkInterface.GetIsNetworkAvailable()) @@ -72,4 +122,4 @@ private static string TryGetAddress(IEnumerable addrs, AddressFamily return addrs.Where(a => a.AddressFamily == family).Select(a => a.ToString()).FirstOrDefault(); } } -} +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/AuditedAuthenticatedOperationAction.cs b/src/NuGetGallery.Core/Auditing/AuditedAuthenticatedOperationAction.cs new file mode 100644 index 0000000000..4e5c7ec896 --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/AuditedAuthenticatedOperationAction.cs @@ -0,0 +1,23 @@ +// 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.Auditing +{ + public enum AuditedAuthenticatedOperationAction + { + /// + /// Package push was attempted by a non-owner of the package + /// + PackagePushAttemptByNonOwner, + + /// + /// Login failed, no such user + /// + FailedLoginNoSuchUser, + + /// + /// Login failed, user exists but password is invalid + /// + FailedLoginInvalidPassword, + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/AuditedEntities/AuditedPackage.cs b/src/NuGetGallery.Core/Auditing/AuditedEntities/AuditedPackage.cs new file mode 100644 index 0000000000..2a82e8cc64 --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/AuditedEntities/AuditedPackage.cs @@ -0,0 +1,91 @@ +// 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.AuditedEntities +{ + public class AuditedPackage + { + public int PackageRegistrationKey { get; private set; } + public string Copyright { get; private set; } + public DateTime Created { get; private set; } + public string Description { get; private set; } + public string ReleaseNotes { get; private set; } + public int DownloadCount { get; private set; } + public string ExternalPackageUrl { get; private set; } + public string HashAlgorithm { get; private set; } + public string Hash { get; private set; } + public string IconUrl { get; private set; } + public bool IsLatest { get; private set; } + public bool IsLatestStable { get; private set; } + public DateTime LastUpdated { get; private set; } + public DateTime? LastEdited { get; private set; } + public string LicenseUrl { get; private set; } + public bool HideLicenseReport { get; private set; } + public string Language { get; private set; } + public DateTime Published { get; private set; } + public long PackageFileSize { get; private set; } + public string ProjectUrl { get; private set; } + public bool RequiresLicenseAcceptance { get; private set; } + public string Summary { get; private set; } + public string Tags { get; private set; } + public string Title { get; private set; } + public string Version { get; private set; } + public string NormalizedVersion { get; private set; } + public string LicenseNames { get; private set; } + public string LicenseReportUrl { get; private set; } + public bool Listed { get; private set; } + public bool IsPrerelease { get; private set; } + public string FlattenedAuthors { get; private set; } + public string FlattenedDependencies { get; private set; } + public int Key { get; private set; } + public string MinClientVersion { get; private set; } + public int? UserKey { get; private set; } + public bool Deleted { get; private set; } + + public static AuditedPackage CreateFrom(Package package) + { + return new AuditedPackage + { + PackageRegistrationKey = package.PackageRegistrationKey, + Copyright = package.Copyright, + Created = package.Created, + Description = package.Description, + ReleaseNotes = package.ReleaseNotes, + DownloadCount = package.DownloadCount, +#pragma warning disable 612 +#pragma warning restore 612 + HashAlgorithm = package.HashAlgorithm, + Hash = package.Hash, + IconUrl = package.IconUrl, + IsLatest = package.IsLatest, + IsLatestStable = package.IsLatestStable, + LastUpdated = package.LastUpdated, + LastEdited = package.LastEdited, + LicenseUrl = package.LicenseUrl, + HideLicenseReport = package.HideLicenseReport, + Language = package.Language, + Published = package.Published, + PackageFileSize = package.PackageFileSize, + ProjectUrl = package.ProjectUrl, + RequiresLicenseAcceptance = package.RequiresLicenseAcceptance, + Summary = package.Summary, + Tags = package.Tags, + Title = package.Title, + Version = package.Version, + NormalizedVersion = package.NormalizedVersion, + LicenseNames = package.LicenseNames, + LicenseReportUrl = package.LicenseReportUrl, + Listed = package.Listed, + IsPrerelease = package.IsPrerelease, + FlattenedAuthors = package.FlattenedAuthors, + FlattenedDependencies = package.FlattenedDependencies, + Key = package.Key, + MinClientVersion = package.MinClientVersion, + UserKey = package.UserKey, + Deleted = package.Deleted + }; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/AuditedEntities/AuditedPackageIdentifier.cs b/src/NuGetGallery.Core/Auditing/AuditedEntities/AuditedPackageIdentifier.cs new file mode 100644 index 0000000000..4fb3a862b2 --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/AuditedEntities/AuditedPackageIdentifier.cs @@ -0,0 +1,17 @@ +// 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.Auditing.AuditedEntities +{ + public class AuditedPackageIdentifier + { + public string Id { get; } + public string Version { get; } + + public AuditedPackageIdentifier(string id, string version) + { + Id = id; + Version = version; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/AuditedEntities/AuditedPackageRegistration.cs b/src/NuGetGallery.Core/Auditing/AuditedEntities/AuditedPackageRegistration.cs new file mode 100644 index 0000000000..0a6063ffa6 --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/AuditedEntities/AuditedPackageRegistration.cs @@ -0,0 +1,22 @@ +// 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.Auditing.AuditedEntities +{ + public class AuditedPackageRegistration + { + public string Id { get; private set; } + public int DownloadCount { get; private set; } + public int Key { get; private set; } + + public static AuditedPackageRegistration CreateFrom(PackageRegistration packageRegistration) + { + return new AuditedPackageRegistration + { + Id = packageRegistration.Id, + DownloadCount = packageRegistration.DownloadCount, + Key = packageRegistration.Key + }; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/AuditedPackageAction.cs b/src/NuGetGallery.Core/Auditing/AuditedPackageAction.cs new file mode 100644 index 0000000000..605afb68f8 --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/AuditedPackageAction.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. + +namespace NuGetGallery.Auditing +{ + public enum AuditedPackageAction + { + Delete, + SoftDelete, + Create, + List, + Unlist, + Edit, + UndoEdit, + + + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/AuditedPackageRegistrationAction.cs b/src/NuGetGallery.Core/Auditing/AuditedPackageRegistrationAction.cs new file mode 100644 index 0000000000..5cc78fbdfa --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/AuditedPackageRegistrationAction.cs @@ -0,0 +1,11 @@ +// 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.Auditing +{ + public enum AuditedPackageRegistrationAction + { + AddOwner, + RemoveOwner + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/AuditedUserAction.cs b/src/NuGetGallery.Core/Auditing/AuditedUserAction.cs new file mode 100644 index 0000000000..a03eafd809 --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/AuditedUserAction.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. + +namespace NuGetGallery.Auditing +{ + public enum AuditedUserAction + { + Register, + AddCredential, + RemoveCredential, + ExpireCredential, + EditCredential, + RequestPasswordReset, + ChangeEmail, + CancelChangeEmail, + ConfirmEmail, + Login + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/AuditingService.cs b/src/NuGetGallery.Core/Auditing/AuditingService.cs index 2561b6d198..907ab3e56a 100644 --- a/src/NuGetGallery.Core/Auditing/AuditingService.cs +++ b/src/NuGetGallery.Core/Auditing/AuditingService.cs @@ -1,21 +1,24 @@ // 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.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace NuGetGallery.Auditing { - public abstract class AuditingService + /// + /// Base class for auditing services. + /// + public abstract class AuditingService : IAuditingService { + /// + /// An auditing service instance with no backing store. + /// public static readonly AuditingService None = new NullAuditingService(); - private static readonly JsonSerializerSettings _auditRecordSerializerSettings; + private static readonly JsonSerializerSettings AuditRecordSerializerSettings; static AuditingService() { @@ -31,24 +34,42 @@ static AuditingService() TypeNameHandling = TypeNameHandling.None }; settings.Converters.Add(new StringEnumConverter()); - _auditRecordSerializerSettings = settings; + AuditRecordSerializerSettings = settings; } - public virtual async Task SaveAuditRecord(AuditRecord record) + /// + /// Persists the audit record to storage. + /// + /// An audit record. + /// A that represents the asynchronous save operation. + /// Thrown if is null. + public async Task SaveAuditRecordAsync(AuditRecord record) { - // Build an audit entry - var entry = new AuditEntry(record, await GetActor()); + if (record == null) + { + throw new ArgumentNullException(nameof(record)); + } - // Serialize to json - string rendered = RenderAuditEntry(entry); + var entry = new AuditEntry(record, await GetActorAsync()); + var rendered = RenderAuditEntry(entry); - // Save the record - return await SaveAuditRecord(rendered, record.GetResourceType(), record.GetPath(), record.GetAction(), entry.Actor.TimestampUtc); + await SaveAuditRecordAsync(rendered, record.GetResourceType(), record.GetPath(), record.GetAction(), entry.Actor.TimestampUtc); } + /// + /// Renders an audit entry as JSON. + /// + /// An audit entry. + /// A JSON string. + /// Thrown if is null. public virtual string RenderAuditEntry(AuditEntry entry) { - return JsonConvert.SerializeObject(entry, _auditRecordSerializerSettings); + if (entry == null) + { + throw new ArgumentNullException(nameof(entry)); + } + + return JsonConvert.SerializeObject(entry, AuditRecordSerializerSettings); } /// @@ -59,22 +80,20 @@ public virtual string RenderAuditEntry(AuditEntry entry) /// The file-system path to use to identify the audit record /// The action recorded in this audit record /// A timestamp indicating when the record was created - /// The URI identifying the audit record resource - protected abstract Task SaveAuditRecord(string auditData, string resourceType, string filePath, string action, DateTime timestamp); + /// A that represents the asynchronous save operation. + protected abstract Task SaveAuditRecordAsync(string auditData, string resourceType, string filePath, string action, DateTime timestamp); - protected virtual Task GetActor() + protected virtual Task GetActorAsync() { - return AuditActor.GetCurrentMachineActor(); + return AuditActor.GetCurrentMachineActorAsync(); } private class NullAuditingService : AuditingService { - protected override Task SaveAuditRecord(string auditData, string resourceType, string filePath, string action, DateTime timestamp) + protected override Task SaveAuditRecordAsync(string auditData, string resourceType, string filePath, string action, DateTime timestamp) { - var uriString = $"http://auditing.local/{resourceType}/{filePath}/{timestamp:s}-{action.ToLowerInvariant()}"; - var uri = new Uri(uriString); - return Task.FromResult(uri); + return Task.FromResult(0); } } } -} +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/CloudAuditingService.cs b/src/NuGetGallery.Core/Auditing/CloudAuditingService.cs index f4389a9b63..020b00a792 100644 --- a/src/NuGetGallery.Core/Auditing/CloudAuditingService.cs +++ b/src/NuGetGallery.Core/Auditing/CloudAuditingService.cs @@ -1,13 +1,10 @@ // 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.Globalization; using System.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; -using System.Web; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; using Microsoft.WindowsAzure.Storage.Blob.Protocol; @@ -24,75 +21,38 @@ public class CloudAuditingService : AuditingService private CloudBlobContainer _auditContainer; private string _instanceId; private string _localIP; - private Func> _onBehalfOfThunk; + private Func> _getOnBehalfOf; - public CloudAuditingService(string instanceId, string localIP, string storageConnectionString, Func> onBehalfOfThunk) - : this(instanceId, localIP, GetContainer(storageConnectionString), onBehalfOfThunk) + public CloudAuditingService(string instanceId, string localIP, string storageConnectionString, Func> getOnBehalfOf) + : this(instanceId, localIP, GetContainer(storageConnectionString), getOnBehalfOf) { } - public CloudAuditingService(string instanceId, string localIP, CloudBlobContainer auditContainer, Func> onBehalfOfThunk) + public CloudAuditingService(string instanceId, string localIP, CloudBlobContainer auditContainer, Func> getOnBehalfOf) { _instanceId = instanceId; _localIP = localIP; _auditContainer = auditContainer; - _onBehalfOfThunk = onBehalfOfThunk; - } - - public static Task AspNetActorThunk() - { - // Use HttpContext to build an actor representing the user performing the action - var context = HttpContext.Current; - if (context == null) - { - return null; - } - - // Try to identify the client IP using various server variables - string clientIP = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"]; - if (String.IsNullOrEmpty(clientIP)) // Try REMOTE_ADDR server variable - { - clientIP = context.Request.ServerVariables["REMOTE_ADDR"]; - } - if (String.IsNullOrEmpty(clientIP)) // Try UserHostAddress property - { - clientIP = context.Request.UserHostAddress; - } - - string user = null; - string authType = null; - if (context.User != null) - { - user = context.User.Identity.Name; - authType = context.User.Identity.AuthenticationType; - } - - return Task.FromResult(new AuditActor( - null, - clientIP, - user, - authType, - DateTime.UtcNow)); + _getOnBehalfOf = getOnBehalfOf; } - protected override async Task GetActor() + protected override async Task GetActorAsync() { // Construct an actor representing the user the service is acting on behalf of AuditActor onBehalfOf = null; - if(_onBehalfOfThunk != null) { - onBehalfOf = await _onBehalfOfThunk(); + if(_getOnBehalfOf != null) { + onBehalfOf = await _getOnBehalfOf(); } - return await AuditActor.GetCurrentMachineActor(onBehalfOf); + return await AuditActor.GetCurrentMachineActorAsync(onBehalfOf); } - protected override async Task SaveAuditRecord(string auditData, string resourceType, string filePath, string action, DateTime timestamp) + protected override async Task SaveAuditRecordAsync(string auditData, string resourceType, string filePath, string action, DateTime timestamp) { - string fullPath = String.Concat( - resourceType.ToLowerInvariant(), "/", - filePath.Replace(Path.DirectorySeparatorChar, '/'), "/", - timestamp.ToString("s"), "-", // Sortable DateTime format - action.ToLowerInvariant(), ".audit.v1.json"); + string fullPath = + $"{resourceType.ToLowerInvariant()}/" + + $"{filePath.Replace(Path.DirectorySeparatorChar, '/')}/" + + $"{Guid.NewGuid().ToString("N")}-{action.ToLowerInvariant()}.audit.v1.json"; var blob = _auditContainer.GetBlockBlobReference(fullPath); bool retry = false; @@ -122,8 +82,6 @@ await Task.Factory.FromAsync( null); await WriteBlob(auditData, fullPath, blob); } - - return blob.Uri; } private static CloudBlobContainer GetContainer(string storageConnectionString) @@ -157,7 +115,7 @@ private static async Task WriteBlob(string auditData, string fullPath, CloudBloc // Blob already existed! throw new InvalidOperationException(String.Format( CultureInfo.CurrentCulture, - Strings.CloudAuditingService_DuplicateAuditRecord, + CoreStrings.CloudAuditingService_DuplicateAuditRecord, fullPath)); } throw; diff --git a/src/NuGetGallery.Core/Auditing/CredentialAuditRecord.cs b/src/NuGetGallery.Core/Auditing/CredentialAuditRecord.cs index 07b9a6db43..638ba3db07 100644 --- a/src/NuGetGallery.Core/Auditing/CredentialAuditRecord.cs +++ b/src/NuGetGallery.Core/Auditing/CredentialAuditRecord.cs @@ -1,25 +1,50 @@ // 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.Auditing { public class CredentialAuditRecord { + public int Key { get; } + public string Type { get; } + public string Value { get; } + public string Description { get; } + public List Scopes { get; set; } + public string Identity { get; } + public DateTime Created { get; } + public DateTime? Expires { get; } + public DateTime? LastUsed { get; } + public CredentialAuditRecord(Credential credential, bool removed) { + if (credential == null) + { + throw new ArgumentNullException(nameof(credential)); + } + + Key = credential.Key; Type = credential.Type; + Description = credential.Description; Identity = credential.Identity; - // Track the value for credentials that are definitely revokable (API Key, etc.) and have been removed + // Track the value for credentials that are definitely revocable (API Key, etc.) and have been removed if (removed && !CredentialTypes.IsPassword(credential.Type)) { Value = credential.Value; } - } - - public string Type { get; set; } - public string Value { get; set; } - public string Identity { get; set; } + Created = credential.Created; + Expires = credential.Expires; + LastUsed = credential.LastUsed; + // Track scopes + Scopes = new List(); + foreach (var scope in credential.Scopes) + { + Scopes.Add(new ScopeAuditRecord(scope.Subject, scope.AllowedAction)); + } + } } } \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/FailedAuthenticatedOperationAuditRecord.cs b/src/NuGetGallery.Core/Auditing/FailedAuthenticatedOperationAuditRecord.cs new file mode 100644 index 0000000000..ec0d768d64 --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/FailedAuthenticatedOperationAuditRecord.cs @@ -0,0 +1,42 @@ +// 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 NuGetGallery.Auditing.AuditedEntities; + +namespace NuGetGallery.Auditing +{ + public class FailedAuthenticatedOperationAuditRecord + : AuditRecord + { + private const string Path = "all"; + + public string UsernameOrEmail { get; } + public AuditedPackageIdentifier AttemptedPackage { get; } + public CredentialAuditRecord AttemptedCredential { get; } + + public FailedAuthenticatedOperationAuditRecord( + string usernameOrEmail, + AuditedAuthenticatedOperationAction action, + AuditedPackageIdentifier attemptedPackage = null, + Credential attemptedCredential = null) + : base(action) + { + UsernameOrEmail = usernameOrEmail; + + if (attemptedPackage != null) + { + AttemptedPackage = attemptedPackage; + } + + if (attemptedCredential != null) + { + AttemptedCredential = new CredentialAuditRecord(attemptedCredential, removed: false); + } + } + + public override string GetPath() + { + return Path; // store in /failedauthenticatedoperation/all + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/FileSystemAuditingService.cs b/src/NuGetGallery.Core/Auditing/FileSystemAuditingService.cs new file mode 100644 index 0000000000..ff599c050b --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/FileSystemAuditingService.cs @@ -0,0 +1,72 @@ +// 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.IO; +using System.Threading.Tasks; + +namespace NuGetGallery.Auditing +{ + /// + /// Writes audit records to a specific directory in the file system + /// + public class FileSystemAuditingService : AuditingService + { + public static readonly string DefaultContainerName = "auditing"; + + private readonly string _auditingPath; + private readonly Func> _getOnBehalfOf; + + public FileSystemAuditingService(string auditingPath, Func> getOnBehalfOf) + { + if (string.IsNullOrEmpty(auditingPath)) + { + throw new ArgumentNullException(nameof(auditingPath)); + } + + if (getOnBehalfOf == null) + { + throw new ArgumentNullException(nameof(getOnBehalfOf)); + } + + _auditingPath = auditingPath; + _getOnBehalfOf = getOnBehalfOf; + } + + protected override async Task GetActorAsync() + { + // Construct an actor representing the user the service is acting on behalf of + AuditActor onBehalfOf = null; + if (_getOnBehalfOf != null) + { + onBehalfOf = await _getOnBehalfOf(); + } + + return await AuditActor.GetCurrentMachineActorAsync(onBehalfOf); + } + + protected override Task SaveAuditRecordAsync(string auditData, string resourceType, string filePath, string action, DateTime timestamp) + { + // Build relative file path + var relativeFilePath = + $"{resourceType.ToLowerInvariant()}{Path.DirectorySeparatorChar}" + + $"{filePath}{Path.DirectorySeparatorChar}" + + $"{Guid.NewGuid().ToString("N")}-{action.ToLowerInvariant()}.audit.v1.json"; + + // Build full file path + var fullFilePath = Path.Combine(_auditingPath, relativeFilePath); + + // Ensure the directory exists + var directoryName = Path.GetDirectoryName(fullFilePath); + if (!Directory.Exists(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + + // Write the data + File.WriteAllText(fullFilePath, auditData); + + return Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/IAuditingService.cs b/src/NuGetGallery.Core/Auditing/IAuditingService.cs new file mode 100644 index 0000000000..a2536c7fa6 --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/IAuditingService.cs @@ -0,0 +1,22 @@ +// 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.Threading.Tasks; + +namespace NuGetGallery.Auditing +{ + /// + /// Base interface for an auditing service. + /// + public interface IAuditingService + { + /// + /// Persists the audit record to storage. + /// + /// An audit record. + /// A that represents the asynchronous save operation. + /// Thrown if is null. + Task SaveAuditRecordAsync(AuditRecord record); + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/PackageAuditRecord.cs b/src/NuGetGallery.Core/Auditing/PackageAuditRecord.cs index 722d837c83..2df9d40619 100644 --- a/src/NuGetGallery.Core/Auditing/PackageAuditRecord.cs +++ b/src/NuGetGallery.Core/Auditing/PackageAuditRecord.cs @@ -1,23 +1,25 @@ // 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.Data; +using NuGetGallery.Auditing.AuditedEntities; namespace NuGetGallery.Auditing { - public class PackageAuditRecord : AuditRecord + public class PackageAuditRecord : AuditRecord { - public string Id { get; set; } - public string Version { get; set; } - public string Hash { get; set; } + public string Id { get; } + public string Version { get; } + public string Hash { get; } - public DataTable PackageRecord { get; set; } - public DataTable RegistrationRecord { get; set; } + public AuditedPackage PackageRecord { get; } + public AuditedPackageRegistration RegistrationRecord { get; } - public string Reason { get; set; } + public string Reason { get; } - public PackageAuditRecord(string id, string version, string hash, DataTable packageRecord, DataTable registrationRecord, PackageAuditAction action, string reason) + public PackageAuditRecord( + string id, string version, string hash, + AuditedPackage packageRecord, AuditedPackageRegistration registrationRecord, + AuditedPackageAction action, string reason) : base(action) { Id = id; @@ -28,21 +30,27 @@ public PackageAuditRecord(string id, string version, string hash, DataTable pack Reason = reason; } - public PackageAuditRecord(Package package, DataTable packageRecord, DataTable registrationRecord, PackageAuditAction action, string reason) - : this(package.PackageRegistration.Id, package.Version, package.Hash, packageRecord, registrationRecord, action, reason) + public PackageAuditRecord( + Package package, AuditedPackageAction action, string reason) + : this(package.PackageRegistration.Id, package.Version, package.Hash, + packageRecord: null, registrationRecord: null, action: action, reason: reason) { + PackageRecord = AuditedPackage.CreateFrom(package); + RegistrationRecord = AuditedPackageRegistration.CreateFrom(package.PackageRegistration); } + public PackageAuditRecord(Package package, AuditedPackageAction action) + : this(package.PackageRegistration.Id, package.Version, package.Hash, + packageRecord: null, registrationRecord: null, action: action, reason: null) + { + PackageRecord = AuditedPackage.CreateFrom(package); + RegistrationRecord = AuditedPackageRegistration.CreateFrom(package.PackageRegistration); + } + public override string GetPath() { return $"{Id}/{NuGetVersionNormalizer.Normalize(Version)}" .ToLowerInvariant(); } } - - public enum PackageAuditAction - { - Deleted, - SoftDeleted - } -} +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/PackageCreatedVia.cs b/src/NuGetGallery.Core/Auditing/PackageCreatedVia.cs new file mode 100644 index 0000000000..2d7b7a5205 --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/PackageCreatedVia.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. + +namespace NuGetGallery.Auditing +{ + public static class PackageCreatedVia + { + /// + /// Package has been created via NuGet API (nuget.exe push) + /// + public const string Api = "Created via API."; + + /// + /// Package has been created via NuGet web interface (browser) + /// + public const string Web = "Created via web."; + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/PackageRegistrationAuditRecord.cs b/src/NuGetGallery.Core/Auditing/PackageRegistrationAuditRecord.cs new file mode 100644 index 0000000000..7109234abe --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/PackageRegistrationAuditRecord.cs @@ -0,0 +1,34 @@ +// 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 NuGetGallery.Auditing.AuditedEntities; + +namespace NuGetGallery.Auditing +{ + public class PackageRegistrationAuditRecord : AuditRecord + { + public string Id { get; } + public AuditedPackageRegistration RegistrationRecord { get; } + public string Owner { get; } + + public PackageRegistrationAuditRecord( + string id, AuditedPackageRegistration registrationRecord, AuditedPackageRegistrationAction action, string owner) + : base(action) + { + Id = id; + RegistrationRecord = registrationRecord; + Owner = owner; + } + + public PackageRegistrationAuditRecord( + PackageRegistration packageRegistration, AuditedPackageRegistrationAction action, string owner) + : this(packageRegistration.Id, AuditedPackageRegistration.CreateFrom(packageRegistration), action, owner) + { + } + + public override string GetPath() + { + return $"{Id}".ToLowerInvariant(); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/ScopeAuditRecord.cs b/src/NuGetGallery.Core/Auditing/ScopeAuditRecord.cs new file mode 100644 index 0000000000..bb2dd3e32c --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/ScopeAuditRecord.cs @@ -0,0 +1,17 @@ +// 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.Auditing +{ + public class ScopeAuditRecord + { + public string Subject { get; set; } + public string AllowedAction { get; set; } + + public ScopeAuditRecord(string subject, string allowedAction) + { + Subject = subject; + AllowedAction = allowedAction; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/UserAuditAction.cs b/src/NuGetGallery.Core/Auditing/UserAuditAction.cs new file mode 100644 index 0000000000..26a1685110 --- /dev/null +++ b/src/NuGetGallery.Core/Auditing/UserAuditAction.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. + +namespace NuGetGallery.Auditing +{ + public enum UserAuditAction + { + Registered, + AddedCredential, + RemovedCredential, + RequestedPasswordReset, + ChangeEmail, + CancelChangeEmail, + ConfirmEmail, + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Auditing/UserAuditRecord.cs b/src/NuGetGallery.Core/Auditing/UserAuditRecord.cs index 1883668cf4..8b65b8b72a 100644 --- a/src/NuGetGallery.Core/Auditing/UserAuditRecord.cs +++ b/src/NuGetGallery.Core/Auditing/UserAuditRecord.cs @@ -1,48 +1,58 @@ // 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.Text; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace NuGetGallery.Auditing { - public class UserAuditRecord : AuditRecord + public class UserAuditRecord : AuditRecord { - public string Username { get; set; } - public string EmailAddress { get; set; } - public string UnconfirmedEmailAddress { get; set; } - public string[] Roles { get; set; } - public CredentialAuditRecord[] Credentials { get; set; } - public CredentialAuditRecord[] AffectedCredential { get; set; } - public string AffectedEmailAddress { get; set; } - - public UserAuditRecord(User user, UserAuditAction action) - : this(user, action, Enumerable.Empty()) { } - public UserAuditRecord(User user, UserAuditAction action, Credential affected) - : this(user, action, SingleEnumerable(affected)) { } - public UserAuditRecord(User user, UserAuditAction action, IEnumerable affected) + public string Username { get; } + public string EmailAddress { get; } + public string UnconfirmedEmailAddress { get; } + public string[] Roles { get; } + public CredentialAuditRecord[] Credentials { get; } + public CredentialAuditRecord[] AffectedCredential { get; } + public string AffectedEmailAddress { get; } + + public UserAuditRecord(User user, AuditedUserAction action) + : this(user, action, Enumerable.Empty()) + { + } + + public UserAuditRecord(User user, AuditedUserAction action, Credential affected) + : this(user, action, SingleEnumerable(affected)) + { + } + + public UserAuditRecord(User user, AuditedUserAction action, IEnumerable affected) : base(action) { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + Username = user.Username; EmailAddress = user.EmailAddress; UnconfirmedEmailAddress = user.UnconfirmedEmailAddress; Roles = user.Roles.Select(r => r.Name).ToArray(); - Credentials = user.Credentials.Select(c => new CredentialAuditRecord(c, removed: false)).ToArray(); + Credentials = user.Credentials.Where(CredentialTypes.IsSupportedCredential) + .Select(c => new CredentialAuditRecord(c, removed: false)).ToArray(); if (affected != null) { - AffectedCredential = affected.Select(c => new CredentialAuditRecord(c, action == UserAuditAction.RemovedCredential)).ToArray(); + AffectedCredential = affected.Select(c => new CredentialAuditRecord(c, action == AuditedUserAction.RemoveCredential)).ToArray(); } Action = action; } - public UserAuditRecord(User user, UserAuditAction action, string affectedEmailAddress) - : this(user, action, Enumerable.Empty()) { + public UserAuditRecord(User user, AuditedUserAction action, string affectedEmailAddress) + : this(user, action, Enumerable.Empty()) + { AffectedEmailAddress = affectedEmailAddress; } @@ -56,34 +66,4 @@ private static IEnumerable SingleEnumerable(Credential affected) yield return affected; } } - - public class CredentialAuditRecord - { - public string Type { get; set; } - public string Value { get; set; } - public string Identity { get; set; } - - public CredentialAuditRecord(Credential credential, bool removed) - { - Type = credential.Type; - Identity = credential.Identity; - - // Track the value for credentials that are definitely revokable (API Key, etc.) and have been removed - if (removed && !CredentialTypes.IsPassword(credential.Type)) - { - Value = credential.Value; - } - } - } - - public enum UserAuditAction - { - Registered, - AddedCredential, - RemovedCredential, - RequestedPasswordReset, - ChangeEmail, - CancelChangeEmail, - ConfirmEmail, - } } diff --git a/src/NuGetGallery.Core/Strings.Designer.cs b/src/NuGetGallery.Core/CoreStrings.Designer.cs similarity index 77% rename from src/NuGetGallery.Core/Strings.Designer.cs rename to src/NuGetGallery.Core/CoreStrings.Designer.cs index 3938793cf2..3b5f610a4b 100644 --- a/src/NuGetGallery.Core/Strings.Designer.cs +++ b/src/NuGetGallery.Core/CoreStrings.Designer.cs @@ -22,24 +22,24 @@ namespace NuGetGallery { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { + public class CoreStrings { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { + internal CoreStrings() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NuGetGallery.Strings", typeof(Strings).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NuGetGallery.CoreStrings", typeof(CoreStrings).Assembly); resourceMan = temp; } return resourceMan; @@ -51,7 +51,7 @@ internal Strings() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal Strings() { /// /// Looks up a localized string similar to Unable to write audit record: '{0}'. Record already exists.. /// - internal static string CloudAuditingService_DuplicateAuditRecord { + public static string CloudAuditingService_DuplicateAuditRecord { get { return ResourceManager.GetString("CloudAuditingService_DuplicateAuditRecord", resourceCulture); } @@ -72,16 +72,34 @@ internal static string CloudAuditingService_DuplicateAuditRecord { /// /// Looks up a localized string similar to No handler for the {0} command is registered.. /// - internal static string CommandExecutor_UnhandledCommand { + public static string CommandExecutor_UnhandledCommand { get { return ResourceManager.GetString("CommandExecutor_UnhandledCommand", resourceCulture); } } + /// + /// Looks up a localized string similar to (404) Error - Not Found. + /// + public static string Http404NotFound { + get { + return ResourceManager.GetString("Http404NotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Index does not exist. + /// + public static string IndexDoesNotExist { + get { + return ResourceManager.GetString("IndexDoesNotExist", resourceCulture); + } + } + /// /// Looks up a localized string similar to The package manifest contains a duplicate dependency: {0} {1}. /// - internal static string Manifest_DuplicateDependency { + public static string Manifest_DuplicateDependency { get { return ResourceManager.GetString("Manifest_DuplicateDependency", resourceCulture); } @@ -90,7 +108,7 @@ internal static string Manifest_DuplicateDependency { /// /// Looks up a localized string similar to The package manifest contains an ID that is too long. Package IDs can be no longer than 100 characters.. /// - internal static string Manifest_IdTooLong { + public static string Manifest_IdTooLong { get { return ResourceManager.GetString("Manifest_IdTooLong", resourceCulture); } @@ -99,7 +117,7 @@ internal static string Manifest_IdTooLong { /// /// Looks up a localized string similar to The package manifest contains an invalid Dependency: '{0} {1}'. /// - internal static string Manifest_InvalidDependency { + public static string Manifest_InvalidDependency { get { return ResourceManager.GetString("Manifest_InvalidDependency", resourceCulture); } @@ -108,7 +126,7 @@ internal static string Manifest_InvalidDependency { /// /// Looks up a localized string similar to The package manifest contains an invalid ID: '{0}'. /// - internal static string Manifest_InvalidId { + public static string Manifest_InvalidId { get { return ResourceManager.GetString("Manifest_InvalidId", resourceCulture); } @@ -117,7 +135,7 @@ internal static string Manifest_InvalidId { /// /// Looks up a localized string similar to The package manifest contains an invalid Minimum Client Version: '{0}'. /// - internal static string Manifest_InvalidMinClientVersion { + public static string Manifest_InvalidMinClientVersion { get { return ResourceManager.GetString("Manifest_InvalidMinClientVersion", resourceCulture); } @@ -126,7 +144,7 @@ internal static string Manifest_InvalidMinClientVersion { /// /// Looks up a localized string similar to The package manifest contains an invalid Target Framework: '{0}'. /// - internal static string Manifest_InvalidTargetFramework { + public static string Manifest_InvalidTargetFramework { get { return ResourceManager.GetString("Manifest_InvalidTargetFramework", resourceCulture); } @@ -135,7 +153,7 @@ internal static string Manifest_InvalidTargetFramework { /// /// Looks up a localized string similar to The package manifest contains an invalid URL. /// - internal static string Manifest_InvalidUrl { + public static string Manifest_InvalidUrl { get { return ResourceManager.GetString("Manifest_InvalidUrl", resourceCulture); } @@ -144,7 +162,7 @@ internal static string Manifest_InvalidUrl { /// /// Looks up a localized string similar to The package manifest contains an invalid Version: '{0}'. /// - internal static string Manifest_InvalidVersion { + public static string Manifest_InvalidVersion { get { return ResourceManager.GetString("Manifest_InvalidVersion", resourceCulture); } @@ -153,7 +171,7 @@ internal static string Manifest_InvalidVersion { /// /// Looks up a localized string similar to The version '{0}' is not supported. The NuGet Gallery currently does not currently support Semantic Version 2.0 as it would break older NuGet clients.. /// - internal static string Manifest_InvalidVersionSemVer200 { + public static string Manifest_InvalidVersionSemVer200 { get { return ResourceManager.GetString("Manifest_InvalidVersionSemVer200", resourceCulture); } @@ -162,7 +180,7 @@ internal static string Manifest_InvalidVersionSemVer200 { /// /// Looks up a localized string similar to The package manifest is missing the Id field. /// - internal static string Manifest_MissingId { + public static string Manifest_MissingId { get { return ResourceManager.GetString("Manifest_MissingId", resourceCulture); } @@ -171,16 +189,25 @@ internal static string Manifest_MissingId { /// /// Looks up a localized string similar to The target framework {0} is not supported.. /// - internal static string Manifest_TargetFrameworkNotSupported { + public static string Manifest_TargetFrameworkNotSupported { get { return ResourceManager.GetString("Manifest_TargetFrameworkNotSupported", resourceCulture); } } + /// + /// Looks up a localized string similar to Negative indexes are invalid.. + /// + public static string NegativeIndexesAreInvalid { + get { + return ResourceManager.GetString("NegativeIndexesAreInvalid", resourceCulture); + } + } + /// /// Looks up a localized string similar to The version string is invalid.. /// - internal static string PackageMetadata_SetPropertiesFromMetadata_VersionStringInvalid { + public static string PackageMetadata_SetPropertiesFromMetadata_VersionStringInvalid { get { return ResourceManager.GetString("PackageMetadata_SetPropertiesFromMetadata_VersionStringInvalid", resourceCulture); } @@ -189,7 +216,7 @@ internal static string PackageMetadata_SetPropertiesFromMetadata_VersionStringIn /// /// Looks up a localized string similar to The version string '{0}' is invalid.. /// - internal static string PackageMetadata_VersionStringInvalid { + public static string PackageMetadata_VersionStringInvalid { get { return ResourceManager.GetString("PackageMetadata_VersionStringInvalid", resourceCulture); } @@ -198,7 +225,7 @@ internal static string PackageMetadata_VersionStringInvalid { /// /// Looks up a localized string similar to Must be a readable stream. /// - internal static string StreamMustBeReadable { + public static string StreamMustBeReadable { get { return ResourceManager.GetString("StreamMustBeReadable", resourceCulture); } @@ -207,7 +234,7 @@ internal static string StreamMustBeReadable { /// /// Looks up a localized string similar to Must be a seekable stream. /// - internal static string StreamMustBeSeekable { + public static string StreamMustBeSeekable { get { return ResourceManager.GetString("StreamMustBeSeekable", resourceCulture); } @@ -216,7 +243,7 @@ internal static string StreamMustBeSeekable { /// /// Looks up a localized string similar to Must be a writeable stream. /// - internal static string StreamMustBeWriteable { + public static string StreamMustBeWriteable { get { return ResourceManager.GetString("StreamMustBeWriteable", resourceCulture); } diff --git a/src/NuGetGallery.Core/Strings.resx b/src/NuGetGallery.Core/CoreStrings.resx similarity index 96% rename from src/NuGetGallery.Core/Strings.resx rename to src/NuGetGallery.Core/CoreStrings.resx index 38b70ce371..0fc99a2625 100644 --- a/src/NuGetGallery.Core/Strings.resx +++ b/src/NuGetGallery.Core/CoreStrings.resx @@ -1,174 +1,183 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 - - - Unable to write audit record: '{0}'. Record already exists. - - - No handler for the {0} command is registered. - - - The package manifest contains an ID that is too long. Package IDs can be no longer than 100 characters. - - - The package manifest contains an invalid Dependency: '{0} {1}' - - - The package manifest contains an invalid ID: '{0}' - - - The package manifest contains an invalid Minimum Client Version: '{0}' - - - The package manifest contains an invalid Target Framework: '{0}' - - - The package manifest contains an invalid URL - - - The package manifest contains an invalid Version: '{0}' - - - The package manifest is missing the Id field - - - Must be a readable stream - - - Must be a writeable stream - - - Must be a seekable stream - - - The package manifest contains a duplicate dependency: {0} {1} - - - The version string is invalid. - - - The version string '{0}' is invalid. - - - The target framework {0} is not supported. - - - The version '{0}' is not supported. The NuGet Gallery currently does not currently support Semantic Version 2.0 as it would break older NuGet clients. - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Unable to write audit record: '{0}'. Record already exists. + + + No handler for the {0} command is registered. + + + The package manifest contains an ID that is too long. Package IDs can be no longer than 100 characters. + + + The package manifest contains an invalid Dependency: '{0} {1}' + + + The package manifest contains an invalid ID: '{0}' + + + The package manifest contains an invalid Minimum Client Version: '{0}' + + + The package manifest contains an invalid Target Framework: '{0}' + + + The package manifest contains an invalid URL + + + The package manifest contains an invalid Version: '{0}' + + + The package manifest is missing the Id field + + + Must be a readable stream + + + Must be a writeable stream + + + Must be a seekable stream + + + The package manifest contains a duplicate dependency: {0} {1} + + + The version string is invalid. + + + The version string '{0}' is invalid. + + + The target framework {0} is not supported. + + + The version '{0}' is not supported. The NuGet Gallery currently does not currently support Semantic Version 2.0 as it would break older NuGet clients. + + + (404) Error - Not Found + + + Index does not exist + + + Negative indexes are invalid. + + \ No newline at end of file diff --git a/src/NuGetGallery.Core/CredentialTypes.cs b/src/NuGetGallery.Core/CredentialTypes.cs index 4b9645cf76..95fb0bcddb 100644 --- a/src/NuGetGallery.Core/CredentialTypes.cs +++ b/src/NuGetGallery.Core/CredentialTypes.cs @@ -1,10 +1,9 @@ // 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.Text; -using System.Threading.Tasks; namespace NuGetGallery { @@ -12,17 +11,65 @@ public static class CredentialTypes { public static class Password { - public static readonly string Prefix = "password."; - public static readonly string Pbkdf2 = Prefix + "pbkdf2"; - public static readonly string Sha1 = Prefix + "sha1"; + public const string Prefix = "password."; + public const string Pbkdf2 = Prefix + "pbkdf2"; + public const string Sha1 = Prefix + "sha1"; + public const string V3 = Prefix + "v3"; } - public static readonly string ApiKeyV1 = "apikey.v1"; - public static readonly string ExternalPrefix = "external."; + public static class ApiKey + { + public const string Prefix = "apikey."; + public const string V1 = Prefix + "v1"; + public const string V2 = Prefix + "v2"; + public const string VerifyV1 = Prefix + "verify.v1"; + } + + public const string ExternalPrefix = "external."; public static bool IsPassword(string type) { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + return type.StartsWith(Password.Prefix, StringComparison.OrdinalIgnoreCase); } + + public static bool IsApiKey(string type) + { + return type.StartsWith(ApiKey.Prefix, StringComparison.OrdinalIgnoreCase); + } + + public static bool IsPackageVerificationApiKey(string type) + { + return type.Equals(ApiKey.VerifyV1, StringComparison.OrdinalIgnoreCase); + } + + internal static List SupportedCredentialTypes = new List { Password.Sha1, Password.Pbkdf2, Password.V3, ApiKey.V1, ApiKey.V2 }; + + /// + /// Determines whether a credential is supported (internal or from the UI). For forward compatibility, + /// this version supports only the credentials below and ignores any others. + /// + /// + /// + public static bool IsSupportedCredential(Credential credential) + { + return IsViewSupportedCredential(credential) || IsPackageVerificationApiKey(credential.Type); + } + + /// + /// Determines whether a credential is supported from the user interface. For forward compatibility, + /// this version supports only the credentials below and ignores any others. + /// + /// + /// + public static bool IsViewSupportedCredential(Credential credential) + { + return SupportedCredentialTypes.Any(credType => string.Compare(credential.Type, credType, StringComparison.OrdinalIgnoreCase) == 0) + || credential.Type.StartsWith(ExternalPrefix, StringComparison.OrdinalIgnoreCase); + } } } diff --git a/src/NuGetGallery.Core/Entities/Credential.cs b/src/NuGetGallery.Core/Entities/Credential.cs index 01c856baaa..e2a72bc145 100644 --- a/src/NuGetGallery.Core/Entities/Credential.cs +++ b/src/NuGetGallery.Core/Entities/Credential.cs @@ -1,23 +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 System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace NuGetGallery { public class Credential : IEntity { + /// + /// Represents a credential used by NuGet Gallery. Can be an API key credential, + /// username/password or external credential like Microsoft Account or Azure Active Directory. + /// public Credential() { + Scopes = new List(); } + + /// + /// Represents a credential used by NuGet Gallery. Can be an API key credential, + /// username/password or external credential like Microsoft Account or Azure Active Directory. + /// + /// Credential type. See + /// Credential value public Credential(string type, string value) + : this() { Type = type; Value = value; } + /// + /// Represents a credential used by NuGet Gallery. Can be an API key credential, + /// username/password or external credential like Microsoft Account or Azure Active Directory. + /// + /// Credential type. See + /// Credential value + /// Optional expiration timespan for the credential. + public Credential(string type, string value, TimeSpan? expiration) + : this(type, value) + { + if (expiration.HasValue && expiration.Value > TimeSpan.Zero) + { + Expires = DateTime.UtcNow.Add(expiration.Value); + ExpirationTicks = expiration.Value.Ticks; + } + } + public int Key { get; set; } [Required] @@ -31,9 +64,47 @@ public Credential(string type, string value) [StringLength(maximumLength: 256)] public string Value { get; set; } + [StringLength(maximumLength: 256)] + public string Description { get; set; } + [StringLength(maximumLength: 256)] public string Identity { get; set; } + [DatabaseGenerated(DatabaseGeneratedOption.Computed)] + public DateTime Created { get; set; } + + public DateTime? Expires { get; set; } + + public long? ExpirationTicks { get; set; } + + public DateTime? LastUsed { get; set; } + public virtual User User { get; set; } + + public virtual ICollection Scopes { get; set; } + + [NotMapped] + public bool HasExpired + { + get + { + if (Expires.HasValue) + { + return DateTime.UtcNow >= Expires.Value; + } + + return false; + } + } + + public bool HasBeenUsedInLastDays(int numberOfDays) + { + if (numberOfDays > 0 && LastUsed.HasValue) + { + return LastUsed.Value.AddDays(numberOfDays) > DateTime.UtcNow; + } + + return true; + } } } diff --git a/src/NuGetGallery.Core/Entities/EntitiesContext.cs b/src/NuGetGallery.Core/Entities/EntitiesContext.cs index 1f21dd9fc0..fd3d3aca74 100644 --- a/src/NuGetGallery.Core/Entities/EntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/EntitiesContext.cs @@ -40,6 +40,7 @@ public EntitiesContext(string connectionString, bool readOnly) public IDbSet CuratedPackages { get; set; } public IDbSet PackageRegistrations { get; set; } public IDbSet Credentials { get; set; } + public IDbSet Scopes { get; set; } public IDbSet Users { get; set; } IDbSet IEntitiesContext.Set() @@ -72,7 +73,7 @@ public Database GetDatabase() return Database; } -#pragma warning disable 618 // TODO: remove Package.Authors completely once prodution services definitely no longer need it +#pragma warning disable 618 // TODO: remove Package.Authors completely once production services definitely no longer need it protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity() @@ -81,6 +82,15 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) .WithMany(u => u.Credentials) .HasForeignKey(c => c.UserKey); + modelBuilder.Entity() + .HasKey(c => c.Key); + + modelBuilder.Entity() + .HasRequired(sc => sc.Credential) + .WithMany(cr => cr.Scopes) + .HasForeignKey(sc => sc.CredentialKey) + .WillCascadeOnDelete(true); + modelBuilder.Entity() .HasKey(r => r.Key) .HasMany(r => r.Licenses) @@ -146,6 +156,11 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) .WithRequired(pd => pd.Package) .HasForeignKey(pd => pd.PackageKey); + modelBuilder.Entity() + .HasMany(p => p.PackageTypes) + .WithRequired(pt => pt.Package) + .HasForeignKey(pt => pt.PackageKey); + modelBuilder.Entity() .HasKey(pm => pm.Key); diff --git a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs index 7de3a3419c..20c3d645c0 100644 --- a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs @@ -12,6 +12,8 @@ public interface IEntitiesContext IDbSet CuratedPackages { get; set; } IDbSet PackageRegistrations { get; set; } IDbSet Credentials { get; set; } + IDbSet Scopes { get; set; } + IDbSet Users { get; set; } Task SaveChangesAsync(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Set", Justification="This is to match the EF terminology.")] diff --git a/src/NuGetGallery.Core/Entities/Package.cs b/src/NuGetGallery.Core/Entities/Package.cs index 33403f0823..d11d794cd1 100644 --- a/src/NuGetGallery.Core/Entities/Package.cs +++ b/src/NuGetGallery.Core/Entities/Package.cs @@ -13,13 +13,14 @@ public class Package : IEntity { -#pragma warning disable 618 // TODO: remove Package.Authors completely once prodution services definitely no longer need it +#pragma warning disable 618 // TODO: remove Package.Authors completely once production services definitely no longer need it public Package() { Authors = new HashSet(); Dependencies = new HashSet(); PackageEdits = new HashSet(); PackageHistories = new HashSet(); + PackageTypes = new HashSet(); SupportedFrameworks = new HashSet(); Listed = true; } @@ -41,6 +42,8 @@ public Package() public virtual ICollection Dependencies { get; set; } + public virtual ICollection PackageTypes { get; set; } + /// /// Has a max length of 4000. Is not indexed but *IS* used for searches. Db column is nvarchar(max). /// @@ -85,6 +88,10 @@ public Package() /// /// This is when the Package Metadata was last edited by a user. Or NULL. In UTC. + /// + /// This field is updated by a trigger on the database if it is edited. + /// This trigger is defined by a migration named "AddTriggerForPackagesLastEdited". + /// The trigger guarantees that the timestamps of multiple instances of the gallery do not conflict. /// public DateTime? LastEdited { get; set; } @@ -139,7 +146,7 @@ public Package() public virtual ICollection LicenseReports { get; set; } - // Pre-calcuated data for the feed + // Pre-calculated data for the feed public string LicenseNames { get; set; } public string LicenseReportUrl { get; set; } @@ -150,6 +157,9 @@ public Package() public string FlattenedAuthors { get; set; } public string FlattenedDependencies { get; set; } + + public string FlattenedPackageTypes { get; set; } + public int Key { get; set; } [StringLength(44)] diff --git a/src/NuGetGallery.Core/Entities/PackageLicense.cs b/src/NuGetGallery.Core/Entities/PackageLicense.cs index 4cf175cac5..509f8117ec 100644 --- a/src/NuGetGallery.Core/Entities/PackageLicense.cs +++ b/src/NuGetGallery.Core/Entities/PackageLicense.cs @@ -12,7 +12,7 @@ public class PackageLicense public int Key { get; set; } [Required] - [StringLength(CoreConstants.MaxPackageIdLength)] + [StringLength(128)] public string Name { get; set; } public virtual ICollection Reports { get; set; } diff --git a/src/NuGetGallery.Core/Entities/PackageType.cs b/src/NuGetGallery.Core/Entities/PackageType.cs new file mode 100644 index 0000000000..0d9ccb6d9b --- /dev/null +++ b/src/NuGetGallery.Core/Entities/PackageType.cs @@ -0,0 +1,24 @@ +// 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 +{ + public class PackageType + : IEntity + { + public Package Package { get; set; } + public int PackageKey { get; set; } + + /// + /// Has a max length of 512. Is indexed. Db column is nvarchar(512). + /// + public string Name { get; set; } + + public string Version { get; set; } + + [Key] + public int Key { get; set; } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Entities/Scope.cs b/src/NuGetGallery.Core/Entities/Scope.cs new file mode 100644 index 0000000000..aef5cc562b --- /dev/null +++ b/src/NuGetGallery.Core/Entities/Scope.cs @@ -0,0 +1,39 @@ +// 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.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace NuGetGallery +{ + public class Scope + : IEntity + { + [JsonIgnore] + public int Key { get; set; } + + [JsonIgnore] + public int CredentialKey { get; set; } + + [JsonProperty("s")] + public string Subject { get; set; } + + [Required] + [JsonProperty("a")] + public string AllowedAction { get; set; } + + [JsonIgnore] + public virtual Credential Credential { get; set; } + + public Scope() + { + } + + public Scope(string subject, string allowedAction) + { + Subject = subject; + AllowedAction = allowedAction; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Entities/User.cs b/src/NuGetGallery.Core/Entities/User.cs index 160203f3c5..398d995dda 100644 --- a/src/NuGetGallery.Core/Entities/User.cs +++ b/src/NuGetGallery.Core/Entities/User.cs @@ -9,8 +9,7 @@ namespace NuGetGallery { - public class User - : IEntity + public class User : IEntity { public User() : this(null) { @@ -57,6 +56,10 @@ public bool Confirmed public DateTime? CreatedUtc { get; set; } + public DateTime? LastFailedLoginUtc { get; set; } + + public int FailedLoginCount { get; set; } + public string LastSavedEmailAddress { get diff --git a/src/NuGetGallery/Infrastructure/AzureEntityList.cs b/src/NuGetGallery.Core/Infrastructure/AzureEntityList.cs similarity index 98% rename from src/NuGetGallery/Infrastructure/AzureEntityList.cs rename to src/NuGetGallery.Core/Infrastructure/AzureEntityList.cs index 3df53f2c2f..0cc879b894 100644 --- a/src/NuGetGallery/Infrastructure/AzureEntityList.cs +++ b/src/NuGetGallery.Core/Infrastructure/AzureEntityList.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; using System.Collections.Generic; using System.Diagnostics; @@ -83,12 +84,12 @@ public T this[long index] { if (index < 0) { - throw new ArgumentOutOfRangeException(nameof(index), index, Strings.NegativeIndexesAreInvalid); + throw new ArgumentOutOfRangeException(nameof(index), index, CoreStrings.NegativeIndexesAreInvalid); } if (index >= LongCount) { - throw new ArgumentOutOfRangeException(nameof(index), index, Strings.IndexDoesNotExist); + throw new ArgumentOutOfRangeException(nameof(index), index, CoreStrings.IndexDoesNotExist); } long page = index / 1000; @@ -99,7 +100,7 @@ public T this[long index] var response = _tableRef.Execute(TableOperation.Retrieve(partitionKey, rowKey)); if (response.HttpStatusCode == 404) { - throw new ArgumentOutOfRangeException(nameof(index), index, Strings.Http404NotFound); + throw new ArgumentOutOfRangeException(nameof(index), index, CoreStrings.Http404NotFound); } ThrowIfErrorStatus(response); @@ -110,12 +111,12 @@ public T this[long index] { if (index < 0) { - throw new ArgumentOutOfRangeException(nameof(index), index, Strings.NegativeIndexesAreInvalid); + throw new ArgumentOutOfRangeException(nameof(index), index, CoreStrings.NegativeIndexesAreInvalid); } if (index >= LongCount) { - throw new ArgumentOutOfRangeException(nameof(index), index, Strings.IndexDoesNotExist); + throw new ArgumentOutOfRangeException(nameof(index), index, CoreStrings.IndexDoesNotExist); } long page = index / 1000; diff --git a/src/NuGetGallery/Infrastructure/TableErrorLog.cs b/src/NuGetGallery.Core/Infrastructure/TableErrorLog.cs similarity index 92% rename from src/NuGetGallery/Infrastructure/TableErrorLog.cs rename to src/NuGetGallery.Core/Infrastructure/TableErrorLog.cs index a24ceb856a..b588d2e602 100644 --- a/src/NuGetGallery/Infrastructure/TableErrorLog.cs +++ b/src/NuGetGallery.Core/Infrastructure/TableErrorLog.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; using System.Collections; using System.Collections.Generic; @@ -7,7 +8,6 @@ using System.Globalization; using System.Linq; using Elmah; -using Microsoft.WindowsAzure.ServiceRuntime; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Table; @@ -143,23 +143,12 @@ public class TableErrorLog : ErrorLog private readonly string _connectionString; private readonly AzureEntityList _entityList; - public TableErrorLog(IDictionary config) - { - _connectionString = (string)config["connectionString"] ?? RoleEnvironment.GetConfigurationSettingValue((string)config["connectionStringName"]); - _entityList = new AzureEntityList(_connectionString, TableName); - } - public TableErrorLog(string connectionString) { _connectionString = connectionString; _entityList = new AzureEntityList(connectionString, TableName); } - public TableErrorLog() - { - _entityList = new AzureEntityList(_connectionString, TableName); - } - public override ErrorLogEntry GetError(string id) { long pos = Int64.Parse(id, CultureInfo.InvariantCulture); diff --git a/src/NuGetGallery.Core/NuGetGallery.Core.csproj b/src/NuGetGallery.Core/NuGetGallery.Core.csproj index 4c1a9945f4..56e7e0e70a 100644 --- a/src/NuGetGallery.Core/NuGetGallery.Core.csproj +++ b/src/NuGetGallery.Core/NuGetGallery.Core.csproj @@ -1,6 +1,5 @@  - Debug @@ -25,7 +24,7 @@ DEBUG;TRACE prompt 4 - ..\Frontend.ruleset + ..\FrontendSDL.ruleset true @@ -37,6 +36,10 @@ 4 + + ..\..\packages\elmah.corelibrary.strongname.1.2.2\lib\Elmah.dll + True + False ..\..\packages\entityframework.6.1.3\lib\net45\EntityFramework.dll @@ -47,6 +50,9 @@ ..\..\packages\entityframework.6.1.3\lib\net45\EntityFramework.SqlServer.dll True + + ..\..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll + False ..\..\packages\Microsoft.Data.Edm.5.6.5-beta\lib\net40\Microsoft.Data.Edm.dll @@ -72,49 +78,29 @@ ..\..\packages\Microsoft.WindowsAzure.ConfigurationManager.3.1.0\lib\net40\Microsoft.WindowsAzure.Configuration.dll True - - False - ..\..\packages\WindowsAzure.Storage.4.3.0\lib\net40\Microsoft.WindowsAzure.Storage.dll - True - - - ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - True + + ..\..\packages\WindowsAzure.Storage.7.0.0\lib\net40\Microsoft.WindowsAzure.Storage.dll - - False - ..\..\packages\NuGet.Common.3.5.0-beta-final\lib\net45\NuGet.Common.dll - True + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - - False - ..\..\packages\NuGet.Frameworks.3.5.0-beta-final\lib\net45\NuGet.Frameworks.dll - True + + ..\..\packages\NuGet.Common.4.0.0\lib\net45\NuGet.Common.dll - - False - ..\..\packages\NuGet.Logging.3.5.0-beta-1160\lib\net45\NuGet.Logging.dll - True + + ..\..\packages\NuGet.Frameworks.4.0.0\lib\net45\NuGet.Frameworks.dll - - False - ..\..\packages\NuGet.Packaging.3.5.0-beta-final\lib\net45\NuGet.Packaging.dll - True + + ..\..\packages\NuGet.Packaging.4.0.0\lib\net45\NuGet.Packaging.dll - - False - ..\..\packages\NuGet.Packaging.Core.3.5.0-beta-final\lib\net45\NuGet.Packaging.Core.dll - True + + ..\..\packages\NuGet.Packaging.Core.4.0.0\lib\net45\NuGet.Packaging.Core.dll - - False - ..\..\packages\NuGet.Packaging.Core.Types.3.5.0-beta-final\lib\net45\NuGet.Packaging.Core.Types.dll - True + + ..\..\packages\NuGet.Packaging.Core.Types.4.0.0\lib\net45\NuGet.Packaging.Core.Types.dll - - False - ..\..\packages\NuGet.Versioning.3.5.0-beta-final\lib\net45\NuGet.Versioning.dll - True + + ..\..\packages\NuGet.Versioning.4.0.0\lib\net45\NuGet.Versioning.dll @@ -135,12 +121,28 @@ + + + + + + + + + + + + + + + + @@ -172,9 +174,13 @@ + + + + @@ -182,36 +188,36 @@ + + - + True True - Strings.resx + CoreStrings.resx - + + Designer + - - ResXFileCodeGenerator - Strings.Designer.cs + + PublicResXFileCodeGenerator + CoreStrings.Designer.cs Designer - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - + + ..\..\build + + \ No newline at end of file diff --git a/src/NuGetGallery.Core/NuGetVersionExtensions.cs b/src/NuGetGallery.Core/NuGetVersionExtensions.cs index 04f8e3d57d..7389214862 100644 --- a/src/NuGetGallery.Core/NuGetVersionExtensions.cs +++ b/src/NuGetGallery.Core/NuGetVersionExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Text.RegularExpressions; using NuGet.Versioning; namespace NuGetGallery @@ -23,6 +24,9 @@ public static string Normalize(string version) public static class NuGetVersionExtensions { + private const RegexOptions Flags = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; + private static readonly Regex SemanticVersionRegex = new Regex(@"^(?\d+(\s*\.\s*\d+){0,3})(?-[a-z][0-9a-z-]*)?$", Flags); + public static string ToNormalizedStringSafe(this NuGetVersion self) { return self != null ? self.ToNormalizedString() : String.Empty; @@ -32,5 +36,12 @@ public static bool IsSemVer200(this NuGetVersion self) { return self.ReleaseLabels.Count() > 1 || self.HasMetadata; } + + public static bool IsValidVersionForLegacyClients(this NuGetVersion self) + { + var match = SemanticVersionRegex.Match(self.ToString().Trim()); + + return match.Success; + } } } \ No newline at end of file diff --git a/src/NuGetGallery.Core/Packaging/ManifestEdit.cs b/src/NuGetGallery.Core/Packaging/ManifestEdit.cs index d2d8963689..688e77718d 100644 --- a/src/NuGetGallery.Core/Packaging/ManifestEdit.cs +++ b/src/NuGetGallery.Core/Packaging/ManifestEdit.cs @@ -1,6 +1,11 @@ +// 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.Packaging { - public class ManifestEdit + public class ManifestEdit : ICloneable { public string Title { get; set; } public string Authors { get; set; } @@ -13,5 +18,10 @@ public class ManifestEdit public bool RequireLicenseAcceptance { get; set; } public string Summary { get; set; } public string Tags { get; set; } + + public object Clone() + { + return this.MemberwiseClone(); + } } } \ No newline at end of file diff --git a/src/NuGetGallery.Core/Packaging/ManifestValidator.cs b/src/NuGetGallery.Core/Packaging/ManifestValidator.cs index 78ffb1f135..4da42fce42 100644 --- a/src/NuGetGallery.Core/Packaging/ManifestValidator.cs +++ b/src/NuGetGallery.Core/Packaging/ManifestValidator.cs @@ -14,9 +14,6 @@ namespace NuGetGallery.Packaging { public class ManifestValidator { - // Copy-pasta from NuGet: src/Core/Utility/PackageIdValidator.cs because that constant is internal :( - public static readonly int MaxPackageIdLength = 100; - public static IEnumerable Validate(Stream nuspecStream, out NuspecReader nuspecReader) { try @@ -42,24 +39,24 @@ private static IEnumerable ValidateCore(PackageMetadata packag // Validate the ID if (string.IsNullOrEmpty(packageMetadata.Id)) { - yield return new ValidationResult(Strings.Manifest_MissingId); + yield return new ValidationResult(CoreStrings.Manifest_MissingId); } else { - if (packageMetadata.Id.Length > MaxPackageIdLength) + if (packageMetadata.Id.Length > NuGet.Packaging.PackageIdValidator.MaxPackageIdLength) { - yield return new ValidationResult(Strings.Manifest_IdTooLong); + yield return new ValidationResult(CoreStrings.Manifest_IdTooLong); } else if (!PackageIdValidator.IsValidPackageId(packageMetadata.Id)) { yield return new ValidationResult(String.Format( CultureInfo.CurrentCulture, - Strings.Manifest_InvalidId, + CoreStrings.Manifest_InvalidId, packageMetadata.Id)); } } - // Check URL properties + // Check and validate URL properties foreach (var result in CheckUrls( packageMetadata.GetValueFromMetadata("IconUrl"), packageMetadata.GetValueFromMetadata("ProjectUrl"), @@ -75,16 +72,14 @@ private static IEnumerable ValidateCore(PackageMetadata packag yield return new ValidationResult(String.Format( CultureInfo.CurrentCulture, - Strings.Manifest_InvalidVersion, + CoreStrings.Manifest_InvalidVersion, version)); } - if (packageMetadata.Version.IsSemVer200()) - { - yield return new ValidationResult(String.Format( - CultureInfo.CurrentCulture, - Strings.Manifest_InvalidVersionSemVer200, - packageMetadata.Version.ToFullString())); + var versionValidationResult = ValidateVersion(packageMetadata.Version); + if (versionValidationResult != null) + { + yield return versionValidationResult; } // Check framework reference groups @@ -98,7 +93,7 @@ private static IEnumerable ValidateCore(PackageMetadata packag { yield return new ValidationResult(String.Format( CultureInfo.CurrentCulture, - Strings.Manifest_TargetFrameworkNotSupported, + CoreStrings.Manifest_TargetFrameworkNotSupported, frameworkReferenceGroup?.TargetFramework?.ToString())); } } @@ -119,11 +114,11 @@ private static IEnumerable ValidateCore(PackageMetadata packag { yield return new ValidationResult(String.Format( CultureInfo.CurrentCulture, - Strings.Manifest_TargetFrameworkNotSupported, + CoreStrings.Manifest_TargetFrameworkNotSupported, dependencyGroup.TargetFramework?.ToString())); } - // Verify package id's + // Verify package id's and versions foreach (var dependency in dependencyGroup.Packages) { bool duplicate = !dependencyIds.Add(dependency.Id); @@ -131,7 +126,7 @@ private static IEnumerable ValidateCore(PackageMetadata packag { yield return new ValidationResult(String.Format( CultureInfo.CurrentCulture, - Strings.Manifest_DuplicateDependency, + CoreStrings.Manifest_DuplicateDependency, dependencyGroup.TargetFramework.GetShortFolderName(), dependency.Id)); } @@ -140,23 +135,67 @@ private static IEnumerable ValidateCore(PackageMetadata packag { yield return new ValidationResult(String.Format( CultureInfo.CurrentCulture, - Strings.Manifest_InvalidDependency, + CoreStrings.Manifest_InvalidDependency, dependency.Id, dependency.VersionRange.OriginalString)); } + + // Versions + if (dependency.VersionRange.MinVersion != null) + { + var versionRangeValidationResult = ValidateVersion(dependency.VersionRange.MinVersion); + if (versionRangeValidationResult != null) + { + yield return versionRangeValidationResult; + } + } + + if (dependency.VersionRange.MaxVersion != null + && dependency.VersionRange.MaxVersion != dependency.VersionRange.MinVersion) + { + var versionRangeValidationResult = ValidateVersion(dependency.VersionRange.MaxVersion); + if (versionRangeValidationResult != null) + { + yield return versionRangeValidationResult; + } + } } } } } + private static ValidationResult ValidateVersion(NuGetVersion version) + { + if (version.IsSemVer200()) + { + return new ValidationResult(string.Format( + CultureInfo.CurrentCulture, + CoreStrings.Manifest_InvalidVersionSemVer200, + version.ToFullString())); + } + else if (!version.IsValidVersionForLegacyClients()) + { + return new ValidationResult(string.Format( + CultureInfo.CurrentCulture, + CoreStrings.Manifest_InvalidVersion, + version)); + } + + return null; + } + private static IEnumerable CheckUrls(params string[] urls) { foreach (var url in urls) { - Uri _; - if (!String.IsNullOrEmpty(url) && !Uri.TryCreate(url, UriKind.Absolute, out _)) + Uri uri = null; + if (!string.IsNullOrEmpty(url) && !Uri.TryCreate(url, UriKind.Absolute, out uri)) + { + yield return new ValidationResult(CoreStrings.Manifest_InvalidUrl); + } + else if (uri != null && uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeHttp) { - yield return new ValidationResult(Strings.Manifest_InvalidUrl); + yield return new ValidationResult(CoreStrings.Manifest_InvalidUrl); } } } diff --git a/src/NuGetGallery.Core/Packaging/NupkgRewriter.cs b/src/NuGetGallery.Core/Packaging/NupkgRewriter.cs index 878ebc6d79..8b61a8e9fc 100644 --- a/src/NuGetGallery.Core/Packaging/NupkgRewriter.cs +++ b/src/NuGetGallery.Core/Packaging/NupkgRewriter.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; using System.Collections.Generic; using System.Globalization; @@ -26,17 +27,17 @@ public static void RewriteNupkgManifest(Stream readWriteStream, IEnumerable node from nuspec var metadataNode = nuspecReader.Xml.Root.Elements() - .FirstOrDefault(e => StringComparer.Ordinal.Equals(e.Name.LocalName, "metadata")); + .FirstOrDefault(e => StringComparer.Ordinal.Equals(e.Name.LocalName, PackageMetadataStrings.Metadata)); if (metadataNode == null) { - throw new PackagingException("The package manifest is missing the 'metadata' node."); + throw new PackagingException($"The package manifest is missing the '{PackageMetadataStrings.Metadata}' node."); } // Convert metadata into a ManifestEdit so that we can run it through the editing pipeline var editableManifestElements = new ManifestEdit { - Title = ReadFromMetadata(metadataNode, "title"), - Authors = ReadFromMetadata(metadataNode, "authors"), - Copyright = ReadFromMetadata(metadataNode, "copyright"), - Description = ReadFromMetadata(metadataNode, "description"), - IconUrl = ReadFromMetadata(metadataNode, "iconUrl"), - LicenseUrl = ReadFromMetadata(metadataNode, "licenseUrl"), - ProjectUrl = ReadFromMetadata(metadataNode, "projectUrl"), - ReleaseNotes = ReadFromMetadata(metadataNode, "releasenotes"), - RequireLicenseAcceptance = ReadBoolFromMetadata(metadataNode, "requireLicenseAcceptance"), - Summary = ReadFromMetadata(metadataNode, "summary"), - Tags = ReadFromMetadata(metadataNode, "tags") + Title = ReadFromMetadata(metadataNode, PackageMetadataStrings.Title), + Authors = ReadFromMetadata(metadataNode, PackageMetadataStrings.Authors), + Copyright = ReadFromMetadata(metadataNode, PackageMetadataStrings.Copyright), + Description = ReadFromMetadata(metadataNode, PackageMetadataStrings.Description), + IconUrl = ReadFromMetadata(metadataNode, PackageMetadataStrings.IconUrl), + LicenseUrl = ReadFromMetadata(metadataNode, PackageMetadataStrings.LicenseUrl), + ProjectUrl = ReadFromMetadata(metadataNode, PackageMetadataStrings.ProjectUrl), + ReleaseNotes = ReadFromMetadata(metadataNode, PackageMetadataStrings.ReleaseNotes), + RequireLicenseAcceptance = ReadBoolFromMetadata(metadataNode, PackageMetadataStrings.RequireLicenseAcceptance), + Summary = ReadFromMetadata(metadataNode, PackageMetadataStrings.Summary), + Tags = ReadFromMetadata(metadataNode, PackageMetadataStrings.Tags) }; - + + var originalManifestElements = (ManifestEdit)editableManifestElements.Clone(); // Perform edits foreach (var edit in edits) { @@ -74,18 +76,25 @@ public static void RewriteNupkgManifest(Stream readWriteStream, IEnumerable node - WriteToMetadata(metadataNode, "title", editableManifestElements.Title); - WriteToMetadata(metadataNode, "authors", editableManifestElements.Authors); - WriteToMetadata(metadataNode, "copyright", editableManifestElements.Copyright); - WriteToMetadata(metadataNode, "description", editableManifestElements.Description); - WriteToMetadata(metadataNode, "iconUrl", editableManifestElements.IconUrl); - WriteToMetadata(metadataNode, "licenseUrl", editableManifestElements.LicenseUrl); - WriteToMetadata(metadataNode, "projectUrl", editableManifestElements.ProjectUrl); - WriteToMetadata(metadataNode, "releasenotes", editableManifestElements.ReleaseNotes); - WriteToMetadata(metadataNode, "requireLicenseAcceptance", editableManifestElements.RequireLicenseAcceptance.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); - WriteToMetadata(metadataNode, "summary", editableManifestElements.Summary); - WriteToMetadata(metadataNode, "tags", editableManifestElements.Tags); - + // Modify metadata elements only if they are changed. + // 1. Do not add empty/null elements to metadata + // 2. Remove the empty/null elements from metadata after edit + // Apart from Authors, Description, Id and Version all other elements are optional. + // Defined by spec here: https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Packaging/compiler/resources/nuspec.xsd + WriteToMetadata(metadataNode, PackageMetadataStrings.Title, originalManifestElements.Title, editableManifestElements.Title, canBeRemoved: true); + WriteToMetadata(metadataNode, PackageMetadataStrings.Authors, originalManifestElements.Authors, editableManifestElements.Authors); + WriteToMetadata(metadataNode, PackageMetadataStrings.Copyright, originalManifestElements.Copyright, editableManifestElements.Copyright, canBeRemoved: true); + WriteToMetadata(metadataNode, PackageMetadataStrings.Description, originalManifestElements.Description, editableManifestElements.Description); + WriteToMetadata(metadataNode, PackageMetadataStrings.IconUrl, originalManifestElements.IconUrl, editableManifestElements.IconUrl, canBeRemoved: true); + WriteToMetadata(metadataNode, PackageMetadataStrings.LicenseUrl, originalManifestElements.LicenseUrl, editableManifestElements.LicenseUrl, canBeRemoved: true); + WriteToMetadata(metadataNode, PackageMetadataStrings.ProjectUrl, originalManifestElements.ProjectUrl, editableManifestElements.ProjectUrl, canBeRemoved: true); + WriteToMetadata(metadataNode, PackageMetadataStrings.ReleaseNotes, originalManifestElements.ReleaseNotes, editableManifestElements.ReleaseNotes, canBeRemoved: true); + WriteToMetadata(metadataNode, PackageMetadataStrings.RequireLicenseAcceptance, + originalManifestElements.RequireLicenseAcceptance.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(), + editableManifestElements.RequireLicenseAcceptance.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + WriteToMetadata(metadataNode, PackageMetadataStrings.Summary, originalManifestElements.Summary, editableManifestElements.Summary, canBeRemoved: true); + WriteToMetadata(metadataNode, PackageMetadataStrings.Tags, originalManifestElements.Tags, editableManifestElements.Tags, canBeRemoved: true); + // Update the package stream using (var newManifestStream = new MemoryStream()) { @@ -125,12 +134,7 @@ private static string ReadFromMetadata(XElement metadataElement, string elementN var element = metadataElement.Elements(XName.Get(elementName, metadataElement.GetDefaultNamespace().NamespaceName)) .FirstOrDefault(); - if (element != null) - { - return element.Value; - } - - return null; + return element?.Value; } private static bool ReadBoolFromMetadata(XElement metadataElement, string elementName) @@ -149,18 +153,31 @@ private static bool ReadBoolFromMetadata(XElement metadataElement, string elemen return false; } - private static void WriteToMetadata(XElement metadataElement, string elementName, string value) + private static void WriteToMetadata(XElement metadataElement, string elementName, string oldValue, string newValue, bool canBeRemoved = false) { + if (oldValue == newValue) + { + return; + } + var element = metadataElement.Elements(XName.Get(elementName, metadataElement.GetDefaultNamespace().NamespaceName)) .FirstOrDefault(); if (element != null) { - element.Value = value; + // Always set a non-null newValue for an element. For null values remove the element if possible. + if (!string.IsNullOrEmpty(newValue)) + { + element.Value = newValue; + } + else if (canBeRemoved) + { + element.Remove(); + } } - else + else if (!string.IsNullOrEmpty(newValue)) { - metadataElement.Add(new XElement(XName.Get(elementName, metadataElement.GetDefaultNamespace().NamespaceName), value)); + metadataElement.Add(new XElement(XName.Get(elementName, metadataElement.GetDefaultNamespace().NamespaceName), newValue)); } } } diff --git a/src/NuGetGallery.Core/Packaging/PackageMetadata.cs b/src/NuGetGallery.Core/Packaging/PackageMetadata.cs index d4c86e9b56..d45400c08b 100644 --- a/src/NuGetGallery.Core/Packaging/PackageMetadata.cs +++ b/src/NuGetGallery.Core/Packaging/PackageMetadata.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using NuGet.Packaging; +using NuGet.Packaging.Core; using NuGet.Versioning; namespace NuGetGallery.Packaging @@ -14,16 +15,19 @@ public class PackageMetadata private readonly Dictionary _metadata; private readonly IReadOnlyCollection _dependencyGroups; private readonly IReadOnlyCollection _frameworkReferenceGroups; + private readonly IReadOnlyCollection _packageTypes; public PackageMetadata( Dictionary metadata, IEnumerable dependencyGroups, IEnumerable frameworkGroups, + IEnumerable packageTypes, NuGetVersion minClientVersion) { _metadata = new Dictionary(metadata, StringComparer.OrdinalIgnoreCase); _dependencyGroups = dependencyGroups.ToList().AsReadOnly(); _frameworkReferenceGroups = frameworkGroups.ToList().AsReadOnly(); + _packageTypes = packageTypes.ToList().AsReadOnly(); SetPropertiesFromMetadata(); MinClientVersion = minClientVersion; @@ -36,7 +40,7 @@ private void SetPropertiesFromMetadata() var versionString = GetValue("version", string.Empty); if (versionString.IndexOf('.') < 0) { - throw new FormatException(string.Format(Strings.PackageMetadata_VersionStringInvalid, versionString)); + throw new FormatException(string.Format(CoreStrings.PackageMetadata_VersionStringInvalid, versionString)); } NuGetVersion nugetVersion; @@ -45,21 +49,21 @@ private void SetPropertiesFromMetadata() Version = nugetVersion; } - IconUrl = GetValue("iconUrl", (Uri) null); - ProjectUrl = GetValue("projectUrl", (Uri) null); - LicenseUrl = GetValue("licenseUrl", (Uri) null); - Copyright = GetValue("copyright", (string) null); - Description = GetValue("description", (string) null); - ReleaseNotes = GetValue("releaseNotes", (string) null); - RequireLicenseAcceptance = GetValue("requireLicenseAcceptance", false); - Summary = GetValue("summary", (string) null); - Title = GetValue("title", (string) null); - Tags = GetValue("tags", (string) null); - Language = GetValue("language", (string) null); - - Owners = GetValue("owners", (string) null); - - var authorsString = GetValue("authors", Owners ?? string.Empty); + IconUrl = GetValue(PackageMetadataStrings.IconUrl, (Uri) null); + ProjectUrl = GetValue(PackageMetadataStrings.ProjectUrl, (Uri) null); + LicenseUrl = GetValue(PackageMetadataStrings.LicenseUrl, (Uri) null); + Copyright = GetValue(PackageMetadataStrings.Copyright, (string) null); + Description = GetValue(PackageMetadataStrings.Description, (string) null); + ReleaseNotes = GetValue(PackageMetadataStrings.ReleaseNotes, (string) null); + RequireLicenseAcceptance = GetValue(PackageMetadataStrings.RequireLicenseAcceptance, false); + Summary = GetValue(PackageMetadataStrings.Summary, (string) null); + Title = GetValue(PackageMetadataStrings.Title, (string) null); + Tags = GetValue(PackageMetadataStrings.Tags, (string) null); + Language = GetValue(PackageMetadataStrings.Language, (string) null); + + Owners = GetValue(PackageMetadataStrings.Owners, (string) null); + + var authorsString = GetValue(PackageMetadataStrings.Authors, Owners ?? string.Empty); Authors = new List(authorsString.Split(',').Select(author => author.Trim())); } @@ -96,6 +100,11 @@ public IReadOnlyCollection GetFrameworkReferenceGroups() return _frameworkReferenceGroups; } + public IReadOnlyCollection GetPackageTypes() + { + return _packageTypes; + } + private string GetValue(string key, string alternateValue) { string value; @@ -141,6 +150,7 @@ public static PackageMetadata FromNuspecReader(NuspecReader nuspecReader) nuspecReader.GetMetadata().ToDictionary(kvp => kvp.Key, kvp => kvp.Value), nuspecReader.GetDependencyGroups(), nuspecReader.GetFrameworkReferenceGroups(), + nuspecReader.GetPackageTypes(), nuspecReader.GetMinClientVersion() ); } diff --git a/src/NuGetGallery.Core/Packaging/PackageMetadataStrings.cs b/src/NuGetGallery.Core/Packaging/PackageMetadataStrings.cs new file mode 100644 index 0000000000..6408fb7525 --- /dev/null +++ b/src/NuGetGallery.Core/Packaging/PackageMetadataStrings.cs @@ -0,0 +1,23 @@ +// 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.Packaging +{ + public static class PackageMetadataStrings + { + public const string Authors = "authors"; + public const string Copyright = "copyright"; + public const string Description = "description"; + public const string IconUrl = "iconUrl"; + public const string Language = "language"; + public const string LicenseUrl = "licenseUrl"; + public const string Metadata = "metadata"; + public const string Owners = "owners"; + public const string ProjectUrl = "projectUrl"; + public const string ReleaseNotes = "releaseNotes"; + public const string RequireLicenseAcceptance = "requireLicenseAcceptance"; + public const string Summary = "summary"; + public const string Tags = "tags"; + public const string Title = "title"; + } +} diff --git a/src/NuGetGallery.Core/Properties/AssemblyInfo.cs b/src/NuGetGallery.Core/Properties/AssemblyInfo.cs index a4df84a959..6ce363c53e 100644 --- a/src/NuGetGallery.Core/Properties/AssemblyInfo.cs +++ b/src/NuGetGallery.Core/Properties/AssemblyInfo.cs @@ -1,9 +1,36 @@ // 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.Reflection; +using System.Resources; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; [assembly: AssemblyTitle("NuGetGallery.Core")] +[assembly: AssemblyCompany(".NET Foundation")] +[assembly: AssemblyProduct("NuGet Services")] +[assembly: AssemblyCopyright("\x00a9 .NET Foundation. All rights reserved.")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +#if !PORTABLE +[assembly: ComVisible(false)] +#endif + +#if DEBUG +[assembly: AssemblyConfiguration("Debug")] +#else +[assembly: AssemblyConfiguration("Release")] +#endif + +[assembly: CLSCompliant(false)] +[assembly: NeutralResourcesLanguage("en-us")] + [assembly: AssemblyDescription("Core support library for NuGet Gallery Frontend and Backend")] -[assembly: InternalsVisibleTo("NuGetGallery.Core.Facts")] \ No newline at end of file + +// The build will automatically inject the following attributes: +// AssemblyVersion, AssemblyFileVersion, AssemblyInformationalVersion, AssemblyMetadata (for Branch, CommitId, and BuildDateUtc) + +[assembly: AssemblyMetadata("RepositoryUrl", "https://www.github.com/NuGet/NuGetGallery")] + diff --git a/src/NuGetGallery.Core/app.config b/src/NuGetGallery.Core/app.config index 6fe5098aca..37e26d2c48 100644 --- a/src/NuGetGallery.Core/app.config +++ b/src/NuGetGallery.Core/app.config @@ -16,19 +16,19 @@ - + - + - + - + diff --git a/src/NuGetGallery.Core/packages.config b/src/NuGetGallery.Core/packages.config index ba6e4194d7..11672f2cc7 100644 --- a/src/NuGetGallery.Core/packages.config +++ b/src/NuGetGallery.Core/packages.config @@ -1,20 +1,20 @@  + + - - - - - - - - - + + + + + + + - + \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Attributes/CommandAttribute.cs b/src/NuGetGallery.Operations/Attributes/CommandAttribute.cs deleted file mode 100644 index d46e0638ec..0000000000 --- a/src/NuGetGallery.Operations/Attributes/CommandAttribute.cs +++ /dev/null @@ -1,109 +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; - -namespace NuGetGallery.Operations -{ - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public sealed class CommandAttribute : Attribute - { - private string _description; - private string _usageSummary; - private string _usageDescription; - private string _example; - - public string CommandName { get; private set; } - public Type ResourceType { get; private set; } - public string DescriptionResourceName { get; private set; } - - - public string AltName { get; set; } - public int MinArgs { get; set; } - public int MaxArgs { get; set; } - public string UsageSummaryResourceName { get; set; } - public string UsageDescriptionResourceName { get; set; } - public string UsageExampleResourceName { get; set; } - public bool IsSpecialPurpose { get; set; } - - public string Description - { - get - { - if (ResourceType != null && !String.IsNullOrEmpty(DescriptionResourceName)) - { - return ResourceHelper.GetLocalizedString(ResourceType, DescriptionResourceName); - } - return _description; - } - private set - { - _description = value; - } - } - - public string UsageSummary - { - get - { - if (ResourceType != null && !String.IsNullOrEmpty(UsageSummaryResourceName)) - { - return ResourceHelper.GetLocalizedString(ResourceType, UsageSummaryResourceName); - } - return _usageSummary; - } - set - { - _usageSummary = value; - } - } - - public string UsageDescription - { - get - { - if (ResourceType != null && !String.IsNullOrEmpty(UsageDescriptionResourceName)) - { - return ResourceHelper.GetLocalizedString(ResourceType, UsageDescriptionResourceName); - } - return _usageDescription; - } - set - { - _usageDescription = value; - } - } - - public string UsageExample - { - get - { - if (ResourceType != null && !String.IsNullOrEmpty(UsageExampleResourceName)) - { - return ResourceHelper.GetLocalizedString(ResourceType, UsageExampleResourceName); - } - return _example; - } - set - { - _example = value; - } - } - - public CommandAttribute(string commandName, string description) - { - CommandName = commandName; - Description = description; - MinArgs = 0; - MaxArgs = Int32.MaxValue; - } - - public CommandAttribute(Type resourceType, string commandName, string descriptionResourceName) - { - ResourceType = resourceType; - CommandName = commandName; - DescriptionResourceName = descriptionResourceName; - MinArgs = 0; - MaxArgs = Int32.MaxValue; - } - } -} diff --git a/src/NuGetGallery.Operations/Attributes/OptionAttribute.cs b/src/NuGetGallery.Operations/Attributes/OptionAttribute.cs deleted file mode 100644 index 039784bfd0..0000000000 --- a/src/NuGetGallery.Operations/Attributes/OptionAttribute.cs +++ /dev/null @@ -1,45 +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; - -namespace NuGetGallery.Operations -{ - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public sealed class OptionAttribute : Attribute - { - private string _description; - - public string AltName { get; set; } - public string DescriptionResourceName { get; private set; } - - public string Description - { - get - { - if (ResourceType != null && !String.IsNullOrEmpty(DescriptionResourceName)) - { - return ResourceHelper.GetLocalizedString(ResourceType, DescriptionResourceName); - } - return _description; - - } - private set - { - _description = value; - } - } - - public Type ResourceType { get; private set; } - - public OptionAttribute(string description) - { - Description = description; - } - - public OptionAttribute(Type resourceType, string descriptionResourceName) - { - ResourceType = resourceType; - DescriptionResourceName = descriptionResourceName; - } - } -} diff --git a/src/NuGetGallery.Operations/CloudBlobExtensions.cs b/src/NuGetGallery.Operations/CloudBlobExtensions.cs deleted file mode 100644 index 0a3a32901b..0000000000 --- a/src/NuGetGallery.Operations/CloudBlobExtensions.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.IO; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; - -namespace NuGetGallery.Operations -{ - public static class CloudBlobExtensions - { - public static bool Exists(this ICloudBlob blob) - { - try - { - blob.FetchAttributes(); - return true; - } - catch (StorageException e) - { - if (e.RequestInformation.HttpStatusCode == 404) - return false; - - throw; - } - } - - public static void DownloadToFile(this ICloudBlob self, string fileName) - { - using (Stream strm = File.OpenWrite(fileName)) - { - self.DownloadToStream(strm); - } - } - - public static void UploadFile(this ICloudBlob self, string fileName) - { - using (Stream strm = File.OpenRead(fileName)) - { - self.UploadFromStream(strm); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Commands/HelpCommand.cs b/src/NuGetGallery.Operations/Commands/HelpCommand.cs deleted file mode 100644 index cb1c1b4806..0000000000 --- a/src/NuGetGallery.Operations/Commands/HelpCommand.cs +++ /dev/null @@ -1,225 +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.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Reflection; -using NuGet.Common; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Export(typeof(HelpCommand))] - [Command(typeof(CommandHelp), "help", "HelpCommandDescription", AltName = "?", MaxArgs = 1, - UsageSummaryResourceName = "HelpCommandUsageSummary", UsageDescriptionResourceName = "HelpCommandUsageDescription", - UsageExampleResourceName = "HelpCommandUsageExamples")] - [SuppressMessage("ReSharper", "LocalizableElement")] - public class HelpCommand : OpsTask - { - private readonly string _commandExe; - private readonly ICommandManager _commandManager; - private readonly string _helpUrl; - private readonly string _productName; - - private string CommandName - { - get - { - if (Arguments != null && Arguments.Count > 0) - { - return Arguments[0]; - } - return null; - } - } - - [Option(typeof(CommandHelp), "HelpCommandAll")] - public bool All { get; set; } - - [ImportingConstructor] - public HelpCommand(ICommandManager commandManager) - : this(commandManager, Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Name, CommandLineConstants.ReferencePage) - { - } - - [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "3#", - Justification = "We don't use the Url for anything besides printing, so it's ok to represent it as a string.")] - public HelpCommand(ICommandManager commandManager, string commandExe, string productName, string helpUrl) - { - _commandManager = commandManager; - _commandExe = commandExe; - _productName = productName; - _helpUrl = helpUrl; - } - - public override void ExecuteCommand() - { - if (!String.IsNullOrEmpty(CommandName)) - { - ViewHelpForCommand(CommandName); - } - else if (All) - { - ViewHelp(all: true); - } - else - { - ViewHelp(all: false); - } - } - - [SuppressMessage("ReSharper", "LocalizableElement")] - public void ViewHelp(bool all) - { - Console.WriteLine("{0} Version: {1}", _productName, GetType().Assembly.GetName().Version); - Console.WriteLine("usage: {0} [args] [options] ", _commandExe); - Console.WriteLine("Type '{0} help ' for help on a specific command.", _commandExe); - Console.WriteLine(); - Console.WriteLine("Available commands:"); - Console.WriteLine(); - - var commands = from c in _commandManager.GetCommands() - where all || !c.CommandAttribute.IsSpecialPurpose - orderby c.CommandAttribute.CommandName - select c.CommandAttribute; - - // Padding for printing - int maxWidth = commands.Max(c => c.CommandName.Length + GetAltText(c.AltName).Length); - - foreach (var command in commands) - { - PrintCommand(maxWidth, command); - } - - if (_helpUrl != null) - { - Console.WriteLine(); - Console.WriteLine("For more information, visit {0}", _helpUrl); - } - } - - private static void PrintCommand(int maxWidth, CommandAttribute commandAttribute) - { - // Write out the command name left justified with the max command's width's padding - Console.Write(" {0, -" + maxWidth + "} ", GetCommandText(commandAttribute)); - // Starting index of the description - int descriptionPadding = maxWidth + 4; - PrintJustified(descriptionPadding, commandAttribute.Description); - } - - private static string GetCommandText(CommandAttribute commandAttribute) - { - return commandAttribute.CommandName + GetAltText(commandAttribute.AltName); - } - - public void ViewHelpForCommand(string commandName) - { - ICommand command = _commandManager.GetCommand(commandName); - CommandAttribute attribute = command.CommandAttribute; - - Console.WriteLine("usage: {0} {1} {2}", _commandExe, attribute.CommandName, attribute.UsageSummary); - Console.WriteLine(); - - if (!String.IsNullOrEmpty(attribute.AltName)) - { - Console.WriteLine("alias: {0}", attribute.AltName); - Console.WriteLine(); - } - - Console.WriteLine(attribute.Description); - Console.WriteLine(); - - if (attribute.UsageDescription != null) - { - const int padding = 5; - PrintJustified(padding, attribute.UsageDescription); - Console.WriteLine(); - } - - var options = _commandManager.GetCommandOptions(command); - - if (options.Count > 0) - { - Console.WriteLine("options:"); - Console.WriteLine(); - - // Get the max option width. +2 for showing + against multivalued properties - int maxOptionWidth = options.Max(o => o.Value.Name.Length) + 2; - // Get the max altname option width - int maxAltOptionWidth = options.Max(o => (o.Key.AltName ?? String.Empty).Length); - - foreach (var o in options) - { - Console.Write(" -{0, -" + (maxOptionWidth + 2) + "}", o.Value.Name + - (TypeHelper.IsMultiValuedProperty(o.Value) ? " +" : String.Empty)); - Console.Write(" {0, -" + (maxAltOptionWidth + 4) + "}", GetAltText(o.Key.AltName)); - - PrintJustified((10 + maxAltOptionWidth + maxOptionWidth), o.Key.Description); - - } - - if (_helpUrl != null) - { - Console.WriteLine(); - Console.WriteLine("For more information, visit {0}", _helpUrl); - } - - Console.WriteLine(); - } - } - - private void ViewHelpForAllCommands() - { - var commands = from c in _commandManager.GetCommands() - orderby c.CommandAttribute.CommandName - select c.CommandAttribute; - TextInfo info = CultureInfo.CurrentCulture.TextInfo; - - foreach (var command in commands) - { - Console.WriteLine(info.ToTitleCase(command.CommandName) + " Command"); - ViewHelpForCommand(command.CommandName); - } - } - - private static string GetAltText(string altNameText) - { - if (String.IsNullOrEmpty(altNameText)) - { - return String.Empty; - } - return String.Format(CultureInfo.CurrentCulture, " ({0})", altNameText); - } - - - private static void PrintJustified(int startIndex, string text) - { - PrintJustified(startIndex, text, Console.WindowWidth); - } - - private static void PrintJustified(int startIndex, string text, int maxWidth) - { - if (maxWidth > startIndex) - { - maxWidth = maxWidth - startIndex - 1; - } - - while (text.Length > 0) - { - // Trim whitespace at the beginning - text = text.TrimStart(); - // Calculate the number of chars to print based on the width of the System.Console - int length = Math.Min(text.Length, maxWidth); - // Text we can print without overflowing the System.Console. - string content = text.Substring(0, length); - int leftPadding = startIndex + length - Console.CursorLeft; - // Print it with the correct padding - Console.WriteLine(content.PadLeft(leftPadding)); - // Get the next substring to be printed - text = text.Substring(content.Length); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Common/ArgCheck.cs b/src/NuGetGallery.Operations/Common/ArgCheck.cs deleted file mode 100644 index d21b537810..0000000000 --- a/src/NuGetGallery.Operations/Common/ArgCheck.cs +++ /dev/null @@ -1,51 +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; - -namespace NuGetGallery.Operations.Common -{ - public static class ArgCheck - { - public static void RequiredOrConfig(object value, string name) - { - if (value == null) - { - throw CreateRequiredOrConfigEx(name); - } - } - - public static void RequiredOrConfig(string value, string name) - { - if (String.IsNullOrWhiteSpace(value)) - { - throw CreateRequiredOrConfigEx(name); - } - } - - public static void Required(object value, string name) - { - if (value == null) - { - throw CreateRequiredEx(name); - } - } - - public static void Required(string value, string name) - { - if (String.IsNullOrWhiteSpace(value)) - { - throw CreateRequiredEx(name); - } - } - - private static CommandLineException CreateRequiredEx(string name) - { - return new CommandLineException(String.Format(CommandHelp.Option_Required, name)); - } - - private static CommandLineException CreateRequiredOrConfigEx(string name) - { - return new CommandLineException(String.Format(CommandHelp.Option_RequiredOrConfig, name)); - } - } -} diff --git a/src/NuGetGallery.Operations/Common/CloudStorageAccountConverter.cs b/src/NuGetGallery.Operations/Common/CloudStorageAccountConverter.cs deleted file mode 100644 index ab68996dc8..0000000000 --- a/src/NuGetGallery.Operations/Common/CloudStorageAccountConverter.cs +++ /dev/null @@ -1,30 +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.ComponentModel; -using Microsoft.WindowsAzure.Storage; - -namespace NuGetGallery.Operations.Common -{ - public class CloudStorageAccountConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) - { - string s = value as string; - if (s != null) - { - CloudStorageAccount acct; - if (CloudStorageAccount.TryParse(s, out acct)) - { - return acct; - } - } - return null; - } - } -} diff --git a/src/NuGetGallery.Operations/Common/CommandHelp.Designer.cs b/src/NuGetGallery.Operations/Common/CommandHelp.Designer.cs deleted file mode 100644 index a2761777d4..0000000000 --- a/src/NuGetGallery.Operations/Common/CommandHelp.Designer.cs +++ /dev/null @@ -1,159 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace NuGetGallery.Operations.Common { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class CommandHelp { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal CommandHelp() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NuGetGallery.Operations.Common.CommandHelp", typeof(CommandHelp).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Show all available commands, instead of hiding special purpose commands. - /// - internal static string HelpCommandAll { - get { - return ResourceManager.GetString("HelpCommandAll", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Displays general help information and help information about other commands.. - /// - internal static string HelpCommandDescription { - get { - return ResourceManager.GetString("HelpCommandDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Pass a command name to display help information for that command.. - /// - internal static string HelpCommandUsageDescription { - get { - return ResourceManager.GetString("HelpCommandUsageDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to nuget help - /// - ///nuget help push - /// - ///nuget ? - /// - ///nuget push -?. - /// - internal static string HelpCommandUsageExamples { - get { - return ResourceManager.GetString("HelpCommandUsageExamples", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [command]. - /// - internal static string HelpCommandUsageSummary { - get { - return ResourceManager.GetString("HelpCommandUsageSummary", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Do not prompt for user input or confirmations.. - /// - internal static string Option_NonInteractive { - get { - return ResourceManager.GetString("Option_NonInteractive", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The '{0}' option is required and must be specified on the command line.. - /// - internal static string Option_Required { - get { - return ResourceManager.GetString("Option_Required", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The '{0}' option is required. It must be specified either on the command line or you must run galops from inside an environment in the NuGet Operations Console. - /// - internal static string Option_RequiredOrConfig { - get { - return ResourceManager.GetString("Option_RequiredOrConfig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The '{0}' option is required. It must be specified either on the command line or in the '{1}' environment variable.. - /// - internal static string Option_RequiredOrEnv { - get { - return ResourceManager.GetString("Option_RequiredOrEnv", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Display this amount of details in the output: normal, quiet, detailed.. - /// - internal static string Option_Verbosity { - get { - return ResourceManager.GetString("Option_Verbosity", resourceCulture); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Common/CommandHelp.resx b/src/NuGetGallery.Operations/Common/CommandHelp.resx deleted file mode 100644 index 7360eba995..0000000000 --- a/src/NuGetGallery.Operations/Common/CommandHelp.resx +++ /dev/null @@ -1,156 +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 - - - Show all available commands, instead of hiding special purpose commands - - - Displays general help information and help information about other commands. - - - Pass a command name to display help information for that command. - - - nuget help - -nuget help push - -nuget ? - -nuget push -? - - - [command] - - - Do not prompt for user input or confirmations. - - - The '{0}' option is required and must be specified on the command line. - - - The '{0}' option is required. It must be specified either on the command line or you must run galops from inside an environment in the NuGet Operations Console - - - The '{0}' option is required. It must be specified either on the command line or in the '{1}' environment variable. - - - Display this amount of details in the output: normal, quiet, detailed. - - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Common/CommandLineConstants.cs b/src/NuGetGallery.Operations/Common/CommandLineConstants.cs deleted file mode 100644 index 496cce293d..0000000000 --- a/src/NuGetGallery.Operations/Common/CommandLineConstants.cs +++ /dev/null @@ -1,9 +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. -namespace NuGet.Common -{ - internal static class CommandLineConstants - { - internal static string ReferencePage = "https://github.com/NuGet/NuGetOperations"; - } -} diff --git a/src/NuGetGallery.Operations/Common/CommandLineException.cs b/src/NuGetGallery.Operations/Common/CommandLineException.cs deleted file mode 100644 index 82d8083d53..0000000000 --- a/src/NuGetGallery.Operations/Common/CommandLineException.cs +++ /dev/null @@ -1,41 +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.Globalization; -using System.Runtime.Serialization; - -namespace NuGetGallery.Operations -{ - [Serializable] - public class CommandLineException : Exception - { - public CommandLineException() - { - } - - public CommandLineException(string message) - : base(message) - { - } - - public CommandLineException(string format, params object[] args) - : base(String.Format(CultureInfo.CurrentCulture, format, args)) - { - } - - public CommandLineException(Exception innerException, string format, params object[] args) - : base(String.Format(CultureInfo.CurrentCulture, format, args), innerException) - { - } - - public CommandLineException(string message, Exception innerException) - : base(message, innerException) - { - } - - protected CommandLineException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} diff --git a/src/NuGetGallery.Operations/Common/CommonResources.Designer.cs b/src/NuGetGallery.Operations/Common/CommonResources.Designer.cs deleted file mode 100644 index 978e15fe53..0000000000 --- a/src/NuGetGallery.Operations/Common/CommonResources.Designer.cs +++ /dev/null @@ -1,135 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace NuGetGallery.Operations.Common { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class CommonResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal CommonResources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NuGetGallery.Operations.Common.CommonResources", typeof(CommonResources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Value cannot be null or an empty string.. - /// - internal static string Argument_Cannot_Be_Null_Or_Empty { - get { - return ResourceManager.GetString("Argument_Cannot_Be_Null_Or_Empty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value must be between {0} and {1}.. - /// - internal static string Argument_Must_Be_Between { - get { - return ResourceManager.GetString("Argument_Must_Be_Between", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value must be a value from the "{0}" enumeration.. - /// - internal static string Argument_Must_Be_Enum_Member { - get { - return ResourceManager.GetString("Argument_Must_Be_Enum_Member", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value must be greater than {0}.. - /// - internal static string Argument_Must_Be_GreaterThan { - get { - return ResourceManager.GetString("Argument_Must_Be_GreaterThan", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value must be greater than or equal to {0}.. - /// - internal static string Argument_Must_Be_GreaterThanOrEqualTo { - get { - return ResourceManager.GetString("Argument_Must_Be_GreaterThanOrEqualTo", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value must be less than {0}.. - /// - internal static string Argument_Must_Be_LessThan { - get { - return ResourceManager.GetString("Argument_Must_Be_LessThan", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value must be less than or equal to {0}.. - /// - internal static string Argument_Must_Be_LessThanOrEqualTo { - get { - return ResourceManager.GetString("Argument_Must_Be_LessThanOrEqualTo", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value cannot be an empty string. It must either be null or a non-empty string.. - /// - internal static string Argument_Must_Be_Null_Or_Non_Empty { - get { - return ResourceManager.GetString("Argument_Must_Be_Null_Or_Non_Empty", resourceCulture); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Common/CommonResources.resx b/src/NuGetGallery.Operations/Common/CommonResources.resx deleted file mode 100644 index 592308e425..0000000000 --- a/src/NuGetGallery.Operations/Common/CommonResources.resx +++ /dev/null @@ -1,144 +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 - - - Value cannot be null or an empty string. - - - Value must be between {0} and {1}. - - - Value must be a value from the "{0}" enumeration. - - - Value must be greater than {0}. - - - Value must be greater than or equal to {0}. - - - Value must be less than {0}. - - - Value must be less than or equal to {0}. - - - Value cannot be an empty string. It must either be null or a non-empty string. - - diff --git a/src/NuGetGallery.Operations/Common/ReportHelpers.cs b/src/NuGetGallery.Operations/Common/ReportHelpers.cs deleted file mode 100644 index 17e6d8c73e..0000000000 --- a/src/NuGetGallery.Operations/Common/ReportHelpers.cs +++ /dev/null @@ -1,45 +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 Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.IO; - -namespace NuGetGallery.Operations.Common -{ - static class ReportHelpers - { - public static Stream ToStream(JToken jToken) - { - MemoryStream stream = new MemoryStream(); - TextWriter writer = new StreamWriter(stream); - writer.Write(jToken.ToString()); - writer.Flush(); - stream.Seek(0, SeekOrigin.Begin); - return stream; - } - - public static Stream ToJson(Tuple> report) - { - JArray jArray = new JArray(); - - foreach (object[] row in report.Item2) - { - JObject jObject = new JObject(); - - for (int i = 0; i < report.Item1.Length; i++) - { - if (row[i] != null) - { - jObject.Add(report.Item1[i], new JValue(row[i])); - } - // ELSE treat null by not defining the property in our internal JSON (aka undefined) - } - - jArray.Add(jObject); - } - - return ToStream(jArray); - } - } -} diff --git a/src/NuGetGallery.Operations/Common/ResourceHelper.cs b/src/NuGetGallery.Operations/Common/ResourceHelper.cs deleted file mode 100644 index 73f9d15480..0000000000 --- a/src/NuGetGallery.Operations/Common/ResourceHelper.cs +++ /dev/null @@ -1,118 +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 NuGetGallery.Operations.Common; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Resources; -using System.Text; - -namespace NuGetGallery.Operations -{ - public static class ResourceHelper - { - private static Dictionary _cachedManagers; - - public static string GetLocalizedString(Type resourceType, string resourceNames) - { - if (String.IsNullOrEmpty(resourceNames)) - { - throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, nameof(resourceNames)); - } - - if (resourceType == null) - { - throw new ArgumentNullException(nameof(resourceType)); - } - - if (_cachedManagers == null) - { - _cachedManagers = new Dictionary(); - } - - ResourceManager resourceManager; - if (!_cachedManagers.TryGetValue(resourceType, out resourceManager)) - { - PropertyInfo property = resourceType.GetProperty("ResourceManager", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); - - if (property == null || property.GetGetMethod(nonPublic: true) == null) - { - throw new InvalidOperationException( - String.Format(CultureInfo.CurrentCulture, TaskResources.ResourceTypeDoesNotHaveProperty, resourceType, "ResourceManager")); - } - - if (property.PropertyType != typeof(ResourceManager)) - { - throw new InvalidOperationException( - String.Format(CultureInfo.CurrentCulture, TaskResources.ResourcePropertyIncorrectType, resourceNames, resourceType)); - } - - resourceManager = (ResourceManager)property.GetGetMethod(nonPublic: true) - .Invoke(obj: null, parameters: null); - } - - var builder = new StringBuilder(); - foreach (var resource in resourceNames.Split(';')) - { - string value = resourceManager.GetString(resource); - if (String.IsNullOrEmpty(value)) - { - throw new InvalidOperationException( - String.Format(CultureInfo.CurrentCulture, TaskResources.ResourceTypeDoesNotHaveProperty, resourceType, resource)); - } - if (builder.Length > 0) - { - builder.AppendLine(); - } - builder.Append(value); - } - - return builder.ToString(); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] - public static string GetBatchFromSqlFile(string filename) - { - using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename)) - { - using (var reader = new StreamReader(stream)) - { - return reader.ReadToEnd(); - } - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] - public static IEnumerable GetBatchesFromSqlFile(string filename) - { - List batches = new List(); - - using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename)) - { - using (var reader = new StreamReader(stream)) - { - StringBuilder batch = new StringBuilder(); - - while (!reader.EndOfStream) - { - string line = reader.ReadLine(); - - if (line.Trim().Equals("GO", StringComparison.OrdinalIgnoreCase)) - { - batches.Add(batch.ToString()); - batch.Clear(); - } - else - { - batch.AppendLine(line); - } - } - } - } - - return batches; - } - } -} diff --git a/src/NuGetGallery.Operations/Common/SqlConnectionStringConverter.cs b/src/NuGetGallery.Operations/Common/SqlConnectionStringConverter.cs deleted file mode 100644 index 854cd08f6d..0000000000 --- a/src/NuGetGallery.Operations/Common/SqlConnectionStringConverter.cs +++ /dev/null @@ -1,27 +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.ComponentModel; -using System.Data.SqlClient; -using System.Globalization; - -namespace NuGetGallery.Operations.Common -{ - class SqlConnectionStringConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - string s = value as string; - if (s != null) - { - return new SqlConnectionStringBuilder(s); - } - return null; - } - } -} diff --git a/src/NuGetGallery.Operations/Common/SqlHelper.cs b/src/NuGetGallery.Operations/Common/SqlHelper.cs deleted file mode 100644 index 0c942cadef..0000000000 --- a/src/NuGetGallery.Operations/Common/SqlHelper.cs +++ /dev/null @@ -1,29 +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.Data.SqlClient; - -namespace NuGetGallery.Operations.Common -{ - static class SqlHelper - { - public static void ExecuteBatch(string connectionString, string sql, int timeout = 180) - { - try - { - using (SqlConnection conn = new SqlConnection(connectionString)) - { - conn.Open(); - - SqlCommand command = new SqlCommand(sql, conn); - command.CommandTimeout = timeout; - command.ExecuteNonQuery(); - } - } - catch (Exception e) - { - throw new ApplicationException($"{e.Message}\n{sql}", e); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Common/StreamConverter.cs b/src/NuGetGallery.Operations/Common/StreamConverter.cs deleted file mode 100644 index 02bbee58eb..0000000000 --- a/src/NuGetGallery.Operations/Common/StreamConverter.cs +++ /dev/null @@ -1,26 +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.ComponentModel; -using System.IO; - -namespace NuGetGallery.Operations.Common -{ - public class FileStreamConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) - { - string s = value as string; - if (s != null) - { - return File.Open(s, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); - } - return null; - } - } -} diff --git a/src/NuGetGallery.Operations/Common/TaskResources.Designer.cs b/src/NuGetGallery.Operations/Common/TaskResources.Designer.cs deleted file mode 100644 index 090fe80dee..0000000000 --- a/src/NuGetGallery.Operations/Common/TaskResources.Designer.cs +++ /dev/null @@ -1,162 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace NuGetGallery.Operations.Common { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class TaskResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal TaskResources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NuGetGallery.Operations.Common.TaskResources", typeof(TaskResources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Ambiguous command '{0}'. Possible values: {1}.. - /// - internal static string AmbiguousCommand { - get { - return ResourceManager.GetString("AmbiguousCommand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ambiguous option '{0}'. Possible values: {1}.. - /// - internal static string AmbiguousOption { - get { - return ResourceManager.GetString("AmbiguousOption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No description was provided for this command.. - /// - internal static string DefaultCommandDescription { - get { - return ResourceManager.GetString("DefaultCommandDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid option value: '{0} {1}': {2}. - /// - internal static string InvalidOptionValueError { - get { - return ResourceManager.GetString("InvalidOptionValueError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Missing option value for: '{0}'. - /// - internal static string MissingOptionValueError { - get { - return ResourceManager.GetString("MissingOptionValueError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [option] on '{0}' is invalid without a setter.. - /// - internal static string OptionInvalidWithoutSetter { - get { - return ResourceManager.GetString("OptionInvalidWithoutSetter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The property '{0}' on resource type '{1}' is not a type of ResourceManager.. - /// - internal static string ResourcePropertyIncorrectType { - get { - return ResourceManager.GetString("ResourcePropertyIncorrectType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The resource type '{0}' does not have an accessible static property named '{1}'.. - /// - internal static string ResourceTypeDoesNotHaveProperty { - get { - return ResourceManager.GetString("ResourceTypeDoesNotHaveProperty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unable to change from type '{0}' to '{1}'.. - /// - internal static string UnableToConvertTypeError { - get { - return ResourceManager.GetString("UnableToConvertTypeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unknown command: '{0}'. - /// - internal static string UnknownCommandError { - get { - return ResourceManager.GetString("UnknownCommandError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unknown option: '{0}'. - /// - internal static string UnknownOptionError { - get { - return ResourceManager.GetString("UnknownOptionError", resourceCulture); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Common/TaskResources.resx b/src/NuGetGallery.Operations/Common/TaskResources.resx deleted file mode 100644 index 0a8a5e0135..0000000000 --- a/src/NuGetGallery.Operations/Common/TaskResources.resx +++ /dev/null @@ -1,153 +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 - - - Ambiguous command '{0}'. Possible values: {1}. - - - Ambiguous option '{0}'. Possible values: {1}. - - - No description was provided for this command. - - - Invalid option value: '{0} {1}': {2} - - - Missing option value for: '{0}' - - - [option] on '{0}' is invalid without a setter. - - - The property '{0}' on resource type '{1}' is not a type of ResourceManager. - - - The resource type '{0}' does not have an accessible static property named '{1}'. - - - Unable to change from type '{0}' to '{1}'. - - - Unknown command: '{0}' - - - Unknown option: '{0}' - - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Common/TypeHelper.cs b/src/NuGetGallery.Operations/Common/TypeHelper.cs deleted file mode 100644 index 850018fb5b..0000000000 --- a/src/NuGetGallery.Operations/Common/TypeHelper.cs +++ /dev/null @@ -1,129 +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 System.ComponentModel; -using System.Data.SqlClient; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using Microsoft.WindowsAzure.Storage; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - internal static class TypeHelper - { - private static Dictionary> _additionalConverters = new Dictionary>() { - { typeof(Stream), () => new FileStreamConverter() }, - {typeof(CloudStorageAccount), () => new CloudStorageAccountConverter()}, - {typeof(SqlConnectionStringBuilder), () => new SqlConnectionStringConverter()}, - }; - - public static Type RemoveNullableFromType(Type type) - { - return Nullable.GetUnderlyingType(type) ?? type; - } - - public static object ChangeType(object value, Type type) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - if (value == null) - { - if (TypeAllowsNull(type)) - { - return null; - } - - return Convert.ChangeType(value, type, CultureInfo.CurrentCulture); - } - - type = RemoveNullableFromType(type); - - if (value.GetType() == type) - { - return value; - } - - TypeConverter converter = TypeDescriptor.GetConverter(type); - if (converter.CanConvertFrom(value.GetType())) - { - return converter.ConvertFrom(value); - } - - converter = TypeDescriptor.GetConverter(value.GetType()); - if (converter.CanConvertTo(type)) - { - return converter.ConvertTo(value, type); - } - - Func ctor; - if (_additionalConverters.TryGetValue(type, out ctor)) - { - converter = ctor(); - if (converter.CanConvertFrom(value.GetType())) - { - return converter.ConvertFrom(value); - } - } - - if (_additionalConverters.TryGetValue(value.GetType(), out ctor)) - { - converter = ctor(); - if (converter.CanConvertTo(type)) - { - return converter.ConvertTo(value, type); - } - } - - throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, - TaskResources.UnableToConvertTypeError, value.GetType(), type)); - } - - public static bool TypeAllowsNull(Type type) - { - return Nullable.GetUnderlyingType(type) != null || !type.IsValueType; - } - - public static Type GetGenericCollectionType(Type type) - { - return GetInterfaceType(type, typeof(ICollection<>)); - } - - public static Type GetDictionaryType(Type type) - { - return GetInterfaceType(type, typeof(IDictionary<,>)); - } - - private static Type GetInterfaceType(Type type, Type interfaceType) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == interfaceType) - { - return type; - } - return (from t in type.GetInterfaces() - where t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType - select t).SingleOrDefault(); - } - - public static bool IsKeyValueProperty(PropertyInfo property) - { - return GetDictionaryType(property.PropertyType) != null; - } - - public static bool IsMultiValuedProperty(PropertyInfo property) - { - return GetGenericCollectionType(property.PropertyType) != null || IsKeyValueProperty(property); - } - - public static bool IsEnumProperty(PropertyInfo property) - { - return property.PropertyType.IsEnum; - } - } -} diff --git a/src/NuGetGallery.Operations/DatabaseBackupHelper.cs b/src/NuGetGallery.Operations/DatabaseBackupHelper.cs deleted file mode 100644 index 6dd037ef8e..0000000000 --- a/src/NuGetGallery.Operations/DatabaseBackupHelper.cs +++ /dev/null @@ -1,34 +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 System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations -{ - public static class DatabaseBackupHelper - { - public static bool GetBackupStatus(NLog.Logger log, SqlConnectionStringBuilder connectionString, string backupName) - { - CheckDatabaseStatusTask checkDatabaseStatusTask = new CheckDatabaseStatusTask - { - ConnectionString = connectionString, - BackupName = backupName, - WhatIf = false // WhatIf isn't used by this task. - }; - - checkDatabaseStatusTask.Execute(); - - if (checkDatabaseStatusTask.State == 0) - { - log.Info("Copy of {0} to {1} complete!", connectionString.InitialCatalog, backupName); - } - - return checkDatabaseStatusTask.State == 0; - } - } -} diff --git a/src/NuGetGallery.Operations/DeploymentEnvironment.cs b/src/NuGetGallery.Operations/DeploymentEnvironment.cs deleted file mode 100644 index 88b027912b..0000000000 --- a/src/NuGetGallery.Operations/DeploymentEnvironment.cs +++ /dev/null @@ -1,136 +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 System.Data.SqlClient; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using System.Xml.Linq; -using Microsoft.WindowsAzure.Storage; -using NLog; - -namespace NuGetGallery.Operations -{ - public class DeploymentEnvironment - { - public static readonly Logger Log = LogManager.GetLogger("DeploymentEnvironment"); - - public IDictionary Settings { get; private set; } - - public string EnvironmentName { get; private set; } - public string SubscriptionId { get; private set; } - public string SubscriptionName { get; private set; } - - public SqlConnectionStringBuilder MainDatabase { get; private set; } - - public SqlConnectionStringBuilder WarehouseDatabase { get; private set; } - - public CloudStorageAccount MainStorage { get; private set; } - - public CloudStorageAccount BackupStorage { get; private set; } - - public CloudStorageAccount DiagnosticsStorage { get; private set; } - - public Uri SqlDacEndpoint { get; private set; } - - public Uri LicenseReportService { get; private set; } - - public NetworkCredential LicenseReportServiceCredentials { get; private set; } - - public DeploymentEnvironment(string environmentName, string subscriptionId, string subscriptionName, IDictionary deploymentSettings) - { - Settings = deploymentSettings; - - EnvironmentName = environmentName; - SubscriptionId = subscriptionId; - SubscriptionName = subscriptionName; - - MainDatabase = GetSqlConnectionStringBuilder("Operations.Sql.Primary"); - WarehouseDatabase = GetSqlConnectionStringBuilder("Operations.Sql.Warehouse"); - - MainStorage = GetCloudStorageAccount("Operations.Storage.Primary"); - BackupStorage = GetCloudStorageAccount("Operations.Storage.Backup") ?? MainStorage; - DiagnosticsStorage = GetCloudStorageAccount("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString") ?? MainStorage; - - SqlDacEndpoint = Get("Operations.SqlDac", str => new Uri(str, UriKind.Absolute)); - - LicenseReportService = Get("Operations.LicenseReport.Service", str => new Uri(str, UriKind.Absolute)); - - string licenseReportUser = Get("Operations.LicenseReport.User"); - string licenseReportPassword = Get("Operations.LicenseReport.Password"); - if (!String.IsNullOrEmpty(licenseReportUser) && !String.IsNullOrEmpty(licenseReportPassword)) - { - LicenseReportServiceCredentials = new NetworkCredential(licenseReportUser, licenseReportPassword); - } - } - - private string Get(string key) - { - string value; - if (!Settings.TryGetValue(key, out value)) - { - return null; - } - return value; - } - - private T Get(string key, Func thunk) - { - string val = Get(key); - return String.IsNullOrEmpty(val) ? default(T) : thunk(val); - } - - private CloudStorageAccount GetCloudStorageAccount(string key) - { - return Get(key, str => CloudStorageAccount.Parse(str)); - } - - private SqlConnectionStringBuilder GetSqlConnectionStringBuilder(string key) - { - return Get(key, str => new SqlConnectionStringBuilder(str)); - } - - private static IDictionary BuildSettingsDictionary(XDocument doc) - { - XNamespace ns = XNamespace.Get("http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration"); - return (from s in doc.Element(ns + "ServiceConfiguration") - .Element(ns + "Role") - .Element(ns + "ConfigurationSettings") - .Elements(ns + "Setting") - select new KeyValuePair( - s.Attribute("name").Value, - s.Attribute("value").Value)) - .ToDictionary(p => p.Key, p => p.Value); - } - - public static DeploymentEnvironment FromEnvironment() - { - string serviceConfig = Environment.GetEnvironmentVariable("NUGET_SERVICE_CONFIG"); - IDictionary settings = null; - if (!String.IsNullOrEmpty(serviceConfig) && File.Exists(serviceConfig)) - { - try - { - // Load the file - var doc = XDocument.Load(serviceConfig); - - // Build a dictionary of settings - settings = BuildSettingsDictionary(doc); - } - catch(Exception ex) - { - Log.ErrorException("Unable to load service config: " + serviceConfig, ex); - } - } - - return new DeploymentEnvironment( - Environment.GetEnvironmentVariable("NUCMD_ENVIRONMENT_NAME"), - Environment.GetEnvironmentVariable("NUCMD_SUBSCRIPTION_ID"), - Environment.GetEnvironmentVariable("NUCMD_SUBSCRIPTION_NAME"), - settings ?? new Dictionary()); - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/ExtensionMethods.cs b/src/NuGetGallery.Operations/ExtensionMethods.cs deleted file mode 100644 index b1161bca68..0000000000 --- a/src/NuGetGallery.Operations/ExtensionMethods.cs +++ /dev/null @@ -1,55 +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 System.Linq; -using System.Runtime.Versioning; -using NuGet; -using NuGet.Frameworks; - -namespace NuGetGallery.Operations -{ - public static class ExtensionMethods - { - public static void AddRange(this ICollection self, IEnumerable items) - { - foreach (var item in items) - { - self.Add(item); - } - } - - public static bool AnySafe(this IEnumerable items, Func predicate) - { - if (items == null) - { - return false; - } - return items.Any(predicate); - } - - public static string ToShortNameOrNull(this NuGetFramework frameworkName) - { - if (frameworkName == null) - { - return null; - } - - var shortFolderName = frameworkName.GetShortFolderName(); - - // If the shortFolderName is "any", we want to return null to preserve NuGet.Core - // compatibility in the V2 feed. - if (String.Equals(shortFolderName, "any", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - return shortFolderName; - } - - public static string ToFriendlyDateTimeString(this DateTime self) - { - return self.ToString("yyyy-MM-dd h:mm tt"); - } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/CommandLineParser.cs b/src/NuGetGallery.Operations/Infrastructure/CommandLineParser.cs deleted file mode 100644 index c9761f292a..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/CommandLineParser.cs +++ /dev/null @@ -1,196 +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 System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Reflection; -using NuGetGallery.Operations; -using NuGetGallery.Operations.Common; - -namespace NuGet -{ - public class CommandLineParser - { - private readonly ICommandManager _commandManager; - - // On Unix or MacOSX slash as a switch indicator would interfere with the path separator - [SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate")] - private static readonly bool _supportSlashAsSwitch = (Environment.OSVersion.Platform != PlatformID.Unix) && (Environment.OSVersion.Platform != PlatformID.MacOSX); - - public CommandLineParser(ICommandManager manager) - { - _commandManager = manager; - } - - public void ExtractOptions(ICommand command, IEnumerator argsEnumerator) - { - List arguments = new List(); - IDictionary properties = _commandManager.GetCommandOptions(command); - - while (true) - { - string option = GetNextCommandLineItem(argsEnumerator); - - if (option == null) - { - break; - } - - if (!(option.StartsWith("/", StringComparison.OrdinalIgnoreCase) && _supportSlashAsSwitch) - && !option.StartsWith("-", StringComparison.OrdinalIgnoreCase)) - { - arguments.Add(option); - continue; - } - - string optionText = option.Substring(1); - string value = null; - - if (optionText.EndsWith("-", StringComparison.OrdinalIgnoreCase)) - { - optionText = optionText.TrimEnd('-'); - value = "false"; - } - - var result = GetPartialOptionMatch(properties, prop => prop.Value.Name, prop => prop.Key.AltName, option, optionText); - PropertyInfo propInfo = result.Value; - - if (propInfo.PropertyType == typeof(bool)) - { - value = value ?? "true"; - } - else - { - value = GetNextCommandLineItem(argsEnumerator); - } - - if (value == null) - { - throw new CommandLineException(TaskResources.MissingOptionValueError, option); - } - - AssignValue(command, propInfo, option, value); - } - command.Arguments.AddRange(arguments); - } - - internal static void AssignValue(object command, PropertyInfo property, string option, object value) - { - try - { - if (TypeHelper.IsMultiValuedProperty(property)) - { - // If we were able to look up a parent of type ICollection<>, perform a Add operation on it. - // Note that we expect the value is a string. - var stringValue = value as string; - Debug.Assert(stringValue != null); - - dynamic list = property.GetValue(command, null); - // The parameter value is one or more semi-colon separated items that might support values also - // Example of a list value : nuget pack -option "foo;bar;baz" - // Example of a keyvalue value: nuget pack -option "foo=bar;baz=false" - foreach (var item in stringValue.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) - { - if (TypeHelper.IsKeyValueProperty(property)) - { - int eqIndex = item.IndexOf("=", StringComparison.OrdinalIgnoreCase); - if (eqIndex > -1) - { - string propertyKey = item.Substring(0, eqIndex); - string propertyValue = item.Substring(eqIndex + 1); - list.Add(propertyKey, propertyValue); - } - } - else - { - list.Add(item); - } - } - } - else if (TypeHelper.IsEnumProperty(property)) - { - var enumValue = Enum.GetValues(property.PropertyType).Cast(); - value = GetPartialOptionMatch(enumValue, e => e.ToString(), e => e.ToString(), option, value.ToString()); - property.SetValue(command, value, index: null); - } - else - { - property.SetValue(command, TypeHelper.ChangeType(value, property.PropertyType), index: null); - } - } - catch (CommandLineException) - { - throw; - } - catch(Exception ex) - { - throw new CommandLineException(ex, TaskResources.InvalidOptionValueError, option, value, ex.Message); - } - } - - public ICommand ParseCommandLine(IEnumerable commandLineArgs) - { - IEnumerator argsEnumerator = commandLineArgs.GetEnumerator(); - - // Get the desired command name - string cmdName = GetNextCommandLineItem(argsEnumerator); - if (cmdName == null) - { - return null; - } - - // Get the command based on the name - ICommand cmd = _commandManager.GetCommand(cmdName); - if (cmd == null) - { - throw new CommandLineException(TaskResources.UnknownCommandError, cmdName); - } - - ExtractOptions(cmd, argsEnumerator); - return cmd; - } - - public static string GetNextCommandLineItem(IEnumerator argsEnumerator) - { - if (argsEnumerator == null || !argsEnumerator.MoveNext()) - { - return null; - } - return argsEnumerator.Current; - } - - private static TVal GetPartialOptionMatch(IEnumerable source, Func getDisplayName, Func getAltName, string option, string value) - { - var results = from item in source - where getDisplayName(item).StartsWith(value, StringComparison.OrdinalIgnoreCase) || - (getAltName(item) ?? String.Empty).StartsWith(value, StringComparison.OrdinalIgnoreCase) - select item; - - if (!results.Any()) - { - throw new CommandLineException(TaskResources.UnknownOptionError, option); - } - - var result = results.FirstOrDefault(); - if (results.Skip(1).Any()) - { - try - { - // When multiple results are found, if there's an exact match, return it. - result = results.First(c => value.Equals(getDisplayName(c), StringComparison.OrdinalIgnoreCase) || - value.Equals(getAltName(c), StringComparison.OrdinalIgnoreCase)); - } - catch (InvalidOperationException) - { - throw new CommandLineException(String.Format(CultureInfo.CurrentCulture, TaskResources.AmbiguousOption, value, - String.Join(" ", from c in results select getDisplayName(c)))); - } - } - - return result; - } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/CommandManager.cs b/src/NuGetGallery.Operations/Infrastructure/CommandManager.cs deleted file mode 100644 index 640262a623..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/CommandManager.cs +++ /dev/null @@ -1,88 +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 System.ComponentModel.Composition; -using System.Globalization; -using System.Linq; -using System.Reflection; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Export(typeof(ICommandManager))] - public class CommandManager : ICommandManager - { - private readonly IList _commands = new List(); - - public IEnumerable GetCommands() - { - return _commands; - } - - public ICommand GetCommand(string commandName) - { - if (String.IsNullOrEmpty(commandName)) - { - throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, commandName); - } - - IEnumerable results = from command in _commands - where command.CommandAttribute.CommandName.StartsWith(commandName, StringComparison.OrdinalIgnoreCase) || - (command.CommandAttribute.AltName ?? String.Empty).StartsWith(commandName, StringComparison.OrdinalIgnoreCase) - select command; - - if (!results.Any()) - { - throw new CommandLineException(TaskResources.UnknownCommandError, commandName); - } - - var matchedCommand = results.First(); - if (results.Skip(1).Any()) - { - // Were there more than one results found? - matchedCommand = results.FirstOrDefault(c => c.CommandAttribute.CommandName.Equals(commandName, StringComparison.OrdinalIgnoreCase) - || commandName.Equals(c.CommandAttribute.AltName, StringComparison.OrdinalIgnoreCase)); - - if (matchedCommand == null) - { - // No exact match was found and the result returned multiple prefixes. - throw new CommandLineException(String.Format(CultureInfo.CurrentCulture, TaskResources.AmbiguousCommand, commandName, - String.Join(" ", from c in results select c.CommandAttribute.CommandName))); - } - } - return matchedCommand; - } - - public IDictionary GetCommandOptions(ICommand command) - { - var result = new Dictionary(); - - foreach (PropertyInfo propInfo in command.GetType().GetProperties()) - { - foreach (OptionAttribute attr in propInfo.GetCustomAttributes(typeof(OptionAttribute), inherit: true)) - { - if (!propInfo.CanWrite && !TypeHelper.IsMultiValuedProperty(propInfo)) - { - // If the property has neither a setter nor is of a type that can be cast to ICollection<> then there's no way to assign - // values to it. In this case throw. - throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, - TaskResources.OptionInvalidWithoutSetter, command.GetType().FullName + "." + propInfo.Name)); - } - result.Add(attr, propInfo); - } - } - - return result; - } - - public void RegisterCommand(ICommand command) - { - var attrib = command.CommandAttribute; - if (attrib != null) - { - _commands.Add(command); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/EntitiesContextFactory.cs b/src/NuGetGallery.Operations/Infrastructure/EntitiesContextFactory.cs deleted file mode 100644 index 625ef0d165..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/EntitiesContextFactory.cs +++ /dev/null @@ -1,25 +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.Data.Entity.Infrastructure; - -namespace NuGetGallery.Infrastructure -{ - /// - /// Used by EF Migrations to load the Entity Context for migrations and such like. - /// Don't use it for anything else because it doesn't respect read-only mode. - /// - public class EntitiesContextFactory : IDbContextFactory - { - // Used by GalleryGateway - internal static string OverrideConnectionString { get; set; } - - public EntitiesContext Create() - { - // readOnly: false - without read access, database migrations will fail and - // the whole site will be down (even when migrations are a no-op apparently). - return new EntitiesContext( - OverrideConnectionString, - readOnly: false); - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Infrastructure/GalleryGateway.cs b/src/NuGetGallery.Operations/Infrastructure/GalleryGateway.cs deleted file mode 100644 index 0b8d217e95..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/GalleryGateway.cs +++ /dev/null @@ -1,35 +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.Data.Entity.Infrastructure; -using System.Data.Entity.Migrations; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using NuGetGallery.Migrations; - -namespace NuGetGallery.Infrastructure -{ - /// - /// Gateway for accessing gallery services from outside the web environment - /// - /// - /// This is created, usually via reflection, by external consumers of the gallery services (i.e. galops). By using the gateway instead of directly - /// constructing Entity Contexts and Migrators in the external consumers we gain a few advantages: 1) We limit the amount of reflection needed (consumers - /// just need to dynamically create a GalleryGateway, instead of a MigrationsConfiguration, EntitiesContext (using the right constructor), etc.). 2) - /// We abstract the consumer from the details of how the data layer gets constructed. - /// - public class GalleryGateway - { - [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification="This is designed to be called using C#'s dynamic feature, so an instance method is preferable")] - public DbMigrator CreateMigrator(string connectionString, string providerType) - { - var config = new MigrationsConfiguration() - { - TargetDatabase = new DbConnectionInfo(connectionString, providerType), - ContextType = typeof(EntitiesContext), - MigrationsAssembly = Assembly.Load("NuGetGallery"), - }; - EntitiesContextFactory.OverrideConnectionString = connectionString; - return new DbMigrator(config); - } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/IAsyncCompletionTask.cs b/src/NuGetGallery.Operations/Infrastructure/IAsyncCompletionTask.cs deleted file mode 100644 index 8e31f4063c..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/IAsyncCompletionTask.cs +++ /dev/null @@ -1,20 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations.Infrastructure -{ - /// - /// Provides an interface to a task which kicks off an asynchronous job to be checked on later - /// - public interface IAsyncCompletionTask - { - TimeSpan MaximumPollingLength { get; } - TimeSpan RecommendedPollingPeriod { get; } - bool PollForCompletion(); - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/ICommand.cs b/src/NuGetGallery.Operations/Infrastructure/ICommand.cs deleted file mode 100644 index 5b57c29672..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/ICommand.cs +++ /dev/null @@ -1,17 +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.Collections.Generic; -using System.ComponentModel.Composition; - -namespace NuGetGallery.Operations -{ - [InheritedExport] - public interface ICommand - { - CommandAttribute CommandAttribute { get; } - - IList Arguments { get; } - - void Execute(); - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/ICommandManager.cs b/src/NuGetGallery.Operations/Infrastructure/ICommandManager.cs deleted file mode 100644 index 51e683a865..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/ICommandManager.cs +++ /dev/null @@ -1,17 +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.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace NuGetGallery.Operations -{ - public interface ICommandManager - { - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Method would do reflection and a property would be inappropriate.")] - IEnumerable GetCommands(); - ICommand GetCommand(string commandName); - IDictionary GetCommandOptions(ICommand command); - void RegisterCommand(ICommand command); - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/ImportExportHelper.cs b/src/NuGetGallery.Operations/Infrastructure/ImportExportHelper.cs deleted file mode 100644 index 684c2740ba..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/ImportExportHelper.cs +++ /dev/null @@ -1,254 +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 System.Linq; -using System.Text; -using System.Net; -using System.IO; -using System.Xml; -using System.Web; -using System.Runtime.Serialization; -using NuGetGallery.Operations.SqlDac; -using NLog; -using System.Threading; -using System.Diagnostics.CodeAnalysis; - -namespace WASDImportExport -{ - class ImportExportHelper - { - public string EndPointUri { get; set; } - public string StorageKey { get; set; } - public string ServerName { get; set; } - public string DatabaseName { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - - private Logger _log; - - public ImportExportHelper(Logger log) - { - _log = log; - EndPointUri = ""; - ServerName = ""; - StorageKey = ""; - DatabaseName = ""; - UserName = ""; - Password = ""; - } - - [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] - public string DoExport(string blobUri, bool whatIf) - { - _log.Info("Starting SQL DAC Export Operation"); - string requestGuid = null; - bool exportComplete = false; - string exportedBlobPath = null; - - //Setup Web Request for Export Operation - WebRequest webRequest = WebRequest.Create(this.EndPointUri + @"/Export"); - webRequest.Method = WebRequestMethods.Http.Post; - webRequest.ContentType = @"application/xml"; - - //Create Web Request Inputs - Blob Storage Credentials and Server Connection Info - ExportInput exportInputs = new ExportInput - { - BlobCredentials = new BlobStorageAccessKeyCredentials - { - StorageAccessKey = this.StorageKey, - Uri = String.Format(blobUri, this.DatabaseName, DateTime.UtcNow.Ticks.ToString()) - }, - ConnectionInfo = new ConnectionInfo - { - ServerName = this.ServerName, - DatabaseName = this.DatabaseName, - UserName = this.UserName, - Password = this.Password - } - }; - - //Perform Web Request - DataContractSerializer dataContractSerializer = new DataContractSerializer(exportInputs.GetType()); - _log.Info("http POST {0}", webRequest.RequestUri.AbsoluteUri); - if (whatIf) - { - _log.Trace("Would have sent:"); - - using (var strm = new MemoryStream()) - { - dataContractSerializer.WriteObject(strm, exportInputs); - strm.Flush(); - strm.Seek(0, SeekOrigin.Begin); - using (var reader = new StreamReader(strm)) - { - _log.Trace(reader.ReadToEnd()); - } - } - return null; - } - else - { - _log.Info("Making Web Request For Export Operation..."); - Stream webRequestStream = webRequest.GetRequestStream(); - dataContractSerializer.WriteObject(webRequestStream, exportInputs); - webRequestStream.Close(); - - //Get Response and Extract Request Identifier - WebResponse webResponse = null; - XmlReader xmlStreamReader = null; - - try - { - //Initialize the WebResponse to the response from the WebRequest - webResponse = webRequest.GetResponse(); - - xmlStreamReader = XmlReader.Create(webResponse.GetResponseStream()); - xmlStreamReader.ReadToFollowing("guid"); - requestGuid = xmlStreamReader.ReadElementContentAsString(); - _log.Info($"Export Request '{requestGuid}' submitted"); - - //Get Export Operation Status - string last = null; - while (!exportComplete) - { - List statusInfoList = CheckRequestStatus(requestGuid); - var status = statusInfoList.FirstOrDefault().Status; - if (!String.Equals(last, status, StringComparison.OrdinalIgnoreCase)) - { - _log.Info(status); - } - last = status; - - if (statusInfoList.FirstOrDefault().Status == "Failed") - { - _log.Error("Database export failed: {0}", statusInfoList.FirstOrDefault().ErrorMessage); - exportComplete = true; - } - - if (statusInfoList.FirstOrDefault().Status == "Completed") - { - exportedBlobPath = statusInfoList.FirstOrDefault().BlobUri; - _log.Info("Export Complete - Database exported to: {0}", exportedBlobPath); - exportComplete = true; - } - Thread.Sleep(5 * 1000); - } - return exportedBlobPath; - } - catch (WebException responseException) - { - _log.Error("Request Falied:{0}", responseException.Message); - if (responseException.Response != null) - { - _log.Error("Status Code: {0}", ((HttpWebResponse)responseException.Response).StatusCode); - _log.Error("Status Description: {0}", ((HttpWebResponse)responseException.Response).StatusDescription); - } - return null; - } - } - } - - //public bool DoImport(string blobUri) - //{ - // Console.Write(String.Format("Starting Import Operation - {0}\n\r", DateTime.UtcNow)); - // string requestGuid = null; - // bool importComplete = false; - - // //Setup Web Request for Import Operation - // WebRequest webRequest = WebRequest.Create(this.EndPointUri + @"/Import"); - // webRequest.Method = WebRequestMethods.Http.Post; - // webRequest.ContentType = @"application/xml"; - - // //Create Web Request Inputs - Database Size & Edition, Blob Store Credentials and Server Connection Info - // ImportInput importInputs = new ImportInput - // { - // AzureEdition = "Web", - // DatabaseSizeInGB = 1, - // BlobCredentials = new BlobStorageAccessKeyCredentials - // { - // StorageAccessKey = this.StorageKey, - // Uri = String.Format(blobUri, this.DatabaseName, DateTime.UtcNow.Ticks.ToString()) - // }, - // ConnectionInfo = new ConnectionInfo - // { - // ServerName = this.ServerName, - // DatabaseName = this.DatabaseName, - // UserName = this.UserName, - // Password = this.Password - // } - // }; - - // //Perform Web Request - // Console.WriteLine("Making Web Request for Import Operation..."); - // Stream webRequestStream = webRequest.GetRequestStream(); - // DataContractSerializer dataContractSerializer = new DataContractSerializer(importInputs.GetType()); - // dataContractSerializer.WriteObject(webRequestStream, importInputs); - // webRequestStream.Close(); - - // //Get Response and Extract Request Identifier - // Console.WriteLine("Serializing response and extracting guid..."); - // WebResponse webResponse = null; - // XmlReader xmlStreamReader = null; - - // try - // { - // //Initialize the WebResponse to the response from the WebRequest - // webResponse = webRequest.GetResponse(); - - // xmlStreamReader = XmlReader.Create(webResponse.GetResponseStream()); - // xmlStreamReader.ReadToFollowing("guid"); - // requestGuid = xmlStreamReader.ReadElementContentAsString(); - // Console.WriteLine(String.Format("Request Guid: {0}", requestGuid)); - - // //Get Status of Import Operation - // while (!importComplete) - // { - // Console.WriteLine("Checking status of Import..."); - // List statusInfoList = CheckRequestStatus(requestGuid); - // Console.WriteLine(statusInfoList.FirstOrDefault().Status); - - // if (statusInfoList.FirstOrDefault().Status == "Failed") - // { - // Console.WriteLine(String.Format("Database import failed: {0}", statusInfoList.FirstOrDefault().ErrorMessage)); - // importComplete = true; - // } - - // if (statusInfoList.FirstOrDefault().Status == "Completed") - // { - // Console.WriteLine(String.Format("Import Complete - Database imported to: {0}\n\r", statusInfoList.FirstOrDefault().DatabaseName)); - // importComplete = true; - // } - // } - // return importComplete; - // } - // catch (WebException responseException) - // { - // Console.WriteLine("Request Falied: {0}", responseException.Message); - // { - // Console.WriteLine("Status Code: {0}", ((HttpWebResponse)responseException.Response).StatusCode); - // Console.WriteLine("Status Description: {0}\n\r", ((HttpWebResponse)responseException.Response).StatusDescription); - // } - - // return importComplete; - // } - //} - - public List CheckRequestStatus(string requestGuid) - { - WebRequest webRequest = WebRequest.Create(this.EndPointUri + string.Format("/Status?servername={0}&username={1}&password={2}&reqId={3}", - HttpUtility.UrlEncode(this.ServerName), - HttpUtility.UrlEncode(this.UserName), - HttpUtility.UrlEncode(this.Password), - HttpUtility.UrlEncode(requestGuid))); - - webRequest.Method = WebRequestMethods.Http.Get; - webRequest.ContentType = @"application/xml"; - WebResponse webResponse = webRequest.GetResponse(); - XmlReader xmlStreamReader = XmlReader.Create(webResponse.GetResponseStream()); - DataContractSerializer dataContractSerializer = new DataContractSerializer(typeof(List)); - - return (List)dataContractSerializer.ReadObject(xmlStreamReader, true); - } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/JobLog.cs b/src/NuGetGallery.Operations/Infrastructure/JobLog.cs deleted file mode 100644 index 747c8dc1d1..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/JobLog.cs +++ /dev/null @@ -1,108 +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 System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace NuGetGallery.Operations.Infrastructure -{ - public class JobLog - { - private static JsonSerializerSettings _serializerSettings = new JsonSerializerSettings() - { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - MissingMemberHandling = MissingMemberHandling.Ignore, - ObjectCreationHandling = ObjectCreationHandling.Auto, - CheckAdditionalContent = false, - MaxDepth = 100 - }; - - [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Needed to add to converters list")] - static JobLog() - { - _serializerSettings.Converters.Add(new LogLevelConverter()); - } - - private IList _blobs; - - public string JobName { get; private set; } - public IEnumerable Blobs { get { return _blobs; } } - - public JobLog(string jobName, List blobs) - { - JobName = jobName; - - // Order by descending date - _blobs = blobs - .OrderByDescending(b => b.ArchiveTimestamp) - .ToList(); - } - - public IEnumerable OrderedEntries() - { - foreach (var logBlob in _blobs) - { - // Load the blob and grab the entries - var entries = LoadEntries(logBlob); - foreach (var entry in entries) - { - yield return entry; - } - } - } - - public static IEnumerable LoadJobLogs(CloudStorageAccount account) - { - // List available blobs in "wad-joblogs" container - var client = account.CreateCloudBlobClient(); - var container = client.GetContainerReference("wad-joblogs"); - var groups = container - .ListBlobs(useFlatBlobListing: true) - .OfType() - .Select(b => new JobLogBlob(b)) - .GroupBy(b => b.JobName); - - // Create Job Log info - var joblogs = groups.Select(g => new JobLog(g.Key, g.ToList())); - return joblogs; - } - - private IEnumerable LoadEntries(JobLogBlob logBlob) - { - // Download the blob to a temp file - var temp = Path.GetTempFileName(); - try - { - logBlob.Blob.DownloadToFile(temp); - - // Each line is an entry! Read them in reverse though - foreach (var line in File.ReadAllLines(temp).Reverse()) - { - yield return ParseEntry(line); - } - } - finally - { - if (File.Exists(temp)) - { - File.Delete(temp); - } - } - } - - private static JobLogEntry ParseEntry(string line) - { - var result = JsonConvert.DeserializeObject(line.Trim(), _serializerSettings); - return result; - } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/JobLogBlob.cs b/src/NuGetGallery.Operations/Infrastructure/JobLogBlob.cs deleted file mode 100644 index 67e5558371..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/JobLogBlob.cs +++ /dev/null @@ -1,53 +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 System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage.Blob; - -namespace NuGetGallery.Operations.Infrastructure -{ - public class JobLogBlob - { - private static readonly Regex BlobNameParser = new Regex(@"^(?[^/]*)/(?[^/]*)/(?[^/]*)/(?[^\.]*)\.(?\d{4}\-\d{2}\-\d{2}(\-\d{4})?)\.log\.json$"); - private const string HourTimeStamp = "yyyy-MM-dd-HHmm"; - private const string DayTimeStamp = "yyyy-MM-dd"; - - // Bob Lobwlaw's Job Log Blob! - public CloudBlockBlob Blob { get; private set; } - - public string JobName { get; private set; } - public DateTime ArchiveTimestamp { get; private set; } - - public JobLogBlob(CloudBlockBlob blob) - { - Blob = blob; - - // Parse the name - var parsed = BlobNameParser.Match(blob.Name); - if (!parsed.Success) - { - throw new ArgumentException(string.Format(NuGetGallery.Strings.JobLogBlobNameInvalid, blob.Name), nameof(blob)); - } - - // Grab the chunks we care about - JobName = parsed.Groups["job"].Value; - - string format = DayTimeStamp; - if (parsed.Groups[1].Success) - { - // Has an hour portion! - format = HourTimeStamp; - } - - ArchiveTimestamp = DateTime.ParseExact( - parsed.Groups["timestamp"].Value, - format, - CultureInfo.InvariantCulture); - } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/JobLogEntry.cs b/src/NuGetGallery.Operations/Infrastructure/JobLogEntry.cs deleted file mode 100644 index 1ba7cf7a94..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/JobLogEntry.cs +++ /dev/null @@ -1,36 +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 System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using Newtonsoft.Json; -using NLog; - -namespace NuGetGallery.Operations.Infrastructure -{ - public class JobLogEntry - { - public DateTimeOffset Timestamp { get; set; } - public string Message { get; set; } - public LogLevel Level { get; set; } - public Exception Exception { get; set; } - public string Logger { get; set; } - public JobLogEvent FullEvent { get; set; } - } - - public class JobLogEvent - { - public int SequenceID { get; set; } - public DateTime TimeStamp { get; set; } - public LogLevel Level { get; set; } - public string LoggerName { get; set; } - public string LoggerShortName { get; set; } - public string Message { get; set; } - - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification="This is a JSON serialized object")] - public string[] Parameters { get; set; } - public string FormattedMessage { get; set; } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/LogLevelConverter.cs b/src/NuGetGallery.Operations/Infrastructure/LogLevelConverter.cs deleted file mode 100644 index ef696efcef..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/LogLevelConverter.cs +++ /dev/null @@ -1,51 +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 System.Diagnostics; -using System.Linq; -using System.Text; -using Newtonsoft.Json; -using NLog; - -namespace NuGetGallery.Operations.Infrastructure -{ - public class LogLevelConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(LogLevel); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.String) - { - var ret = LogLevel.FromString((string)reader.Value); - return ret; - } - else if (reader.TokenType == JsonToken.StartObject) - { - reader.Read(); - if (reader.TokenType == JsonToken.PropertyName && String.Equals((string)reader.Value, "name", StringComparison.OrdinalIgnoreCase)) - { - reader.Read(); - if (reader.TokenType == JsonToken.String) - { - string val = (string)reader.Value; - reader.Read(); - Debug.Assert(reader.TokenType == JsonToken.EndObject); - return LogLevel.FromString(val); - } - } - } - return null; - - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(((LogLevel)value).Name); - } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/SnazzyConsoleTarget.cs b/src/NuGetGallery.Operations/Infrastructure/SnazzyConsoleTarget.cs deleted file mode 100644 index fd0ba6e5ec..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/SnazzyConsoleTarget.cs +++ /dev/null @@ -1,121 +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 System.Linq; -using NLog; -using NLog.Targets; -using ColorPair = System.Tuple; - -namespace NuGetGallery.Operations.Infrastructure -{ - public class SnazzyConsoleTarget : TargetWithLayout - { - private static readonly Dictionary ColorTable = new Dictionary() - { - { LogLevel.Debug, new ColorPair(ConsoleColor.Magenta, null) }, - { LogLevel.Error, new ColorPair(ConsoleColor.Red, null) }, - { LogLevel.Fatal, new ColorPair(ConsoleColor.White, ConsoleColor.Red) }, - { LogLevel.Info, new ColorPair(ConsoleColor.Green, null) }, - { LogLevel.Trace, new ColorPair(ConsoleColor.DarkGray, null) }, - { LogLevel.Warn, new ColorPair(ConsoleColor.Black, ConsoleColor.Yellow) } - }; - - private static readonly Dictionary LevelNames = new Dictionary() { - { LogLevel.Debug, "debug" }, - { LogLevel.Error, "error" }, - { LogLevel.Fatal, "fatal" }, - { LogLevel.Info, "info" }, - { LogLevel.Trace, "trace" }, - { LogLevel.Warn, "warn" }, - }; - - private static readonly int LevelLength = LevelNames.Values.Max(s => s.Length); - - protected override void Write(LogEventInfo logEvent) - { - var oldForeground = Console.ForegroundColor; - var oldBackground = Console.BackgroundColor; - - // Get us to the start of a line - if (Console.CursorLeft > 0) - { - Console.WriteLine(); - } - - // Get Color Pair colors - ColorPair pair; - if (!ColorTable.TryGetValue(logEvent.Level, out pair)) - { - pair = new ColorPair(Console.ForegroundColor, Console.BackgroundColor); - } - - // Get level string - string levelName; - if (!LevelNames.TryGetValue(logEvent.Level, out levelName)) - { - levelName = logEvent.Level.ToString(); - } - levelName = levelName.PadRight(LevelLength).Substring(0, LevelLength); - - // Break the message in to lines as necessary - var message = Layout.Render(logEvent); - var existingLines = message.Split(new string[] {Environment.NewLine}, StringSplitOptions.None); - var lines = new List(); - foreach (var existingLine in existingLines) - { - var prefix = levelName + ": "; - var fullMessage = prefix + existingLine; - var maxWidth = Console.BufferWidth - 2; - var currentLine = existingLine; - while (fullMessage.Length > maxWidth) - { - int end = maxWidth - prefix.Length; - int spaceIndex = currentLine.LastIndexOf(' ', Math.Min(end, message.Length - 1)); - if (spaceIndex < 10) - { - spaceIndex = end; - } - lines.Add(currentLine.Substring(0, spaceIndex).Trim()); - currentLine = currentLine.Substring(spaceIndex).Trim(); - fullMessage = prefix + currentLine; - } - lines.Add(currentLine); - } - - // Write lines - bool first = true; - foreach (var line in lines.Where(l => !String.IsNullOrWhiteSpace(l))) - { - if (first) - { - first = false; - } - else - { - Console.WriteLine(); - } - - // Write Level - Console.ForegroundColor = pair.Item1; - if (pair.Item2.HasValue) - { - Console.BackgroundColor = pair.Item2.Value; - } - Console.Write(levelName); - - // Write the message using the default foreground color, but the specified background color - // UNLESS: The background color has been changed. In which case the foreground color applies here too - var foreground = pair.Item2.HasValue - ? pair.Item1 - : oldForeground; - Console.ForegroundColor = foreground; - Console.Write(": " + line); - } - Console.WriteLine(); - - Console.ForegroundColor = oldForeground; - Console.BackgroundColor = oldBackground; - } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/SqlDbExecutorFactory.cs b/src/NuGetGallery.Operations/Infrastructure/SqlDbExecutorFactory.cs deleted file mode 100644 index 4a74cd9dcf..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/SqlDbExecutorFactory.cs +++ /dev/null @@ -1,20 +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 System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AnglicanGeek.DbExecutor; - -namespace NuGetGallery.Operations.Infrastructure -{ - public class SqlDbExecutorFactory : IDbExecutorFactory - { - public IDbExecutor OpenConnection(string connectionString) - { - return new SqlExecutor(new SqlConnection(connectionString)); - } - } -} diff --git a/src/NuGetGallery.Operations/Infrastructure/TableValuedParameter.cs b/src/NuGetGallery.Operations/Infrastructure/TableValuedParameter.cs deleted file mode 100644 index 7107aa82b3..0000000000 --- a/src/NuGetGallery.Operations/Infrastructure/TableValuedParameter.cs +++ /dev/null @@ -1,39 +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 System.Data; -using System.Data.SqlClient; -using System.Linq; -using System.Text; -using Dapper; -using Microsoft.SqlServer.Server; - -namespace NuGetGallery.Operations.Infrastructure -{ - public class TableValuedParameter : SqlMapper.IDynamicParameters - { - public string Name { get; private set; } - public string TableType { get; private set; } - public DataTable TableValue { get; private set; } - - public TableValuedParameter(string name, string tableType, DataTable tableValue) - { - Name = name; - TableType = tableType; - TableValue = tableValue; - } - - public void AddParameters(IDbCommand command, SqlMapper.Identity identity) - { - var sqlCommand = (SqlCommand)command; - var param = new SqlParameter(Name, TableValue) - { - TypeName = TableType, - SqlDbType = SqlDbType.Structured, - Direction = ParameterDirection.Input - }; - sqlCommand.Parameters.Add(param); - } - } -} diff --git a/src/NuGetGallery.Operations/LoggerExtensions.cs b/src/NuGetGallery.Operations/LoggerExtensions.cs deleted file mode 100644 index ab82bffe96..0000000000 --- a/src/NuGetGallery.Operations/LoggerExtensions.cs +++ /dev/null @@ -1,38 +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 System.Globalization; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using NLog; - -namespace NuGetGallery.Operations -{ - public static class LoggerExtensions - { - public static void Http(this Logger self, HttpWebResponse response) - { - string message = String.Format( - CultureInfo.CurrentCulture, - "http {0} {1}", - (int)response.StatusCode, - response.ResponseUri.AbsoluteUri); - if ((int)response.StatusCode >= 400) - { - self.Error(message); - } - else - { - self.Info(message); - } - } - - public static void Http(this Logger self, HttpWebRequest request) - { - self.Info("http {0} {1}", request.Method, request.RequestUri.AbsoluteUri); - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Model/OfflineDatabaseBackup.cs b/src/NuGetGallery.Operations/Model/OfflineDatabaseBackup.cs deleted file mode 100644 index 8bd0ffc1da..0000000000 --- a/src/NuGetGallery.Operations/Model/OfflineDatabaseBackup.cs +++ /dev/null @@ -1,39 +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 System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage.Blob; - -namespace NuGetGallery.Operations.Model -{ - public class OfflineDatabaseBackup - { - private static readonly Regex OfflineBlobParser = new Regex(@"Backup_(?\d{4}[A-Za-z]{3}\d{2}_\d{4})Z\.bacpac"); - - public CloudBlockBlob Blob { get; private set; } - public DateTime Timestamp { get; private set; } - - public OfflineDatabaseBackup(CloudBlockBlob blob) - { - Blob = blob; - - var match = OfflineBlobParser.Match(blob.Name); - if (!match.Success) - { - throw new FormatException("Invalid database backup name: " + blob.Name); - } - - Timestamp = DateTime.ParseExact(match.Groups["timestamp"].Value, "yyyyMMMdd_HHmm", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); - } - - public static bool IsOfflineBackup(CloudBlockBlob blob) - { - return OfflineBlobParser.IsMatch(blob.Name); - } - } -} diff --git a/src/NuGetGallery.Operations/Model/OnlineDatabaseBackup.cs b/src/NuGetGallery.Operations/Model/OnlineDatabaseBackup.cs deleted file mode 100644 index 136d6771c8..0000000000 --- a/src/NuGetGallery.Operations/Model/OnlineDatabaseBackup.cs +++ /dev/null @@ -1,75 +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 System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations.Model -{ - public class OnlineDatabaseBackup - { - public int State { get; private set; } - public string ServerName { get; private set; } - public string DatabaseName { get; private set; } - public DateTimeOffset? Timestamp { get; private set; } - - public OnlineDatabaseBackup(string serverName, string databaseName, int state) - : this(serverName, databaseName, state, ParseTimestamp(databaseName)) - { - } - - public OnlineDatabaseBackup(string serverName, string databaseName, int state, DateTimeOffset? timestamp) - { - State = state; - Timestamp = timestamp; - ServerName = serverName; - DatabaseName = databaseName; - } - - - private static readonly Regex OldBackupNameFormat = new Regex(@"^(?.+)_(?\d{14})$"); - private static readonly Regex BackupNameFormat = new Regex(@"^(?.+)_(?\d{4}[A-Za-z]{3}\d{2}_\d{4})Z$"); // Backup_2013Apr12_1452Z - public static DateTimeOffset? ParseTimestamp(string databaseName) - { - var match = BackupNameFormat.Match(databaseName); - if (match.Success) - { - return ParseNewTimestamp(match.Groups["timestamp"].Value); - } - match = OldBackupNameFormat.Match(databaseName); - if (match.Success) - { - return ParseOldTimestamp(match.Groups["timestamp"].Value); - } - return null; - } - - private static DateTimeOffset ParseOldTimestamp(string timestamp) - { - return new DateTimeOffset( - DateTime.ParseExact(timestamp, "yyyyMMddHHmmss", CultureInfo.CurrentCulture), - TimeSpan.Zero); - } - - private static DateTimeOffset ParseNewTimestamp(string timestamp) - { - return new DateTimeOffset( - DateTime.ParseExact(timestamp, "yyyyMMMdd_HHmm", CultureInfo.CurrentCulture), - TimeSpan.Zero); - } - - public override string ToString() - { - return ServerName + "." + DatabaseName; - } - - public override int GetHashCode() - { - return ToString().GetHashCode(); - } - } -} diff --git a/src/NuGetGallery.Operations/NuGetGallery.Operations.csproj b/src/NuGetGallery.Operations/NuGetGallery.Operations.csproj deleted file mode 100644 index 65d77042d6..0000000000 --- a/src/NuGetGallery.Operations/NuGetGallery.Operations.csproj +++ /dev/null @@ -1,464 +0,0 @@ - - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {DBECF66B-8F2F-4B32-9143-E243BAFF12DF} - Library - Properties - NuGetGallery.Operations - NuGetGallery.Operations - v4.6.1 - 512 - - - - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - ..\Backend.ruleset - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - False - ..\..\packages\AnglicanGeek.DbExecutor.0.1.2\lib\net40\AnglicanGeek.DbExecutor.dll - True - - - False - ..\..\packages\Dapper.1.13\lib\net45\Dapper.dll - True - - - False - ..\..\packages\entityframework.6.1.3\lib\net45\EntityFramework.dll - True - - - False - ..\..\packages\entityframework.6.1.3\lib\net45\EntityFramework.SqlServer.dll - True - - - False - ..\..\packages\Microsoft.Data.Edm.5.6.5-beta\lib\net40\Microsoft.Data.Edm.dll - True - - - False - ..\..\packages\Microsoft.Data.OData.5.6.5-beta\lib\net40\Microsoft.Data.OData.dll - True - - - False - ..\..\packages\Microsoft.Data.Services.Client.5.6.5-beta\lib\net40\Microsoft.Data.Services.Client.dll - True - - - False - ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll - True - - - False - ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll - True - - - False - ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll - True - - - False - ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - True - - - False - ..\..\packages\Microsoft.Web.Xdt.2.1.1\lib\net40\Microsoft.Web.XmlTransform.dll - True - - - False - ..\..\packages\Microsoft.WindowsAzure.Common.1.4.1\lib\net45\Microsoft.WindowsAzure.Common.dll - True - - - False - ..\..\packages\Microsoft.WindowsAzure.Common.1.4.1\lib\net45\Microsoft.WindowsAzure.Common.NetFramework.dll - True - - - False - ..\..\packages\Microsoft.WindowsAzure.ConfigurationManager.3.1.0\lib\net40\Microsoft.WindowsAzure.Configuration.dll - True - - - False - ..\..\packages\Microsoft.WindowsAzure.Management.Scheduler.0.9.2-preview\lib\net40\Microsoft.WindowsAzure.Management.Scheduler.dll - True - - - False - ..\..\packages\WindowsAzure.Storage.4.3.0\lib\net40\Microsoft.WindowsAzure.Storage.dll - True - - - ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - True - - - False - ..\..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll - True - - - False - ..\..\packages\NuGet.Common.3.5.0-beta-final\lib\net45\NuGet.Common.dll - True - - - False - ..\..\packages\NuGet.Frameworks.3.5.0-beta-final\lib\net45\NuGet.Frameworks.dll - True - - - False - ..\..\packages\NuGet.Logging.3.5.0-beta-1160\lib\net45\NuGet.Logging.dll - True - - - False - ..\..\packages\NuGet.Packaging.3.5.0-beta-final\lib\net45\NuGet.Packaging.dll - True - - - False - ..\..\packages\NuGet.Packaging.Core.3.5.0-beta-final\lib\net45\NuGet.Packaging.Core.dll - True - - - False - ..\..\packages\NuGet.Packaging.Core.Types.3.5.0-beta-final\lib\net45\NuGet.Packaging.Core.Types.dll - True - - - False - ..\..\packages\NuGet.Versioning.3.5.0-beta-final\lib\net45\NuGet.Versioning.dll - True - - - - - - - - - - - - ..\..\packages\System.Net.Http.2.0.20126.16343\lib\net40\System.Net.Http.dll - True - - - False - ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll - True - - - False - ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll - True - - - ..\..\packages\System.Net.Http.2.0.20126.16343\lib\net40\System.Net.Http.WebRequest.dll - True - - - - - False - ..\..\packages\System.Spatial.5.6.5-beta\lib\net40\System.Spatial.dll - True - - - - - False - ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll - True - - - False - ..\..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll - True - - - False - ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll - True - - - False - ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll - True - - - False - ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Reference.svcmap - - - - - - - - - True - True - CommandHelp.resx - - - - - True - True - CommonResources.resx - - - True - True - TaskResources.resx - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - Designer - - - Reference.svcmap - - - - Designer - - - Designer - - - - - ResXFileCodeGenerator - CommonResources.Designer.cs - - - ResXFileCodeGenerator - CommandHelp.Designer.cs - - - ResXFileCodeGenerator - TaskResources.Designer.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WCF Proxy Generator - Reference.cs - - - - - {097b2cdd-9623-4c34-93c2-d373d51f5b4e} - NuGetGallery.Core - - - {1dacf781-5cd0-4123-8bac-cd385d864be5} - NuGetGallery - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Package.cs b/src/NuGetGallery.Operations/Package.cs deleted file mode 100644 index b4328a0012..0000000000 --- a/src/NuGetGallery.Operations/Package.cs +++ /dev/null @@ -1,16 +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; -namespace NuGetGallery.Operations -{ - public class Package - { - public string Hash { get; set; } - public string Id { get; set; } - public int Key { get; set; } - public string Version { get; set; } - public string NormalizedVersion { get; set; } - public string ExternalPackageUrl { get; set; } - public DateTime? Created { get; set; } - } -} diff --git a/src/NuGetGallery.Operations/PackageComparer.cs b/src/NuGetGallery.Operations/PackageComparer.cs deleted file mode 100644 index 09c1b462d4..0000000000 --- a/src/NuGetGallery.Operations/PackageComparer.cs +++ /dev/null @@ -1,31 +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; - -namespace NuGetGallery.Operations -{ - public class PackageComparer : IEqualityComparer - { - public bool Equals( - Package firstPackage, - Package secondPackage) - { - return firstPackage.Id.Equals(secondPackage.Id, StringComparison.OrdinalIgnoreCase) && - firstPackage.Version.Equals(secondPackage.Version, StringComparison.OrdinalIgnoreCase) && - firstPackage.Hash.Equals(secondPackage.Hash, StringComparison.OrdinalIgnoreCase); - } - - public int GetHashCode(Package package) - { - unchecked - { - var hash = 17; - hash = hash * 23 + package.Id.GetHashCode(); - hash = hash * 23 + package.Version.GetHashCode(); - hash = hash * 23 + package.Hash.GetHashCode(); - return hash; - } - } - } -} diff --git a/src/NuGetGallery.Operations/Properties/AssemblyInfo.cs b/src/NuGetGallery.Operations/Properties/AssemblyInfo.cs deleted file mode 100644 index 217cc5215f..0000000000 --- a/src/NuGetGallery.Operations/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +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.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("NuGetGallery.Operations")] -[assembly: AssemblyDescription("Extensions to NuGetGallery.Monitoring for Azure Web Applications")] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("13364c31-0176-41ce-a6d2-f0b0905e77f5")] \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Scripts/DownloadReport_Last6Weeks.sql b/src/NuGetGallery.Operations/Scripts/DownloadReport_Last6Weeks.sql deleted file mode 100644 index e80b4f56d3..0000000000 --- a/src/NuGetGallery.Operations/Scripts/DownloadReport_Last6Weeks.sql +++ /dev/null @@ -1,42 +0,0 @@ - - DECLARE @MinDate DATE - DECLARE @MinWeekOfYear INT - DECLARE @MinYear INT - - SELECT @MinWeekOfYear = [WeekOfYear], - @MinYear = [Year] - FROM [dbo].[Dimension_Date] (NOLOCK) - WHERE [Date] = CAST(DATEADD(day, -42, GETUTCDATE) AS DATE) - - SELECT @MinDate = MIN([Date]) - FROM [dbo].[Dimension_Date] (NOLOCK) - WHERE [WeekOfYear] = @MinWeekOfYear - AND [Year] = @MinYear - - DECLARE @Cursor DATETIME = (SELECT ISNULL(MAX([Position]), @ReportGenerationTime) FROM [dbo].[Cursors] (NOLOCK) WHERE [Name] = 'GetDirtyPackageId') - - SELECT TOP 6 D.[Year], - D.[WeekOfYear], - SUM(ISNULL(Facts.[DownloadCount], 0)) 'Downloads' - FROM [dbo].[Fact_Download] AS Facts (NOLOCK) - - INNER JOIN [dbo].[Dimension_Date] AS D (NOLOCK) - ON D.[Id] = Facts.[Dimension_Date_Id] - - WHERE D.[Date] IS NOT NULL - AND ISNULL(D.[Date], CONVERT(DATE, '1900-01-01')) >= - DATETIMEFROMPARTS( - DATEPART(year, @MinDate), - DATEPART(month, @MinDate), - DATEPART(day, @MinDate), - 0, 0, 0, 0) - AND ISNULL(D.[Date], CONVERT(DATE, DATEADD(day, 1, @ReportGenerationTime))) < - DATETIMEFROMPARTS( - DATEPART(year, @ReportGenerationTime), - DATEPART(month, @ReportGenerationTime), - DATEPART(day, @ReportGenerationTime), - 0, 0, 0, 0) - AND Facts.[Timestamp] <= @Cursor - - GROUP BY D.[Year], D.[WeekOfYear] - ORDER BY [Year], [WeekOfYear] \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Scripts/DownloadReport_ListInactive.sql b/src/NuGetGallery.Operations/Scripts/DownloadReport_ListInactive.sql deleted file mode 100644 index d88ee3b59a..0000000000 --- a/src/NuGetGallery.Operations/Scripts/DownloadReport_ListInactive.sql +++ /dev/null @@ -1,10 +0,0 @@ - -SELECT DISTINCT Dimension_Package.PackageId -FROM Dimension_Package -WHERE Dimension_Package.PackageId NOT IN ( - SELECT DISTINCT Dimension_Package.PackageId - FROM Fact_Download - INNER JOIN Dimension_Package ON Dimension_Package.Id = Fact_Download.Dimension_Package_Id - INNER JOIN Dimension_Date ON Dimension_Date.Id = Fact_Download.Dimension_Date_Id - WHERE Dimension_Date.[Date] >= CONVERT(DATE, DATEADD(day, -42, GETDATE())) - AND Dimension_Date.[Date] < CONVERT(DATE, GETDATE())) diff --git a/src/NuGetGallery.Operations/Scripts/DownloadReport_NuGetClientVersion.sql b/src/NuGetGallery.Operations/Scripts/DownloadReport_NuGetClientVersion.sql deleted file mode 100644 index 34172e5e6c..0000000000 --- a/src/NuGetGallery.Operations/Scripts/DownloadReport_NuGetClientVersion.sql +++ /dev/null @@ -1,12 +0,0 @@ - -SELECT Dimension_UserAgent.ClientMajorVersion, Dimension_UserAgent.ClientMinorVersion, SUM(DownloadCount) 'Downloads' -FROM Fact_Download -INNER JOIN Dimension_UserAgent ON Dimension_UserAgent.Id = Fact_Download.Dimension_UserAgent_Id -INNER JOIN Dimension_Date ON Dimension_Date.Id = Fact_Download.Dimension_Date_Id -WHERE Dimension_Date.[Date] >= CONVERT(DATE, DATEADD(day, -42, GETDATE())) - AND Dimension_Date.[Date] < CONVERT(DATE, GETDATE()) - AND Dimension_UserAgent.ClientCategory = 'NuGet' - AND Dimension_UserAgent.ClientMajorVersion <= 2 - AND Dimension_UserAgent.ClientMinorVersion <= 7 -GROUP BY Dimension_UserAgent.ClientMajorVersion, Dimension_UserAgent.ClientMinorVersion -ORDER BY Dimension_UserAgent.ClientMajorVersion, Dimension_UserAgent.ClientMinorVersion diff --git a/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularity.sql b/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularity.sql deleted file mode 100644 index a4156851a3..0000000000 --- a/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularity.sql +++ /dev/null @@ -1,10 +0,0 @@ - -SELECT TOP(100) Dimension_Package.PackageId, SUM(DownloadCount) 'Downloads' -FROM Fact_Download -INNER JOIN Dimension_Package ON Dimension_Package.Id = Fact_Download.Dimension_Package_Id -INNER JOIN Dimension_Date ON Dimension_Date.Id = Fact_Download.Dimension_Date_Id -WHERE Dimension_Date.[Date] >= CONVERT(DATE, DATEADD(day, -42, GETDATE())) - AND Dimension_Date.[Date] < CONVERT(DATE, GETDATE()) - AND Dimension_Package.PackageListed = 1 -GROUP BY Dimension_Package.PackageId -ORDER BY SUM(DownloadCount) DESC diff --git a/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularityByPackage.sql b/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularityByPackage.sql deleted file mode 100644 index 10e21d9af9..0000000000 --- a/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularityByPackage.sql +++ /dev/null @@ -1,9 +0,0 @@ -SELECT Dimension_Package.PackageVersion, SUM(DownloadCount) 'Downloads' -FROM Fact_Download -INNER JOIN Dimension_Package ON Dimension_Package.Id = Fact_Download.Dimension_Package_Id -INNER JOIN Dimension_Date ON Dimension_Date.Id = Fact_Download.Dimension_Date_Id -WHERE Dimension_Date.[Date] >= CONVERT(DATE, DATEADD(day, -42, GETDATE())) - AND Dimension_Date.[Date] < CONVERT(DATE, GETDATE()) - AND Dimension_Package.PackageId = @PackageId -GROUP BY Dimension_Package.PackageVersion -ORDER BY SUM(DownloadCount) DESC diff --git a/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularityDetail.sql b/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularityDetail.sql deleted file mode 100644 index c0f6d1c74c..0000000000 --- a/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularityDetail.sql +++ /dev/null @@ -1,21 +0,0 @@ - -SELECT TOP(500) - Dimension_Package.PackageId, - Dimension_Package.PackageVersion, - Dimension_Package.PackageTitle, - Dimension_Package.PackageDescription, - Dimension_Package.PackageIconUrl, - SUM(DownloadCount) 'Downloads' -FROM Fact_Download -INNER JOIN Dimension_Package ON Dimension_Package.Id = Fact_Download.Dimension_Package_Id -INNER JOIN Dimension_Date ON Dimension_Date.Id = Fact_Download.Dimension_Date_Id -WHERE Dimension_Date.[Date] >= CONVERT(DATE, DATEADD(day, -42, GETDATE())) - AND Dimension_Date.[Date] < CONVERT(DATE, GETDATE()) - AND Dimension_Package.PackageListed = 1 -GROUP BY - Dimension_Package.PackageId, - Dimension_Package.PackageVersion, - Dimension_Package.PackageTitle, - Dimension_Package.PackageDescription, - Dimension_Package.PackageIconUrl -ORDER BY SUM(DownloadCount) DESC diff --git a/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularityDetailByPackage.sql b/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularityDetailByPackage.sql deleted file mode 100644 index 737229930d..0000000000 --- a/src/NuGetGallery.Operations/Scripts/DownloadReport_RecentPopularityDetailByPackage.sql +++ /dev/null @@ -1,31 +0,0 @@ -SELECT - Dimension_Package.PackageVersion, - Dimension_UserAgent.ClientCategory, - Dimension_UserAgent.Client, - Dimension_UserAgent.ClientMajorVersion, - Dimension_UserAgent.ClientMinorVersion, - Dimension_Operation.Operation, - SUM(DownloadCount) 'Downloads' -FROM Fact_Download -INNER JOIN Dimension_Package ON Dimension_Package.Id = Fact_Download.Dimension_Package_Id -INNER JOIN Dimension_Date ON Dimension_Date.Id = Fact_Download.Dimension_Date_Id -INNER JOIN Dimension_Operation ON Dimension_Operation.Id = Fact_Download.Dimension_Operation_Id -INNER JOIN Dimension_UserAgent ON Dimension_UserAgent.Id = Fact_Download.Dimension_UserAgent_Id -WHERE Dimension_Date.[Date] >= CONVERT(DATE, DATEADD(day, -42, GETDATE())) - AND Dimension_Date.[Date] < CONVERT(DATE, GETDATE()) - AND Dimension_Package.PackageId = @PackageId -GROUP BY - Dimension_Package.PackageVersion, - Dimension_UserAgent.Client, - Dimension_UserAgent.ClientCategory, - Dimension_UserAgent.ClientMajorVersion, - Dimension_UserAgent.ClientMinorVersion, - Dimension_Operation.Operation -ORDER BY - Dimension_Package.PackageVersion, - Dimension_UserAgent.Client, - Dimension_UserAgent.ClientCategory, - Dimension_UserAgent.ClientMajorVersion, - Dimension_UserAgent.ClientMinorVersion, - Dimension_Operation.Operation, - SUM(DownloadCount) DESC diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/Microsoft.SqlServer.Management.Dac.ServiceTypes.xsd b/src/NuGetGallery.Operations/Service References/SqlDac/Microsoft.SqlServer.Management.Dac.ServiceTypes.xsd deleted file mode 100644 index b541395a50..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/Microsoft.SqlServer.Management.Dac.ServiceTypes.xsd +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/Microsoft.SqlServer.Management.Dac.Services.wsdl b/src/NuGetGallery.Operations/Service References/SqlDac/Microsoft.SqlServer.Management.Dac.Services.wsdl deleted file mode 100644 index 8dcbddf070..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/Microsoft.SqlServer.Management.Dac.Services.wsdl +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.ExportResponse.datasource b/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.ExportResponse.datasource deleted file mode 100644 index 4d26e2867f..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.ExportResponse.datasource +++ /dev/null @@ -1,10 +0,0 @@ - - - - NuGetGallery.Operations.SqlDac.ExportResponse, Service References.SqlDac.Reference.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.GetStatusResponse.datasource b/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.GetStatusResponse.datasource deleted file mode 100644 index 67cf715912..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.GetStatusResponse.datasource +++ /dev/null @@ -1,10 +0,0 @@ - - - - NuGetGallery.Operations.SqlDac.GetStatusResponse, Service References.SqlDac.Reference.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.ImportResponse.datasource b/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.ImportResponse.datasource deleted file mode 100644 index 0905aa6e1f..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.ImportResponse.datasource +++ /dev/null @@ -1,10 +0,0 @@ - - - - NuGetGallery.Operations.SqlDac.ImportResponse, Service References.SqlDac.Reference.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.PostStatusResponse.datasource b/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.PostStatusResponse.datasource deleted file mode 100644 index 0d83e9d5c0..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.PostStatusResponse.datasource +++ /dev/null @@ -1,10 +0,0 @@ - - - - NuGetGallery.Operations.SqlDac.PostStatusResponse, Service References.SqlDac.Reference.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.SelectiveExportResponse.datasource b/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.SelectiveExportResponse.datasource deleted file mode 100644 index 8f835fcad0..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.SelectiveExportResponse.datasource +++ /dev/null @@ -1,10 +0,0 @@ - - - - NuGetGallery.Operations.SqlDac.SelectiveExportResponse, Service References.SqlDac.Reference.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.StatusInfo.datasource b/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.StatusInfo.datasource deleted file mode 100644 index e6e37559d9..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.StatusInfo.datasource +++ /dev/null @@ -1,10 +0,0 @@ - - - - NuGetGallery.Operations.SqlDac.StatusInfo, Service References.SqlDac.Reference.cs.dll, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.TestResponse.datasource b/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.TestResponse.datasource deleted file mode 100644 index 9d8aadf960..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/NuGetGallery.Operations.SqlDac.TestResponse.datasource +++ /dev/null @@ -1,10 +0,0 @@ - - - - NuGetGallery.Operations.SqlDac.TestResponse, Service References.SqlDac.Reference.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/Reference.cs b/src/NuGetGallery.Operations/Service References/SqlDac/Reference.cs deleted file mode 100644 index 56104e3cc4..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/Reference.cs +++ /dev/null @@ -1,877 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace NuGetGallery.Operations.SqlDac { - using System.Runtime.Serialization; - using System; - - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")] - [System.Runtime.Serialization.DataContractAttribute(Name="ExportInput", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.SqlServer.Management.Dac.Servic" + - "eTypes")] - [System.SerializableAttribute()] - public partial class ExportInput : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged { - - [System.NonSerializedAttribute()] - private System.Runtime.Serialization.ExtensionDataObject extensionDataField; - - private NuGetGallery.Operations.SqlDac.BlobCredentials BlobCredentialsField; - - private NuGetGallery.Operations.SqlDac.ConnectionInfo ConnectionInfoField; - - [global::System.ComponentModel.BrowsableAttribute(false)] - public System.Runtime.Serialization.ExtensionDataObject ExtensionData { - get { - return this.extensionDataField; - } - set { - this.extensionDataField = value; - } - } - - [System.Runtime.Serialization.DataMemberAttribute(IsRequired=true, EmitDefaultValue=false)] - public NuGetGallery.Operations.SqlDac.BlobCredentials BlobCredentials { - get { - return this.BlobCredentialsField; - } - set { - if ((object.ReferenceEquals(this.BlobCredentialsField, value) != true)) { - this.BlobCredentialsField = value; - this.RaisePropertyChanged("BlobCredentials"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute(IsRequired=true, EmitDefaultValue=false)] - public NuGetGallery.Operations.SqlDac.ConnectionInfo ConnectionInfo { - get { - return this.ConnectionInfoField; - } - set { - if ((object.ReferenceEquals(this.ConnectionInfoField, value) != true)) { - this.ConnectionInfoField = value; - this.RaisePropertyChanged("ConnectionInfo"); - } - } - } - - public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; - - protected void RaisePropertyChanged(string propertyName) { - System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; - if ((propertyChanged != null)) { - propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); - } - } - } - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")] - [System.Runtime.Serialization.DataContractAttribute(Name="BlobCredentials", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.SqlServer.Management.Dac.Servic" + - "eTypes")] - [System.SerializableAttribute()] - [System.Runtime.Serialization.KnownTypeAttribute(typeof(NuGetGallery.Operations.SqlDac.BlobSharedAccessKeyCredentials))] - [System.Runtime.Serialization.KnownTypeAttribute(typeof(NuGetGallery.Operations.SqlDac.BlobStorageAccessKeyCredentials))] - public partial class BlobCredentials : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged { - - [System.NonSerializedAttribute()] - private System.Runtime.Serialization.ExtensionDataObject extensionDataField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string UriField; - - [global::System.ComponentModel.BrowsableAttribute(false)] - public System.Runtime.Serialization.ExtensionDataObject ExtensionData { - get { - return this.extensionDataField; - } - set { - this.extensionDataField = value; - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string Uri { - get { - return this.UriField; - } - set { - if ((object.ReferenceEquals(this.UriField, value) != true)) { - this.UriField = value; - this.RaisePropertyChanged("Uri"); - } - } - } - - public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; - - protected void RaisePropertyChanged(string propertyName) { - System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; - if ((propertyChanged != null)) { - propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); - } - } - } - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")] - [System.Runtime.Serialization.DataContractAttribute(Name="ConnectionInfo", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.SqlServer.Management.Dac.Servic" + - "eTypes")] - [System.SerializableAttribute()] - public partial class ConnectionInfo : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged { - - [System.NonSerializedAttribute()] - private System.Runtime.Serialization.ExtensionDataObject extensionDataField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string DatabaseNameField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string PasswordField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string ServerNameField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string UserNameField; - - [global::System.ComponentModel.BrowsableAttribute(false)] - public System.Runtime.Serialization.ExtensionDataObject ExtensionData { - get { - return this.extensionDataField; - } - set { - this.extensionDataField = value; - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string DatabaseName { - get { - return this.DatabaseNameField; - } - set { - if ((object.ReferenceEquals(this.DatabaseNameField, value) != true)) { - this.DatabaseNameField = value; - this.RaisePropertyChanged("DatabaseName"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string Password { - get { - return this.PasswordField; - } - set { - if ((object.ReferenceEquals(this.PasswordField, value) != true)) { - this.PasswordField = value; - this.RaisePropertyChanged("Password"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string ServerName { - get { - return this.ServerNameField; - } - set { - if ((object.ReferenceEquals(this.ServerNameField, value) != true)) { - this.ServerNameField = value; - this.RaisePropertyChanged("ServerName"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string UserName { - get { - return this.UserNameField; - } - set { - if ((object.ReferenceEquals(this.UserNameField, value) != true)) { - this.UserNameField = value; - this.RaisePropertyChanged("UserName"); - } - } - } - - public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; - - protected void RaisePropertyChanged(string propertyName) { - System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; - if ((propertyChanged != null)) { - propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); - } - } - } - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")] - [System.Runtime.Serialization.DataContractAttribute(Name="BlobSharedAccessKeyCredentials", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.SqlServer.Management.Dac.Servic" + - "eTypes")] - [System.SerializableAttribute()] - public partial class BlobSharedAccessKeyCredentials : NuGetGallery.Operations.SqlDac.BlobCredentials { - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string SharedAccessKeyField; - - [System.Runtime.Serialization.DataMemberAttribute()] - public string SharedAccessKey { - get { - return this.SharedAccessKeyField; - } - set { - if ((object.ReferenceEquals(this.SharedAccessKeyField, value) != true)) { - this.SharedAccessKeyField = value; - this.RaisePropertyChanged("SharedAccessKey"); - } - } - } - } - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")] - [System.Runtime.Serialization.DataContractAttribute(Name="BlobStorageAccessKeyCredentials", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.SqlServer.Management.Dac.Servic" + - "eTypes")] - [System.SerializableAttribute()] - public partial class BlobStorageAccessKeyCredentials : NuGetGallery.Operations.SqlDac.BlobCredentials { - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string StorageAccessKeyField; - - [System.Runtime.Serialization.DataMemberAttribute()] - public string StorageAccessKey { - get { - return this.StorageAccessKeyField; - } - set { - if ((object.ReferenceEquals(this.StorageAccessKeyField, value) != true)) { - this.StorageAccessKeyField = value; - this.RaisePropertyChanged("StorageAccessKey"); - } - } - } - } - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")] - [System.Runtime.Serialization.DataContractAttribute(Name="SelectiveExportInput", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.SqlServer.Management.Dac.Servic" + - "eTypes")] - [System.SerializableAttribute()] - public partial class SelectiveExportInput : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged { - - [System.NonSerializedAttribute()] - private System.Runtime.Serialization.ExtensionDataObject extensionDataField; - - private NuGetGallery.Operations.SqlDac.BlobCredentials BlobCredentialsField; - - private NuGetGallery.Operations.SqlDac.ConnectionInfo ConnectionInfoField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private System.Collections.Generic.List TablesField; - - [global::System.ComponentModel.BrowsableAttribute(false)] - public System.Runtime.Serialization.ExtensionDataObject ExtensionData { - get { - return this.extensionDataField; - } - set { - this.extensionDataField = value; - } - } - - [System.Runtime.Serialization.DataMemberAttribute(IsRequired=true, EmitDefaultValue=false)] - public NuGetGallery.Operations.SqlDac.BlobCredentials BlobCredentials { - get { - return this.BlobCredentialsField; - } - set { - if ((object.ReferenceEquals(this.BlobCredentialsField, value) != true)) { - this.BlobCredentialsField = value; - this.RaisePropertyChanged("BlobCredentials"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute(IsRequired=true, EmitDefaultValue=false)] - public NuGetGallery.Operations.SqlDac.ConnectionInfo ConnectionInfo { - get { - return this.ConnectionInfoField; - } - set { - if ((object.ReferenceEquals(this.ConnectionInfoField, value) != true)) { - this.ConnectionInfoField = value; - this.RaisePropertyChanged("ConnectionInfo"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public System.Collections.Generic.List Tables { - get { - return this.TablesField; - } - set { - if ((object.ReferenceEquals(this.TablesField, value) != true)) { - this.TablesField = value; - this.RaisePropertyChanged("Tables"); - } - } - } - - public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; - - protected void RaisePropertyChanged(string propertyName) { - System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; - if ((propertyChanged != null)) { - propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); - } - } - } - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")] - [System.Runtime.Serialization.DataContractAttribute(Name="TableName", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.SqlServer.Management.Dac.Servic" + - "eTypes")] - [System.SerializableAttribute()] - public partial class TableName : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged { - - [System.NonSerializedAttribute()] - private System.Runtime.Serialization.ExtensionDataObject extensionDataField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string NameField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string SchemaNameField; - - [global::System.ComponentModel.BrowsableAttribute(false)] - public System.Runtime.Serialization.ExtensionDataObject ExtensionData { - get { - return this.extensionDataField; - } - set { - this.extensionDataField = value; - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string Name { - get { - return this.NameField; - } - set { - if ((object.ReferenceEquals(this.NameField, value) != true)) { - this.NameField = value; - this.RaisePropertyChanged("Name"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string SchemaName { - get { - return this.SchemaNameField; - } - set { - if ((object.ReferenceEquals(this.SchemaNameField, value) != true)) { - this.SchemaNameField = value; - this.RaisePropertyChanged("SchemaName"); - } - } - } - - public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; - - protected void RaisePropertyChanged(string propertyName) { - System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; - if ((propertyChanged != null)) { - propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); - } - } - } - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")] - [System.Runtime.Serialization.DataContractAttribute(Name="ImportInput", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.SqlServer.Management.Dac.Servic" + - "eTypes")] - [System.SerializableAttribute()] - public partial class ImportInput : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged { - - [System.NonSerializedAttribute()] - private System.Runtime.Serialization.ExtensionDataObject extensionDataField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string AzureEditionField; - - private NuGetGallery.Operations.SqlDac.BlobCredentials BlobCredentialsField; - - private NuGetGallery.Operations.SqlDac.ConnectionInfo ConnectionInfoField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private int DatabaseSizeInGBField; - - [global::System.ComponentModel.BrowsableAttribute(false)] - public System.Runtime.Serialization.ExtensionDataObject ExtensionData { - get { - return this.extensionDataField; - } - set { - this.extensionDataField = value; - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string AzureEdition { - get { - return this.AzureEditionField; - } - set { - if ((object.ReferenceEquals(this.AzureEditionField, value) != true)) { - this.AzureEditionField = value; - this.RaisePropertyChanged("AzureEdition"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute(IsRequired=true, EmitDefaultValue=false)] - public NuGetGallery.Operations.SqlDac.BlobCredentials BlobCredentials { - get { - return this.BlobCredentialsField; - } - set { - if ((object.ReferenceEquals(this.BlobCredentialsField, value) != true)) { - this.BlobCredentialsField = value; - this.RaisePropertyChanged("BlobCredentials"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute(IsRequired=true, EmitDefaultValue=false)] - public NuGetGallery.Operations.SqlDac.ConnectionInfo ConnectionInfo { - get { - return this.ConnectionInfoField; - } - set { - if ((object.ReferenceEquals(this.ConnectionInfoField, value) != true)) { - this.ConnectionInfoField = value; - this.RaisePropertyChanged("ConnectionInfo"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public int DatabaseSizeInGB { - get { - return this.DatabaseSizeInGBField; - } - set { - if ((this.DatabaseSizeInGBField.Equals(value) != true)) { - this.DatabaseSizeInGBField = value; - this.RaisePropertyChanged("DatabaseSizeInGB"); - } - } - } - - public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; - - protected void RaisePropertyChanged(string propertyName) { - System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; - if ((propertyChanged != null)) { - propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); - } - } - } - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")] - [System.Runtime.Serialization.DataContractAttribute(Name="StatusInput", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.SqlServer.Management.Dac.Servic" + - "eTypes")] - [System.SerializableAttribute()] - public partial class StatusInput : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged { - - [System.NonSerializedAttribute()] - private System.Runtime.Serialization.ExtensionDataObject extensionDataField; - - private string PasswordField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string RequestIdField; - - private string ServerNameField; - - private string UserNameField; - - [global::System.ComponentModel.BrowsableAttribute(false)] - public System.Runtime.Serialization.ExtensionDataObject ExtensionData { - get { - return this.extensionDataField; - } - set { - this.extensionDataField = value; - } - } - - [System.Runtime.Serialization.DataMemberAttribute(IsRequired=true, EmitDefaultValue=false)] - public string Password { - get { - return this.PasswordField; - } - set { - if ((object.ReferenceEquals(this.PasswordField, value) != true)) { - this.PasswordField = value; - this.RaisePropertyChanged("Password"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string RequestId { - get { - return this.RequestIdField; - } - set { - if ((object.ReferenceEquals(this.RequestIdField, value) != true)) { - this.RequestIdField = value; - this.RaisePropertyChanged("RequestId"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute(IsRequired=true, EmitDefaultValue=false)] - public string ServerName { - get { - return this.ServerNameField; - } - set { - if ((object.ReferenceEquals(this.ServerNameField, value) != true)) { - this.ServerNameField = value; - this.RaisePropertyChanged("ServerName"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute(IsRequired=true, EmitDefaultValue=false)] - public string UserName { - get { - return this.UserNameField; - } - set { - if ((object.ReferenceEquals(this.UserNameField, value) != true)) { - this.UserNameField = value; - this.RaisePropertyChanged("UserName"); - } - } - } - - public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; - - protected void RaisePropertyChanged(string propertyName) { - System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; - if ((propertyChanged != null)) { - propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); - } - } - } - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")] - [System.Runtime.Serialization.DataContractAttribute(Name="StatusInfo", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.SqlServer.Management.Dac.Servic" + - "eTypes")] - [System.SerializableAttribute()] - public partial class StatusInfo : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged { - - [System.NonSerializedAttribute()] - private System.Runtime.Serialization.ExtensionDataObject extensionDataField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string BlobUriField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string DatabaseNameField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string ErrorMessageField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private System.DateTime LastModifiedTimeField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private System.DateTime QueuedTimeField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string RequestIdField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string RequestTypeField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string ServerNameField; - - [System.Runtime.Serialization.OptionalFieldAttribute()] - private string StatusField; - - [global::System.ComponentModel.BrowsableAttribute(false)] - public System.Runtime.Serialization.ExtensionDataObject ExtensionData { - get { - return this.extensionDataField; - } - set { - this.extensionDataField = value; - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string BlobUri { - get { - return this.BlobUriField; - } - set { - if ((object.ReferenceEquals(this.BlobUriField, value) != true)) { - this.BlobUriField = value; - this.RaisePropertyChanged("BlobUri"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string DatabaseName { - get { - return this.DatabaseNameField; - } - set { - if ((object.ReferenceEquals(this.DatabaseNameField, value) != true)) { - this.DatabaseNameField = value; - this.RaisePropertyChanged("DatabaseName"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string ErrorMessage { - get { - return this.ErrorMessageField; - } - set { - if ((object.ReferenceEquals(this.ErrorMessageField, value) != true)) { - this.ErrorMessageField = value; - this.RaisePropertyChanged("ErrorMessage"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public System.DateTime LastModifiedTime { - get { - return this.LastModifiedTimeField; - } - set { - if ((this.LastModifiedTimeField.Equals(value) != true)) { - this.LastModifiedTimeField = value; - this.RaisePropertyChanged("LastModifiedTime"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public System.DateTime QueuedTime { - get { - return this.QueuedTimeField; - } - set { - if ((this.QueuedTimeField.Equals(value) != true)) { - this.QueuedTimeField = value; - this.RaisePropertyChanged("QueuedTime"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string RequestId { - get { - return this.RequestIdField; - } - set { - if ((object.ReferenceEquals(this.RequestIdField, value) != true)) { - this.RequestIdField = value; - this.RaisePropertyChanged("RequestId"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string RequestType { - get { - return this.RequestTypeField; - } - set { - if ((object.ReferenceEquals(this.RequestTypeField, value) != true)) { - this.RequestTypeField = value; - this.RaisePropertyChanged("RequestType"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string ServerName { - get { - return this.ServerNameField; - } - set { - if ((object.ReferenceEquals(this.ServerNameField, value) != true)) { - this.ServerNameField = value; - this.RaisePropertyChanged("ServerName"); - } - } - } - - [System.Runtime.Serialization.DataMemberAttribute()] - public string Status { - get { - return this.StatusField; - } - set { - if ((object.ReferenceEquals(this.StatusField, value) != true)) { - this.StatusField = value; - this.RaisePropertyChanged("Status"); - } - } - } - - public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; - - protected void RaisePropertyChanged(string propertyName) { - System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged; - if ((propertyChanged != null)) { - propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); - } - } - } - - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] - [System.ServiceModel.ServiceContractAttribute(ConfigurationName="SqlDac.IDACWebService")] - public interface IDACWebService { - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/Export", ReplyAction="http://tempuri.org/IDACWebService/ExportResponse")] - System.Guid Export(NuGetGallery.Operations.SqlDac.ExportInput exportInput); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/Export", ReplyAction="http://tempuri.org/IDACWebService/ExportResponse")] - System.Threading.Tasks.Task ExportAsync(NuGetGallery.Operations.SqlDac.ExportInput exportInput); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/SelectiveExport", ReplyAction="http://tempuri.org/IDACWebService/SelectiveExportResponse")] - System.Guid SelectiveExport(NuGetGallery.Operations.SqlDac.SelectiveExportInput exportInput); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/SelectiveExport", ReplyAction="http://tempuri.org/IDACWebService/SelectiveExportResponse")] - System.Threading.Tasks.Task SelectiveExportAsync(NuGetGallery.Operations.SqlDac.SelectiveExportInput exportInput); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/Import", ReplyAction="http://tempuri.org/IDACWebService/ImportResponse")] - System.Guid Import(NuGetGallery.Operations.SqlDac.ImportInput importInput); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/Import", ReplyAction="http://tempuri.org/IDACWebService/ImportResponse")] - System.Threading.Tasks.Task ImportAsync(NuGetGallery.Operations.SqlDac.ImportInput importInput); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/PostStatus", ReplyAction="http://tempuri.org/IDACWebService/PostStatusResponse")] - System.Collections.Generic.List PostStatus(NuGetGallery.Operations.SqlDac.StatusInput statusInput); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/PostStatus", ReplyAction="http://tempuri.org/IDACWebService/PostStatusResponse")] - System.Threading.Tasks.Task> PostStatusAsync(NuGetGallery.Operations.SqlDac.StatusInput statusInput); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/GetStatus", ReplyAction="http://tempuri.org/IDACWebService/GetStatusResponse")] - System.Collections.Generic.List GetStatus(string serverName, string userName, string password, string requestId); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/GetStatus", ReplyAction="http://tempuri.org/IDACWebService/GetStatusResponse")] - System.Threading.Tasks.Task> GetStatusAsync(string serverName, string userName, string password, string requestId); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/Test", ReplyAction="http://tempuri.org/IDACWebService/TestResponse")] - int Test(); - - [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IDACWebService/Test", ReplyAction="http://tempuri.org/IDACWebService/TestResponse")] - System.Threading.Tasks.Task TestAsync(); - } - - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] - public interface IDACWebServiceChannel : NuGetGallery.Operations.SqlDac.IDACWebService, System.ServiceModel.IClientChannel { - } - - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] - public partial class DACWebServiceClient : System.ServiceModel.ClientBase, NuGetGallery.Operations.SqlDac.IDACWebService { - - public DACWebServiceClient() { - } - - public DACWebServiceClient(string endpointConfigurationName) : - base(endpointConfigurationName) { - } - - public DACWebServiceClient(string endpointConfigurationName, string remoteAddress) : - base(endpointConfigurationName, remoteAddress) { - } - - public DACWebServiceClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : - base(endpointConfigurationName, remoteAddress) { - } - - public DACWebServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : - base(binding, remoteAddress) { - } - - public System.Guid Export(NuGetGallery.Operations.SqlDac.ExportInput exportInput) { - return base.Channel.Export(exportInput); - } - - public System.Threading.Tasks.Task ExportAsync(NuGetGallery.Operations.SqlDac.ExportInput exportInput) { - return base.Channel.ExportAsync(exportInput); - } - - public System.Guid SelectiveExport(NuGetGallery.Operations.SqlDac.SelectiveExportInput exportInput) { - return base.Channel.SelectiveExport(exportInput); - } - - public System.Threading.Tasks.Task SelectiveExportAsync(NuGetGallery.Operations.SqlDac.SelectiveExportInput exportInput) { - return base.Channel.SelectiveExportAsync(exportInput); - } - - public System.Guid Import(NuGetGallery.Operations.SqlDac.ImportInput importInput) { - return base.Channel.Import(importInput); - } - - public System.Threading.Tasks.Task ImportAsync(NuGetGallery.Operations.SqlDac.ImportInput importInput) { - return base.Channel.ImportAsync(importInput); - } - - public System.Collections.Generic.List PostStatus(NuGetGallery.Operations.SqlDac.StatusInput statusInput) { - return base.Channel.PostStatus(statusInput); - } - - public System.Threading.Tasks.Task> PostStatusAsync(NuGetGallery.Operations.SqlDac.StatusInput statusInput) { - return base.Channel.PostStatusAsync(statusInput); - } - - public System.Collections.Generic.List GetStatus(string serverName, string userName, string password, string requestId) { - return base.Channel.GetStatus(serverName, userName, password, requestId); - } - - public System.Threading.Tasks.Task> GetStatusAsync(string serverName, string userName, string password, string requestId) { - return base.Channel.GetStatusAsync(serverName, userName, password, requestId); - } - - public int Test() { - return base.Channel.Test(); - } - - public System.Threading.Tasks.Task TestAsync() { - return base.Channel.TestAsync(); - } - } -} diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/Reference.svcmap b/src/NuGetGallery.Operations/Service References/SqlDac/Reference.svcmap deleted file mode 100644 index d9dd22ad83..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/Reference.svcmap +++ /dev/null @@ -1,63 +0,0 @@ - - - - false - true - true - - false - false - false - - - - - true - Auto - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/configuration.svcinfo b/src/NuGetGallery.Operations/Service References/SqlDac/configuration.svcinfo deleted file mode 100644 index d3da738a3b..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/configuration.svcinfo +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/configuration91.svcinfo b/src/NuGetGallery.Operations/Service References/SqlDac/configuration91.svcinfo deleted file mode 100644 index 89ea0afa23..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/configuration91.svcinfo +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/service.wsdl b/src/NuGetGallery.Operations/Service References/SqlDac/service.wsdl deleted file mode 100644 index 56a50a399f..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/service.wsdl +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/service.xsd b/src/NuGetGallery.Operations/Service References/SqlDac/service.xsd deleted file mode 100644 index 392c4aa7a5..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/service.xsd +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Service References/SqlDac/service1.xsd b/src/NuGetGallery.Operations/Service References/SqlDac/service1.xsd deleted file mode 100644 index b4d5ff0f12..0000000000 --- a/src/NuGetGallery.Operations/Service References/SqlDac/service1.xsd +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Tasks/Backups/BackupDatabaseTask.cs b/src/NuGetGallery.Operations/Tasks/Backups/BackupDatabaseTask.cs deleted file mode 100644 index f870ca508f..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Backups/BackupDatabaseTask.cs +++ /dev/null @@ -1,116 +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.Data.SqlClient; -using AnglicanGeek.DbExecutor; -using NuGetGallery.Operations.Infrastructure; - -namespace NuGetGallery.Operations -{ - [Command("backupdatabase", "Backs up the database", AltName = "bdb", MaxArgs = 0)] - public class BackupDatabaseTask : OpsTask, IAsyncCompletionTask - { - private bool _startedBackup = false; - - [Option("Backup should occur if the database is older than X minutes (default 30 minutes)")] - public int IfOlderThan { get; set; } - - [Option("The prefix to apply to the backup name (the suffix is a timestamp)")] - public string BackupNamePrefix { get; set; } - - [Option("Forces the backup to be created, even if there is a recent enough backup.")] - public bool Force { get; set; } - - [Option("The connection to the database to backup", AltName = "db")] - public SqlConnectionStringBuilder ConnectionString { get; set; } - - private string _backupName; - - public BackupDatabaseTask() - { - IfOlderThan = 30; - } - - public override void ValidateArguments() - { - if (ConnectionString == null && CurrentEnvironment != null) - { - ConnectionString = SelectEnvironmentConnection(CurrentEnvironment); - } - - BackupNamePrefix = BackupNamePrefix ?? "Backup_"; - } - - public override void ExecuteCommand() - { - Log.Trace("Connecting to server '{0}' to back up database '{1}'.", ConnectionString.InitialCatalog, Util.GetDatabaseServerName(ConnectionString)); - - _startedBackup = false; - - var cstr = Util.GetMasterConnectionString(ConnectionString.ConnectionString); - using(var connection = new SqlConnection(cstr)) - using(var db = new SqlExecutor(connection)) - { - connection.Open(); - - if (!Force) - { - Log.Trace("Checking for a backup in progress."); - if (Util.BackupIsInProgress(db, BackupNamePrefix)) - { - Log.Trace("Found a backup in progress; exiting."); - return; - } - - Log.Trace("Found no backup in progress."); - - Log.Trace("Getting last backup time."); - var lastBackupTime = Util.GetLastBackupTime(db, BackupNamePrefix); - if (lastBackupTime >= DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(IfOlderThan))) - { - Log.Info("Skipping Backup. Last Backup was less than {0} minutes ago", IfOlderThan); - return; - } - Log.Trace("Last backup time is more than {0} minutes ago. Starting new backup.", IfOlderThan); - } - else - { - Log.Trace("Forcing new backup"); - } - - // Generate a backup name - var timestamp = Util.GetTimestamp(); - - _backupName = BackupNamePrefix + timestamp; - - if (!WhatIf) - { - db.Execute($"CREATE DATABASE {_backupName} AS COPY OF {ConnectionString.InitialCatalog}"); - _startedBackup = true; - } - - Log.Info("Started Copy of '{0}' to '{1}'", ConnectionString.InitialCatalog, _backupName); - } - } - - public TimeSpan RecommendedPollingPeriod - { - get { return TimeSpan.FromMinutes(1); } - } - - public TimeSpan MaximumPollingLength - { - get { return TimeSpan.FromMinutes(45); } - } - - public bool PollForCompletion() - { - return !_startedBackup || DatabaseBackupHelper.GetBackupStatus(Log, ConnectionString, _backupName); - } - - protected virtual SqlConnectionStringBuilder SelectEnvironmentConnection(DeploymentEnvironment env) - { - return env.MainDatabase; - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Backups/BackupPackageFileTask.cs b/src/NuGetGallery.Operations/Tasks/Backups/BackupPackageFileTask.cs deleted file mode 100644 index c97b596020..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Backups/BackupPackageFileTask.cs +++ /dev/null @@ -1,87 +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.IO; -using Microsoft.WindowsAzure.Storage; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("backuppackagefile", "Back up a specific package file", AltName = "bpf", MaxArgs = 0)] - public class BackupPackageFileTask : PackageVersionTask - { - [Option("The destination storage account for the backups", AltName = "d")] - public CloudStorageAccount BackupStorage { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (BackupStorage == null) - { - BackupStorage = StorageAccount; - if (CurrentEnvironment != null) - { - BackupStorage = CurrentEnvironment.BackupStorage; - } - } - } - - public override void ExecuteCommand() - { - var client = CreateBlobClient(); - var backupClient = BackupStorage.CreateCloudBlobClient(); - - var backupBlobs = backupClient.GetContainerReference("package-backups"); - var packageBlobs = client.GetContainerReference("packages"); - if (!WhatIf) - { - backupBlobs.CreateIfNotExists(); - } - - var backupFileName = Util.GetPackageBackupFileName( - PackageId, - PackageVersion, - PackageHash); - var backupPackageBlob = backupBlobs.GetBlockBlobReference(backupFileName); - if (backupPackageBlob.Exists()) - { - Log.Info("Skipped {0} {1}: backup already exists", PackageId, PackageVersion); - return; - } - - var packageFileBlob = Util.GetPackageFileBlob( - packageBlobs, - PackageId, - PackageVersion); - var packageFileName = Util.GetPackageFileName( - PackageId, - PackageVersion); - var downloadedPackageFilePath = Path.Combine(Util.GetTempFolder(), packageFileName); - - // Why are we still downloading/uploading instead of using Async Blob Copy? - // Because it feels a little safer to ensure we know the copy is truely complete before continuing. - // I could be convinced otherwise though - // - anurse - Log.Trace("Downloading package file '{0}' to temporary file '{1}'.", packageFileName, downloadedPackageFilePath); - if (!WhatIf) - { - packageFileBlob.DownloadToFile(downloadedPackageFilePath); - } - - Log.Trace("Uploading package file backup '{0}' from temporary file '{1}'.", backupFileName, downloadedPackageFilePath); - if (!WhatIf) - { - backupPackageBlob.UploadFile(downloadedPackageFilePath); - backupPackageBlob.Properties.ContentType = "application/zip"; - backupPackageBlob.SetProperties(); - } - - Log.Trace("Deleting temporary file '{0}'.", downloadedPackageFilePath); - if (!WhatIf) - { - File.Delete(downloadedPackageFilePath); - } - Log.Info("Backed Up {0} {1}", PackageId, PackageVersion); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Backups/BackupPackagesTask.cs b/src/NuGetGallery.Operations/Tasks/Backups/BackupPackagesTask.cs deleted file mode 100644 index ba894aaced..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Backups/BackupPackagesTask.cs +++ /dev/null @@ -1,234 +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 System.Data.SqlClient; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using AnglicanGeek.DbExecutor; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NuGetGallery.Operations.Tasks; - -namespace NuGetGallery.Operations -{ - [Command("backuppackages", "Back up all packages at the source storage server", AltName = "bps", MaxArgs = 0)] - public class BackupPackagesTask : DatabaseAndStorageTask - { - [Option("The destination storage account for the backups", AltName="d")] - public CloudStorageAccount BackupStorage { get; set; } - - [Option("Perform back ups in a single thread", AltName="s")] - public bool SingleThreaded { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (BackupStorage == null) - { - BackupStorage = StorageAccount; - if (CurrentEnvironment != null) - { - BackupStorage = CurrentEnvironment.BackupStorage; - } - } - } - - public override void ExecuteCommand() - { - Log.Info( - "Backing up '{0}/packages' -> '{1}/package-backups'.", - StorageAccount.Credentials.AccountName, - BackupStorage.Credentials.AccountName); - - var client = CreateBlobClient(); - var backupClient = BackupStorage.CreateCloudBlobClient(); - - // Get the state file object - var state = GetStateFile(backupClient); - var lastId = state.LastBackedUpId; - bool forcedRecheck = false; - if (state.LastBackupCompletedUtc.HasValue && ((DateTimeOffset.UtcNow - state.LastBackupCompletedUtc.Value) > TimeSpan.FromDays(1))) - { - // Do a "full" backup (check every package file) every day - lastId = null; - forcedRecheck = true; - } - - var packagesToBackUp = GetPackagesToBackUp(lastId, forcedRecheck); - - var processedCount = 0; - - var backupBlobs = backupClient.GetContainerReference("package-backups"); - var packageBlobs = client.GetContainerReference("packages"); - if (!WhatIf) - { - backupBlobs.CreateIfNotExists(); - } - Parallel.ForEach(packagesToBackUp, new ParallelOptions { MaxDegreeOfParallelism = SingleThreaded ? 1 : 10 }, package => - { - try - { - var packageBlob = packageBlobs.GetBlockBlobReference(Util.GetPackageFileName(package.Id, package.Version)); - var backupBlob = backupBlobs.GetBlockBlobReference(Util.GetPackageBackupFileName(package.Id, package.Version, package.Hash)); - if (packageBlob.Exists()) - { - bool shouldCopy = backupBlob.Exists(); - - // Verify the package, if it exists - if (shouldCopy) - { - packageBlob.FetchAttributes(); - backupBlob.FetchAttributes(); - shouldCopy = - String.IsNullOrEmpty(packageBlob.Properties.ContentMD5) || - String.IsNullOrEmpty(backupBlob.Properties.ContentMD5) || - !String.Equals(packageBlob.Properties.ContentMD5, backupBlob.Properties.ContentMD5, StringComparison.Ordinal); - } - - if (!shouldCopy && !WhatIf) - { - backupBlob.StartCopyFromBlob(packageBlob); - } - - Interlocked.Increment(ref processedCount); - Log.Trace( - "[{2:000000}/{3:000000} {4:00.0}%] {5} Backup of '{0}@{1}'.", - package.Id, - package.Version, - processedCount, - packagesToBackUp.Count, - (double)processedCount / (double)packagesToBackUp.Count, - shouldCopy ? "Skipped" : "Started"); - } - else - { - Log.Warn( - "[{2:000000}/{3:000000} {4:00.0}%] Package File not found in source: '{0}@{1}'", - package.Id, - package.Version, - processedCount, - packagesToBackUp.Count, - (double)processedCount / (double)packagesToBackUp.Count); - } - - } - catch (Exception ex) - { - Interlocked.Increment(ref processedCount); - Log.Error( - "[{2:000000}/{3:000000} {4:00.0}%] Error Starting Backup of '{0}@{1}': {5}", - package.Id, - package.Version, - processedCount, - packagesToBackUp.Count, - (double)processedCount / (double)packagesToBackUp.Count, - ex.Message); - } - }); - - Log.Info("Backed up {0} packages from {1} to {2}", processedCount, StorageAccount.Credentials.AccountName, BackupStorage.Credentials.AccountName); - - state.LastBackupCompletedUtc = DateTimeOffset.UtcNow; - state.LastBackedUpId = packagesToBackUp.Max(p => p.Key); - - WriteStateFile(backupClient, state); - } - - [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification="StreamReader will leave the stream open.")] - private State GetStateFile(CloudBlobClient backupClient) - { - var container = backupClient.GetContainerReference("package-backups"); - container.CreateIfNotExists(); - var blob = container.GetBlockBlobReference("__backupstate.json"); - if (blob.Exists()) - { - using (var strm = new MemoryStream()) - { - blob.DownloadToStream(strm); - strm.Flush(); - strm.Seek(0, SeekOrigin.Begin); - using (var rdr = new StreamReader(strm, Encoding.Default, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true)) - { - try - { - return JsonConvert.DeserializeObject(rdr.ReadToEnd()); - } - catch (Exception ex) - { - Log.ErrorException($"Error parsing state file: {ex.Message}", ex); - return new State(); // Return an empty state and continue - } - } - } - } - else - { - return new State(); - } - } - - [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "StreamReader will leave the stream open.")] - private static void WriteStateFile(CloudBlobClient backupClient, State state) - { - var container = backupClient.GetContainerReference("package-backups"); - container.CreateIfNotExists(); - var blob = container.GetBlockBlobReference("__backupstate.json"); - using (var strm = new MemoryStream()) - { - using (var writer = new StreamWriter(strm, Encoding.UTF8, bufferSize: 1024, leaveOpen: true)) - { - writer.Write(JsonConvert.SerializeObject(state)); - writer.Flush(); - } - strm.Flush(); - strm.Seek(0, SeekOrigin.Begin); - blob.UploadFromStream(strm); - } - } - - IList GetPackagesToBackUp(long? lastBackupId, bool forcedRecheck) - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - - Log.Info("Getting {1} packages to back up (since Package #{0})...", lastBackupId?.ToString() ?? "?", forcedRecheck ? "all" : "1000"); - - StringBuilder uglySqlInjectionyStringBuilder = new StringBuilder(); // We trust our own code so it's not so SQL Injectiony... - uglySqlInjectionyStringBuilder.Append("SELECT "); - if (!forcedRecheck) - { - // Back up in 1000 package chunks - uglySqlInjectionyStringBuilder.Append("TOP 1000 "); - } - uglySqlInjectionyStringBuilder.Append("p.[Key], pr.Id, p.Version, p.Hash "); - uglySqlInjectionyStringBuilder.Append("FROM Packages p "); - uglySqlInjectionyStringBuilder.Append("JOIN PackageRegistrations pr ON pr.[Key] = p.PackageRegistrationKey "); - uglySqlInjectionyStringBuilder.Append("WHERE p.ExternalPackageUrl IS NULL "); - if (lastBackupId != null) - { - uglySqlInjectionyStringBuilder.Append("AND p.[Key] > " + lastBackupId.Value + " "); - } - uglySqlInjectionyStringBuilder.Append("ORDER BY Id, Version, Hash"); - var list = dbExecutor.Query(uglySqlInjectionyStringBuilder.ToString()).ToList(); - Log.Info("Got {0} packages.", list.Count); - return list; - } - } - - private class State - { - public long? LastBackedUpId { get; set; } - public DateTimeOffset? LastBackupCompletedUtc { get; set; } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Backups/BackupWarehouseTask.cs b/src/NuGetGallery.Operations/Tasks/Backups/BackupWarehouseTask.cs deleted file mode 100644 index 4b4e5207a9..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Backups/BackupWarehouseTask.cs +++ /dev/null @@ -1,27 +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 System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations.Tasks.Backups -{ - [Command("backupwarehouse", "Backs up the database", AltName = "bw", MaxArgs = 0)] - public class BackupWarehouseTask : BackupDatabaseTask - { - public override void ValidateArguments() - { - BackupNamePrefix = BackupNamePrefix ?? "WarehouseBackup_"; - - base.ValidateArguments(); - } - - protected override SqlConnectionStringBuilder SelectEnvironmentConnection(DeploymentEnvironment env) - { - return env.WarehouseDatabase; - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Backups/CleanOfflineDatabaseBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/Backups/CleanOfflineDatabaseBackupsTask.cs deleted file mode 100644 index 64f70a580b..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Backups/CleanOfflineDatabaseBackupsTask.cs +++ /dev/null @@ -1,82 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using NuGetGallery.Operations.Model; - -namespace NuGetGallery.Operations.Tasks.Backups -{ - [Command("cleanofflinedatabasebackups", "Lists the available offline database backups", AltName = "cloffback")] - public class CleanOfflineDatabaseBackupsTask : BackupStorageTask - { - public override void ExecuteCommand() - { - var today = DateTime.UtcNow.Date; - - // List available backups - var toKeep = new HashSet(); - var client = CreateBlobClient(); - var container = client.GetContainerReference("database-backups"); - var backups = container - .ListBlobs(useFlatBlobListing: true, blobListingDetails: BlobListingDetails.Metadata) - .Cast() - .Where(b => OfflineDatabaseBackup.IsOfflineBackup(b)) - .Select(b => new OfflineDatabaseBackup(b)) - .ToList(); - - // Group by days - var days = backups.GroupBy(b => b.Timestamp.Date).OrderByDescending(g => g.Key); - - // Keep the last backup of each day for the past 7 days where we have backups - foreach (var backup in days.Take(7).Select(g => g.OrderByDescending(b => b.Timestamp).First())) - { - if (backup.Timestamp < today.AddDays(-7)) - { - Log.Warn("Backup '{0}' is in the last 7 days of backups but is older than 7 days!", backup.Blob.Name); - } - toKeep.Add(backup.Blob.Name); - } - - // Group previous backups in to months - var months = backups - .GroupBy(b => new { b.Timestamp.Month, b.Timestamp.Year }) - .Where(g => g.Key.Month != today.Month || g.Key.Year != today.Year); - - // Keep the last chronological backup for each month - foreach(var backup in months.Select(g => g.OrderByDescending(b => b.Timestamp).First())) - { - toKeep.Add(backup.Blob.Name); - } - - // Remove anything older than a year - foreach (var backup in backups.Where(b => b.Timestamp < today.AddYears(-1) && toKeep.Contains(b.Blob.Name))) - { - toKeep.Remove(backup.Blob.Name); - } - - foreach (var backup in backups) - { - if (!toKeep.Contains(backup.Blob.Name)) - { - DeleteBackup(backup.Blob); - } - } - - Log.Info("Finished cleaning backups!"); - } - - private void DeleteBackup(CloudBlockBlob blob) - { - Log.Info("Deleting Blob: {0}", blob.Uri.AbsoluteUri); - if (!WhatIf) - { - blob.DeleteIfExists(DeleteSnapshotsOption.IncludeSnapshots, accessCondition: AccessCondition.GenerateIfMatchCondition(blob.Properties.ETag)); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Backups/CleanOnlineDatabaseBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/Backups/CleanOnlineDatabaseBackupsTask.cs deleted file mode 100644 index 242ea90b43..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Backups/CleanOnlineDatabaseBackupsTask.cs +++ /dev/null @@ -1,133 +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 System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AnglicanGeek.DbExecutor; -using Microsoft.WindowsAzure.Storage; -using NuGetGallery.Operations.Common; -using NuGetGallery.Operations.Model; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("cleanonlinebackups", "Cleans up online database backups", AltName="clodb")] - public class CleanOnlineDatabaseBackupsTask : DatabaseTask - { - [Option("The storage account containing offline database backups (.bacpac files)", AltName="st")] - public CloudStorageAccount BackupStorage { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (BackupStorage == null && CurrentEnvironment != null) - { - BackupStorage = CurrentEnvironment.BackupStorage; - } - - ArgCheck.RequiredOrConfig(BackupStorage, "BackupStorage"); - } - - public override void ExecuteCommand() - { - // The backup policy: - // 1. Keep 2 rolling 30 minute backups - // 2. Keep the last backup (from 23:30 - 23:59) of the past two days - // 3. Delete any backup from before the past two days - - // The result is: - // 1 Active Database - // 1 30min-old Backup - // 1 60min-old Backup - // 1 24hr-old (at most) Backup - // 1 48hr-old (at most) Backup - - // TODO: Parameterize the policy (i.e. BackupPeriod, RollingBackupCount, DailyBackupCount, etc.) - - var cstr = Util.GetMasterConnectionString(ConnectionString.ConnectionString); - using (var connection = new SqlConnection(cstr)) - using (var db = new SqlExecutor(connection)) - { - connection.Open(); - - // Get the list of backups - var backups = db.Query( - "SELECT name, state FROM sys.databases WHERE name LIKE 'Backup_%'", - new { state = Util.OnlineState }) - .Select(d => new OnlineDatabaseBackup(Util.GetDatabaseServerName(ConnectionString), d.Name, d.State)) - .OrderByDescending(b => b.Timestamp) - .ToList(); - - // Any currently copying are safe - var keepers = new HashSet(); - keepers.AddRange(backups.Where(b => b.State == Util.CopyingState).Select(b => b.DatabaseName)); - - // The last 2 online databases are definitely safe - keepers.AddRange(backups.Where(b => b.State == Util.OnlineState).Take(2).Select(b => b.DatabaseName)); - Log.Info("Selected most recent two backups: {0}", String.Join(", ", keepers)); - - // Group by day, and skip any from today - var days = backups - .GroupBy(b => b.Timestamp.Value.Date) - .OrderByDescending(g => g.Key) - .Where(g => g.Key < DateTime.UtcNow.Date); // .Date gives us the current day at 00:00 hours, so "<" means previous day and earlier - - // Keep the last backup from each of the previous two days - var dailyBackups = days - .Take(2) // Grab the last two days - .Select(day => day.OrderByDescending(b => b.Timestamp.Value).First()); // The last backup from each day - Log.Info("Selected most recent two daily backups: {0}", String.Join(", ", dailyBackups.Select(b => b.DatabaseName))); - - // Verify data - var brokenDays = dailyBackups.Where(b => b.Timestamp.Value.TimeOfDay < new TimeSpan(23, 30, 00)); - if(brokenDays.Any()) { - foreach(var brokenDay in brokenDays) { - Log.Warn("Daily backups for {0} are from earlier than 23:30 hours?", brokenDay.Timestamp.Value.DateTime.ToShortDateString()); - } - } - var exportedDailyBackups = days.Skip(2).Select(day => day.Last()); - var client = BackupStorage.CreateCloudBlobClient(); - var container = client.GetContainerReference("database-backups"); - foreach (var exportedDaily in exportedDailyBackups) - { - // We should be able to find a backup blob - string blobName = exportedDaily.DatabaseName + ".bacpac"; - var blob = container.GetBlockBlobReference(blobName); - if (!blob.Exists()) - { - // Derp? - Log.Warn("Expected {0} blob to exist but it hasn't been exported!", blob.Name); - keepers.Add(exportedDaily.DatabaseName); // Keep it for now. - } - } - - // Keep those backups! - keepers.AddRange(dailyBackups.Select(b => b.DatabaseName)); - - // Figure out how many we're keeping - Log.Info("Keeping the following Backups: {0}", String.Join(", ", keepers)); - - if (keepers.Count < 2) - { - // Abort! - Log.Warn("About to clean too many backups. Aborting until we have enough to be in-policy."); - } - else - { - // Delete the rest! - foreach (var backup in backups.Where(b => !keepers.Contains(b.DatabaseName))) - { - if (!WhatIf) - { - db.Execute("DROP DATABASE " + backup.DatabaseName); - } - Log.Info("Deleted {0}", backup.DatabaseName); - } - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Backups/CleanWarehouseBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/Backups/CleanWarehouseBackupsTask.cs deleted file mode 100644 index adf480e132..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Backups/CleanWarehouseBackupsTask.cs +++ /dev/null @@ -1,53 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; -using NuGetGallery.Operations.Model; - -namespace NuGetGallery.Operations.Tasks.Backups -{ - [Command("cleanwarehousebackups", "Clean and exports warehouse backups", AltName = "clwb")] - public class CleanWarehouseBackupsTask : WarehouseTask - { - public override void ExecuteCommand() - { - WithMasterConnection((connection, db) => - { - // Get the list of backups - var backups = db.Query( - "SELECT name, state FROM sys.databases WHERE name LIKE 'WarehouseBackup_%'", - new { state = Util.OnlineState }) - .Select(d => new OnlineDatabaseBackup(Util.GetDatabaseServerName(ConnectionString), d.Name, d.State)) - .OrderByDescending(b => b.Timestamp) - .ToList(); - - // Any currently copying are safe - var keepers = new HashSet(); - keepers.AddRange(backups.Where(b => b.State == Util.CopyingState).Select(b => b.DatabaseName)); - - // The last online database is safe - keepers.AddRange(backups - .Where(b => b.State == Util.OnlineState && b.Timestamp != null) - .OrderByDescending(d => d.Timestamp.Value) - .Select(b => b.DatabaseName) - .Take(1)); - - // Figure out how many we're keeping - Log.Info("Keeping the following Backups: {0}", String.Join(", ", keepers)); - - // Done! Delete the non-keepers - foreach (var backup in backups.Where(b => !keepers.Contains(b.DatabaseName))) - { - if (!WhatIf) - { - db.Execute("DROP DATABASE " + backup.DatabaseName); - } - Log.Info("Deleted {0}", backup.DatabaseName); - } - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Backups/ExportDailyBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/Backups/ExportDailyBackupsTask.cs deleted file mode 100644 index b0b0b1292a..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Backups/ExportDailyBackupsTask.cs +++ /dev/null @@ -1,103 +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 System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AnglicanGeek.DbExecutor; -using Microsoft.WindowsAzure.Storage; -using NuGetGallery.Operations.Common; -using NuGetGallery.Operations.Model; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("exportdailybackups", "Exports the daily backup for each day to Blob storage", AltName = "xddb", IsSpecialPurpose = true)] - public class ExportDailyBackupsTask : DatabaseTask - { - [Option("The storage account in which to place the backup", AltName = "s")] - public CloudStorageAccount StorageAccount { get; set; } - - [Option("The URL of the SQL DAC Endpoint", AltName="dac")] - public Uri SqlDacEndpoint { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (CurrentEnvironment != null) - { - if (StorageAccount == null) - { - StorageAccount = CurrentEnvironment.BackupStorage; - } - if (SqlDacEndpoint == null) - { - SqlDacEndpoint = CurrentEnvironment.SqlDacEndpoint; - } - } - - ArgCheck.RequiredOrConfig(StorageAccount, "StorageAccount"); - ArgCheck.RequiredOrConfig(SqlDacEndpoint, "SqlDacEndpoint"); - } - - public override void ExecuteCommand() - { - var cstr = Util.GetMasterConnectionString(ConnectionString.ConnectionString); - using (var connection = new SqlConnection(cstr)) - using (var db = new SqlExecutor(connection)) - { - connection.Open(); - - // Snap the current date just in case we are running right on the cusp - var today = DateTime.UtcNow; - - // Get the list of database backups - var backups = db.Query( - "SELECT name, state FROM sys.databases WHERE name LIKE 'Backup_%'") - .Select(d => new OnlineDatabaseBackup(Util.GetDatabaseServerName(ConnectionString), d.Name, d.State)) - .OrderByDescending(b => b.Timestamp) - .ToList(); - - // Grab end-of-day backups from days before today - var dailyBackups = backups - .GroupBy(b => b.Timestamp.Value.Date) - .Where(g => g.Key < today.Date) - .Select(g => g.OrderByDescending(b => b.Timestamp.Value).Last()) - .ToList(); - Log.Info("Found {0} daily backups to export", dailyBackups.Count); - - // Start exporting them - foreach (var dailyBackup in dailyBackups) - { - if (dailyBackup.State != Util.OnlineState) - { - Log.Info("Skipping '{0}', it is still being copied", dailyBackup.DatabaseName); - } - else - { - if (dailyBackup.Timestamp.Value.TimeOfDay < new TimeSpan(23, 30, 00)) - { - Log.Warn("Somehow, '{0}' is the only backup from {1}. Exporting it to be paranoid", - dailyBackup.DatabaseName, - dailyBackup.Timestamp.Value.Date.ToShortDateString()); - } - Log.Info("Exporting '{0}'...", dailyBackup.DatabaseName); - (new ExportDatabaseTask() - { - ConnectionString = new SqlConnectionStringBuilder(ConnectionString.ConnectionString) - { - InitialCatalog = dailyBackup.DatabaseName - }, - DestinationStorage = StorageAccount, - DestinationContainer = "database-backups", - SqlDacEndpoint = SqlDacEndpoint, - WhatIf = WhatIf - }).Execute(); - } - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Backups/ExportWarehouseBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/Backups/ExportWarehouseBackupsTask.cs deleted file mode 100644 index 971b0a15c7..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Backups/ExportWarehouseBackupsTask.cs +++ /dev/null @@ -1,83 +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 System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AnglicanGeek.DbExecutor; -using Microsoft.WindowsAzure.Storage; -using NuGetGallery.Operations.Common; -using NuGetGallery.Operations.Model; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("exportwarehousebackups", "Exports the daily backup for each day to Blob storage", AltName = "xwb", IsSpecialPurpose = true)] - public class ExportWarehouseBackupsTask : WarehouseTask - { - [Option("The storage account in which to place the backup", AltName = "s")] - public CloudStorageAccount StorageAccount { get; set; } - - [Option("The URL of the SQL DAC Endpoint", AltName="dac")] - public Uri SqlDacEndpoint { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (CurrentEnvironment != null) - { - if (StorageAccount == null) - { - StorageAccount = CurrentEnvironment.BackupStorage; - } - if (SqlDacEndpoint == null) - { - SqlDacEndpoint = CurrentEnvironment.SqlDacEndpoint; - } - } - - ArgCheck.RequiredOrConfig(StorageAccount, "StorageAccount"); - ArgCheck.RequiredOrConfig(SqlDacEndpoint, "SqlDacEndpoint"); - } - - public override void ExecuteCommand() - { - WithMasterConnection((connection, db) => - { - // Get the list of database backups - var backups = db.Query( - "SELECT name, state FROM sys.databases WHERE name LIKE 'WarehouseBackup_%' AND state = @state", - new { state = Util.OnlineState }) - .Select(d => new OnlineDatabaseBackup(Util.GetDatabaseServerName(ConnectionString), d.Name, d.State)) - .Where(b => b.Timestamp != null) - .OrderByDescending(b => b.Timestamp) - .ToList(); - - // Grab any end-of-day backups - var dailyBackups = backups - .Where(b => b.Timestamp.Value.Hour == 23 && b.Timestamp.Value.Minute > 30) - .ToList(); - Log.Info("Found {0} daily backups to export", dailyBackups.Count); - - // Start exporting them - foreach (var dailyBackup in dailyBackups) - { - Log.Info("Exporting '{0}'...", dailyBackup.DatabaseName); - (new ExportDatabaseTask() - { - ConnectionString = new SqlConnectionStringBuilder(ConnectionString.ConnectionString) - { - InitialCatalog = dailyBackup.DatabaseName - }, - DestinationStorage = StorageAccount, - DestinationContainer = "warehouse-backups", - SqlDacEndpoint = SqlDacEndpoint, - WhatIf = WhatIf - }).Execute(); - } - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Backups/ListOfflineDatabaseBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/Backups/ListOfflineDatabaseBackupsTask.cs deleted file mode 100644 index 21208ee9c4..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Backups/ListOfflineDatabaseBackupsTask.cs +++ /dev/null @@ -1,35 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage.Blob; -using NuGetGallery.Operations.Model; - -namespace NuGetGallery.Operations.Tasks.Backups -{ - [Command("listofflinedatabasebackups", "Lists the available offline database backups", AltName="loffback")] - public class ListOfflineDatabaseBackupsTask : BackupStorageTask - { - public override void ExecuteCommand() - { - // List available backups - var client = CreateBlobClient(); - var container = client.GetContainerReference("database-backups"); - var backups = container - .ListBlobs(useFlatBlobListing: true) - .Cast() - .Where(b => OfflineDatabaseBackup.IsOfflineBackup(b)) - .Select(b => new OfflineDatabaseBackup(b)) - .ToList(); - - Log.Info("Available Backups:"); - foreach (var backup in backups) - { - Log.Info("* {0} ({1} Local, {2} UTC)", backup.Blob.Name, backup.Timestamp.ToLocalTime().ToFriendlyDateTimeString(), backup.Timestamp.ToUniversalTime().ToFriendlyDateTimeString()); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/BlobBackupFailedException.cs b/src/NuGetGallery.Operations/Tasks/BlobBackupFailedException.cs deleted file mode 100644 index 116b4ccfef..0000000000 --- a/src/NuGetGallery.Operations/Tasks/BlobBackupFailedException.cs +++ /dev/null @@ -1,15 +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; - -namespace NuGetGallery.Operations.Tasks -{ - [Serializable] - public class BlobBackupFailedException : Exception - { - public BlobBackupFailedException(string message) - : base(message) - { - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Tasks/CheckBlobCopiesTask.cs b/src/NuGetGallery.Operations/Tasks/CheckBlobCopiesTask.cs deleted file mode 100644 index cbb22074cd..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CheckBlobCopiesTask.cs +++ /dev/null @@ -1,74 +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 System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("checkblobcopies", "Copies blobs from a source to a destination", AltName = "chkcp")] - public class CheckBlobCopiesTask : OpsTask - { - [Option("Connection string to the destination storage server", AltName = "ds")] - public CloudStorageAccount DestinationStorage { get; set; } - - [Option("Container to copy the blobs to", AltName = "dc")] - public string DestinationContainer { get; set; } - - [Option("Prefix of source blobs to copy. If not specified, copies all blobs", AltName = "p")] - public string Prefix { get; set; } - - [Option("Set this switch to wait for the copies to complete and continue to report status")] - public bool Wait { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - ArgCheck.Required(DestinationStorage, "DestinationStorage"); - ArgCheck.Required(DestinationContainer, "DestinationContainer"); - } - - public override void ExecuteCommand() - { - var destClient = DestinationStorage.CreateCloudBlobClient(); - var destContainer = destClient.GetContainerReference(DestinationContainer); - - // Iterate through the blobs - int index = 0; - var blobs = Util.EnumerateBlobs(Log, destContainer, Prefix ?? String.Empty); - Parallel.ForEach(blobs, new ParallelOptions() { MaxDegreeOfParallelism = 10 }, blob => - { - Interlocked.Increment(ref index); - if (blob.CopyState.Status != CopyStatus.Pending) - { - int counter = 0; - while (blob.CopyState.Status == CopyStatus.Pending) - { - Thread.Sleep(1000); - counter++; - blob.FetchAttributes(); - - if (counter % 5 == 0) - { - Log.Info("{1}Waiting on {0} ...", blob.Name, counter > 5 ? "Still " : ""); - } - } - if (counter > 2) - { - Log.Info("Copy of {0} has finished!", blob.Name); - } - } - index++; - }); - - Log.Info("{0} Copies Complete!", index); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CheckDatabaseStatusTask.cs b/src/NuGetGallery.Operations/Tasks/CheckDatabaseStatusTask.cs deleted file mode 100644 index 923083fe9e..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CheckDatabaseStatusTask.cs +++ /dev/null @@ -1,59 +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.Data.SqlClient; -using AnglicanGeek.DbExecutor; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("checkdatabase", "Checks the status of the database", AltName = "chkdb", MaxArgs = 0)] - public class CheckDatabaseStatusTask: DatabaseTask - { - [Option("Force a backup, even if there is one less than 24 hours old", AltName="db")] - public string BackupName { get; set; } - - public int State { get; private set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(BackupName, "BackupName"); - } - - public override void ExecuteCommand() - { - var masterConnectionString = Util.GetMasterConnectionString(ConnectionString.ConnectionString); - - using (SqlConnection connection = new SqlConnection(masterConnectionString)) - { - connection.Open(); - - SqlCommand command = new SqlCommand("SELECT [state] FROM sys.databases WHERE [name] = @BackupName", connection); - command.Parameters.AddWithValue("BackupName", BackupName); - - SqlDataReader reader = command.ExecuteReader(); - - int count = 0; - - while (reader.Read()) - { - State = int.Parse(reader.GetValue(0).ToString()); - - count++; - } - - if (count < 1) - { - State = -1; // Not present. - } - if (count > 1) - { - throw new InvalidOperationException("Please provide a specific database name"); - } - - Log.Info("'{0}' State: {1}", BackupName, State); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CleanTagsTask.cs b/src/NuGetGallery.Operations/Tasks/CleanTagsTask.cs deleted file mode 100644 index e067f113e7..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CleanTagsTask.cs +++ /dev/null @@ -1,32 +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 System.Data; -using System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("cleantabs", "Cleans up Tags by removing commas", AltName = "ctabs", MaxArgs = 0)] - public class CleanTagsTask : DatabaseTask - { - public override void ExecuteCommand() - { - using (SqlConnection connection = new SqlConnection(ConnectionString.ConnectionString)) - { - connection.Open(); - - string sql = "UPDATE Packages SET Tags = REPLACE(REPLACE(Tags, ',', ' '), ' ', ' ')"; - - SqlCommand command = new SqlCommand(sql, connection); - command.CommandType = CommandType.Text; - command.CommandTimeout = 60 * 5; - - command.ExecuteNonQuery(); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CopyBlobsTask.cs b/src/NuGetGallery.Operations/Tasks/CopyBlobsTask.cs deleted file mode 100644 index c9a475b712..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CopyBlobsTask.cs +++ /dev/null @@ -1,123 +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 System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using Microsoft.WindowsAzure.Storage.Blob.Protocol; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("copyblobs", "Copies blobs from a source to a destination", AltName = "cpb")] - public class CopyBlobsTask : OpsTask - { - [Option("Connection string to the source storage server", AltName = "ss")] - public CloudStorageAccount SourceStorage { get; set; } - - [Option("Container containing the blobs to copy", AltName = "sc")] - public string SourceContainer { get; set; } - - [Option("Connection string to the destination storage server", AltName = "ds")] - public CloudStorageAccount DestinationStorage { get; set; } - - [Option("Container to copy the blobs to", AltName = "dc")] - public string DestinationContainer { get; set; } - - [Option("Prefix of source blobs to copy. If not specified, copies all blobs", AltName = "p")] - public string Prefix { get; set; } - - [Option("If specified, overwrite existing blobs. Otherwise, blobs that exist will be ignored (this is a name check ONLY)", AltName = "w")] - public bool Overwrite { get; set; } - - [Option("If specified, adds checks to ensure the process only copies valid package blobs (i.e. no '/' prefix and all lowercase names)")] - public bool PackageBlobsOnly { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - ArgCheck.Required(SourceStorage, "SourceStorage"); - ArgCheck.Required(SourceContainer, "SourceContainer"); - ArgCheck.Required(DestinationStorage, "DestinationStorage"); - ArgCheck.Required(DestinationContainer, "DestinationContainer"); - } - - public override void ExecuteCommand() - { - var sourceClient = SourceStorage.CreateCloudBlobClient(); - var sourceContainer = sourceClient.GetContainerReference(SourceContainer); - if (!sourceContainer.Exists()) - { - Log.Warn("No blobs in container!"); - return; - } - - var destClient = DestinationStorage.CreateCloudBlobClient(); - var destContainer = destClient.GetContainerReference(DestinationContainer); - destContainer.CreateIfNotExists(); - - Log.Info("Collecting blob names in {0}...", SourceStorage.Credentials.AccountName); - var sourceBlobs = Util.CollectBlobs( - Log, - sourceContainer, - Prefix ?? String.Empty, - condition: b => (!PackageBlobsOnly || (!b.Name.StartsWith("/", StringComparison.Ordinal) && String.Equals(b.Name.ToLowerInvariant(), b.Name, StringComparison.Ordinal))), - countEstimate: 250000); - - Log.Info("Collecting blob names in {0}...", DestinationStorage.Credentials.AccountName); - var destBlobs = Util.CollectBlobs( - Log, - destContainer, - Prefix ?? String.Empty, - condition: b => (!PackageBlobsOnly || (!b.Name.StartsWith("/", StringComparison.Ordinal) && String.Equals(b.Name.ToLowerInvariant(), b.Name, StringComparison.Ordinal))), - countEstimate: 250000); - var count = sourceBlobs.Count; - int index = 0; - - Parallel.ForEach(sourceBlobs, new ParallelOptions { MaxDegreeOfParallelism = 10 }, blob => - { - int currentIndex = Interlocked.Increment(ref index); - var percentage = (((double)currentIndex / (double)count) * 100); - var destBlob = destContainer.GetBlockBlobReference(blob.Name); - - try - { - - if (!destBlob.Exists() || Overwrite) - { - Log.Info("[{1:000000}/{2:000000}] ({3:000.00}%) Started Async Copy of {0}.", blob.Name, currentIndex, count, percentage); - if (!WhatIf) - { - destBlob.StartCopyFromBlob(blob); - } - } - else - { - Log.Info("[{1:000000}/{2:000000}] ({3:000.00}%) Skipped {0}. Blob already Exists", blob.Name, index, count, percentage); - } - } - catch (StorageException stex) - { - if (stex.RequestInformation.HttpStatusCode == (int)HttpStatusCode.Conflict && - stex.RequestInformation.ExtendedErrorInformation.ErrorCode == BlobErrorCodeStrings.PendingCopyOperation) - { - Log.Info("[{1:000000}/{2:000000}] ({3:000.00}%) Skipped {0}. Already being copied", blob.Name, index, count, percentage); - } - } - catch (Exception ex) - { - Log.Error("Error processing {0}: {1}", blob.Name, ex.ToString()); - throw; - } - }); - - Log.Info("Copies started. Run checkblobcopies with similar parameters to wait on blob copy completion"); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CopyDatabaseBackupTask.cs b/src/NuGetGallery.Operations/Tasks/CopyDatabaseBackupTask.cs deleted file mode 100644 index a2ebd7dee3..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CopyDatabaseBackupTask.cs +++ /dev/null @@ -1,122 +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.Data.SqlClient; -using System.Threading; -using AnglicanGeek.DbExecutor; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("copydatabasebackup", "Copy the specified database backup from the source to the destination", AltName = "cdb", MaxArgs=0)] - public class CopyDatabaseBackupTask : OpsTask - { - [Option("Connection string to the source database server", AltName = "s")] - public SqlConnectionStringBuilder SourceConnectionString { get; set; } - - [Option("Connection string to the destination database server", AltName = "d")] - public SqlConnectionStringBuilder DestinationConnectionString { get; set; } - - [Option("Name of the backup file", AltName = "n")] - public string BackupName { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (CurrentEnvironment != null) - { - if (DestinationConnectionString == null) - { - DestinationConnectionString = CurrentEnvironment.MainDatabase; - } - } - - ArgCheck.Required(SourceConnectionString, "SourceConnectionString"); - ArgCheck.RequiredOrConfig(DestinationConnectionString, "DestinationConnectionString"); - ArgCheck.Required(BackupName, "BackupName"); - } - - public override void ExecuteCommand() - { - using (var destinationConnection = new SqlConnection(Util.GetMasterConnectionString(DestinationConnectionString.ConnectionString))) - using (var destinationDbExecutor = new SqlExecutor(destinationConnection)) - { - string sourceDbServerName = Util.GetDatabaseServerName(SourceConnectionString); - string destinationDbServerName = Util.GetDatabaseServerName(DestinationConnectionString); - - destinationConnection.Open(); - - var copyDbName = $"CopyOf{BackupName}"; - - var existingDatabaseBackup = Util.GetDatabase( - destinationDbExecutor, - copyDbName); - - if (existingDatabaseBackup != null && existingDatabaseBackup.State == Util.OnlineState) - { - Log.Info("Skipping {0}. It already exists on {1} and is online.", copyDbName, destinationDbServerName); - return; - } - - if (existingDatabaseBackup == null) - { - StartBackupCopy( - destinationDbExecutor, - sourceDbServerName, - destinationDbServerName, - BackupName, - copyDbName); - - Log.Trace("Waiting 15 minutes for copy of {0} from {1} to {2} to complete.", BackupName, sourceDbServerName, destinationDbServerName); - if (!WhatIf) - { - Thread.Sleep(15 * 60 * 1000); - } - } - - WaitForBackupCopy( - destinationDbExecutor, - destinationDbServerName, - copyDbName); - } - } - - private void StartBackupCopy( - IDbExecutor dbExecutor, - string sourceDbServerName, - string destinationDbServerName, - string sourceDbName, - string copyDbName) - { - Log.Trace("Starting copy of {0} from {1} to {2}.", sourceDbName, sourceDbServerName, destinationDbServerName); - if (!WhatIf) - { - var sql = $"CREATE DATABASE {copyDbName} AS COPY OF {sourceDbServerName}.{sourceDbName}"; - dbExecutor.Execute(sql); - } - Log.Info("Copying {0} from {1} to {2}.", sourceDbName, sourceDbServerName, destinationDbServerName); - } - - private void WaitForBackupCopy( - SqlExecutor dbExecutor, - string destinationDbServerName, - string copyDbName) - { - var timeToGiveUp = DateTime.UtcNow.AddHours(1).AddSeconds(30); - while (DateTime.UtcNow < timeToGiveUp) - { - if (WhatIf || Util.DatabaseExistsAndIsOnline( - dbExecutor, - copyDbName)) - { - Log.Info("Copied {0} to {1}.", copyDbName, destinationDbServerName); - return; - } - - Log.Trace("Database {0} on {1} is not yet ready and online. Waiting for 5 minutes (will give up in {2} minutes).", copyDbName, destinationDbServerName, Math.Round(timeToGiveUp.Subtract(DateTime.UtcNow).TotalMinutes)); - Thread.Sleep(5 * 60 * 1000); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CopyExternalPackagesTask.cs b/src/NuGetGallery.Operations/Tasks/CopyExternalPackagesTask.cs deleted file mode 100644 index 8759365d77..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CopyExternalPackagesTask.cs +++ /dev/null @@ -1,70 +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.Data.SqlClient; -using System.IO; -using System.Net.Http; -using AnglicanGeek.DbExecutor; -using Microsoft.WindowsAzure.Storage; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("copyexternalpackages", "Copies the nupkg file of any packages that were using ExternalPackageUrl to blob storage, in preparation for deprecating the ExternalPackageUrl feature.", AltName = "cpxp", IsSpecialPurpose = true)] - public class CopyExternalPackagesTask : DatabaseAndStorageTask - { - public override void ExecuteCommand() - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - - var externalPackages = dbExecutor.Query(@" - SELECT pr.Id, p.Version, p.ExternalPackageUrl - FROM Packages p - JOIN PackageRegistrations pr ON pr.[Key] = p.PackageRegistrationKey - WHERE p.ExternalPackageUrl IS NOT NULL - ORDER BY Id, Version"); - - foreach (Package pkg in externalPackages) - { - Console.WriteLine(); - HttpClient client = new HttpClient(); - var responseTask = client.GetAsync(pkg.ExternalPackageUrl); - var response = responseTask.Result; - if (!response.IsSuccessStatusCode) - { - Console.WriteLine("Found broken package: " + response.StatusCode + " " + pkg.ExternalPackageUrl); - Console.WriteLine("You should ask the package owner to unlist the package " + pkg.Id + " " + pkg.Version); - } - - var bytesTask = response.Content.ReadAsByteArrayAsync(); - byte[] bytes = bytesTask.Result; - var blobClient = CreateBlobClient(); - var packagesBlobContainer = Util.GetPackagesBlobContainer(blobClient); - var packageFileBlob = Util.GetPackageFileBlob( - packagesBlobContainer, - pkg.Id, - pkg.Version); - var fileName = Util.GetPackageFileName( - pkg.Id, - pkg.Version); - if (packageFileBlob.Exists()) - { - Console.WriteLine(NuGetGallery.Strings.CopyExternalPackages_PackageFileBlobAlreadyExists, fileName); - } - else - { - Console.WriteLine(NuGetGallery.Strings.CopyExternalPackages_SavingPackageFileBlob, pkg.ExternalPackageUrl, fileName); - if (!WhatIf) - { - packageFileBlob.UploadFromStream( - new MemoryStream(bytes), - AccessCondition.GenerateIfNoneMatchCondition("*")); - } - } - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CreateWarehouseArtifactsTask.cs b/src/NuGetGallery.Operations/Tasks/CreateWarehouseArtifactsTask.cs deleted file mode 100644 index 71a9e891b4..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CreateWarehouseArtifactsTask.cs +++ /dev/null @@ -1,64 +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 NuGetGallery.Operations.Common; -using System; -using System.Collections.Generic; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("createwarehousedatabase", "Create warehouse artifacts", AltName = "cwdb", IsSpecialPurpose = true)] - public class CreateWarehouseArtifactsTask : WarehouseTask - { - [Option("Force recreation of the database artifacts", AltName = "f")] - public bool Force { get; set; } - - public override void ExecuteCommand() - { - Log.Info("Create warehouse artifacts"); - - AddTablesAndProcs(); - PrePopulateDimensions(); - - Log.Info("Warehouse artifacts successfully created"); - } - - void AddTablesAndProcs() - { - Log.Info("Adding Tables and Stored Procedures"); - - if (Force) - { - Log.Info("Dropping any existing tables."); - - ExecuteSqlBatch("NuGetGallery.Operations.Scripts.NuGetDownloadsDropTables.sql"); - } - - Log.Info("Creating tables."); - ExecuteSqlBatch("NuGetGallery.Operations.Scripts.NuGetDownloadsCreateTables.sql"); - - Log.Info("Creating stored functions and procedures."); - ExecuteSqlBatch("NuGetGallery.Operations.Scripts.NuGetDownloadsFuncs_UserAgent.sql"); - ExecuteSqlBatch("NuGetGallery.Operations.Scripts.NuGetDownloadsProcs_AddDownloadFact.sql"); - ExecuteSqlBatch("NuGetGallery.Operations.Scripts.NuGetDownloadsProcs_ConfirmPackageExported.sql"); - ExecuteSqlBatch("NuGetGallery.Operations.Scripts.NuGetDownloadsProcs_GetLastOriginalKey.sql"); - ExecuteSqlBatch("NuGetGallery.Operations.Scripts.NuGetDownloadsProcs_GetPackagesForExport.sql"); - } - - void PrePopulateDimensions() - { - Log.Info("Pre-populating Dimensions"); - - ExecuteSqlBatch("NuGetGallery.Operations.Scripts.PopulateDimensions.sql"); - } - - void ExecuteSqlBatch(string name) - { - IEnumerable batches = ResourceHelper.GetBatchesFromSqlFile(name); - foreach (string batch in batches) - { - SqlHelper.ExecuteBatch(ConnectionString.ConnectionString, batch); - } - } - } -} - diff --git a/src/NuGetGallery.Operations/Tasks/CreateWarehouseReportsTask.cs b/src/NuGetGallery.Operations/Tasks/CreateWarehouseReportsTask.cs deleted file mode 100644 index 84f121ed9a..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CreateWarehouseReportsTask.cs +++ /dev/null @@ -1,544 +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 System.Data; -using System.Data.SqlClient; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage.Blob; -using Newtonsoft.Json.Linq; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("createwarehousereports", "Create warehouse reports", AltName = "cwrep")] - public class CreateWarehouseReportsTask : DatabaseAndStorageTask - { - private const string JsonContentType = "application/json"; - private const string NuGetClientVersion = "nugetclientversion"; - private const string Last6Weeks = "last6weeks"; - private const string RecentPopularity = "recentpopularity"; - private const string RecentPopularityDetail = "recentpopularitydetail"; - private const string PackageReportDetailBaseName = "recentpopularitydetail_"; - - [Option("Re-create all reports", AltName = "all")] - public bool All { get; set; } - - public override void ExecuteCommand() - { - Log.Info("Generate reports begin"); - - CreateContainerIfNotExists(); - - CreateReport_NuGetClientVersion(); - CreateReport_Last6Weeks(); - CreateReport_RecentPopularityDetail(); - CreateReport_RecentPopularity(); - - if (All) - { - CreateAllPerPackageReports(); - } - else - { - CreateDirtyPerPackageReports(); - ClearInactivePackageReports(); - } - - Log.Info("Generate reports end"); - } - - private void CreateReport_NuGetClientVersion() - { - Log.Info("CreateReport_NuGetClientVersion"); - - Tuple> report = ExecuteSql("NuGetGallery.Operations.Scripts.DownloadReport_NuGetClientVersion.sql"); - - CreateBlob(NuGetClientVersion + ".json", JsonContentType, ReportHelpers.ToJson(report)); - } - - private void CreateReport_Last6Weeks() - { - Log.Info("CreateReport_Last6Weeks"); - - Tuple> report = ExecuteSql("NuGetGallery.Operations.Scripts.DownloadReport_Last6Weeks.sql"); - - CreateBlob(Last6Weeks + ".json", JsonContentType, ReportHelpers.ToJson(report)); - } - - private void CreateReport_RecentPopularityDetail() - { - Log.Info("CreateReport_RecentPopularityDetail"); - - Tuple> report = ExecuteSql("NuGetGallery.Operations.Scripts.DownloadReport_RecentPopularityDetail.sql"); - - CreateBlob(RecentPopularityDetail + ".json", JsonContentType, ReportHelpers.ToJson(report)); - } - - private void CreateReport_RecentPopularity() - { - Log.Info("CreateReport_RecentPopularity"); - - Tuple> report = ExecuteSql("NuGetGallery.Operations.Scripts.DownloadReport_RecentPopularity.sql"); - - CreateBlob(RecentPopularity + ".json", JsonContentType, ReportHelpers.ToJson(report)); - - CreatePerPackageReports(report); - } - - private void CreatePerPackageReports(Tuple> report) - { - Log.Info($"CreatePerPackageReports (count = {report.Item2.Count})"); - - int indexOfPackageId = 0; - foreach (string column in report.Item1) - { - if (column == "PackageId") - { - break; - } - indexOfPackageId++; - } - - if (indexOfPackageId == report.Item1.Length) - { - throw new InvalidOperationException("expected PackageId in result"); - } - - foreach (object[] row in report.Item2) - { - string packageId = row[indexOfPackageId].ToString(); - WithRetry(() => - { - CreatePackageReport(packageId); - }); - } - } - - private void CreateAllPerPackageReports() - { - Log.Info("CreateAllPerPackageReports"); - - DateTime before = DateTime.UtcNow; - - IList packageIds = GetAllPackageIds(); - - string[] bag = new string[packageIds.Count]; - - int index = 0; - foreach (string packageId in packageIds) - { - bag[index++] = packageId; - } - - ParallelOptions options = new ParallelOptions() { MaxDegreeOfParallelism = 4 }; - - Parallel.ForEach(bag, options, packageId => - { - WithRetry(() => - { - CreatePackageReport(packageId); - }); - }); - - string msg = $"CreateAllPerPackageReports complete {(DateTime.UtcNow - before).TotalSeconds} seconds"; - - Log.Info(msg); - } - - private IList GetAllPackageIds() - { - IList packageIds = new List(); - - using (SqlConnection connection = new SqlConnection(ConnectionString.ConnectionString)) - { - connection.Open(); - - SqlCommand command = new SqlCommand("SELECT DISTINCT packageId FROM Dimension_Package", connection); - command.CommandType = CommandType.Text; - command.CommandTimeout = 60 * 5; - - SqlDataReader reader = command.ExecuteReader(); - - while (reader.Read()) - { - string packageId = reader.GetValue(0).ToString(); - packageIds.Add(packageId); - } - } - - return packageIds; - } - - private void CreateDirtyPerPackageReports() - { - Log.Info("CreateDirtyPerPackageReports"); - - DateTime before = DateTime.UtcNow; - - IList> packageIds = GetPackageIds(); - - Log.Info($"Creating {packageIds.Count} Reports"); - - Tuple[] bag = new Tuple[packageIds.Count]; - - int index =0; - foreach (Tuple packageId in packageIds) - { - bag[index++] = packageId; - } - - // limit the potential concurrency becasue this is against SQL - - ParallelOptions options = new ParallelOptions() { MaxDegreeOfParallelism = 4 }; - - Parallel.ForEach(bag, options, packageId => - { - WithRetry(() => - { - CreatePackageReport(packageId.Item1); - - ConfirmExport(packageId); - }); - }); - - string msg = $"CreateDirtyPerPackageReports complete {(DateTime.UtcNow - before).TotalSeconds} seconds"; - - Log.Info(msg); - } - - private IList> GetPackageIds() - { - IList> packageIds = new List>(); - - using (SqlConnection connection = new SqlConnection(ConnectionString.ConnectionString)) - { - connection.Open(); - - SqlCommand command = new SqlCommand("GetPackagesForExport", connection); - command.CommandType = CommandType.StoredProcedure; - command.CommandTimeout = 60 * 5; - - SqlDataReader reader = command.ExecuteReader(); - - while (reader.Read()) - { - string packageId = reader.GetValue(0).ToString(); - int dirtyCount = (int)reader.GetValue(1); - - packageIds.Add(new Tuple(packageId, dirtyCount)); - } - } - - return packageIds; - } - - // for the initial release we will run New and Old reports in parallel - // (the difference is that new reports contain more details) - // then when we are happy with our new deployment we will drop the old - - private void CreatePackageReport(string packageId) - { - Log.Info($"CreatePackageReport for {packageId}"); - - // All blob names use lower case identifiers in the NuGet Gallery Azure Blob Storage - - string name = PackageReportDetailBaseName + packageId.ToLowerInvariant(); - - JObject report = CreateJsonContent(packageId); - - CreateBlob(name + ".json", JsonContentType, ReportHelpers.ToStream(report)); - } - - private JObject CreateJsonContent(string packageId) - { - Tuple> data = ExecuteSql("NuGetGallery.Operations.Scripts.DownloadReport_RecentPopularityDetailByPackage.sql", new Tuple("@packageId", 128, packageId)); - JObject content = MakeReportJson(data); - TotalDownloads(content); - SortItems(content); - return content; - } - - static JObject MakeReportJson(Tuple> data) - { - JObject report = new JObject(); - - report.Add("Downloads", 0); - - JObject items = new JObject(); - - foreach (object[] row in data.Item2) - { - string packageVersion = (string)row[0]; - - JObject childReport; - JToken token; - if (items.TryGetValue(packageVersion, out token)) - { - childReport = (JObject)token; - } - else - { - childReport = new JObject(); - childReport.Add("Downloads", 0); - childReport.Add("Items", new JArray()); - childReport.Add("Version", packageVersion); - - items.Add(packageVersion, childReport); - } - - JObject obj = new JObject(); - - if (row[1].ToString() == "NuGet" || row[1].ToString() == "WebMatrix") - { - obj.Add("Client", $"{row[2]} {row[3]}.{row[4]}"); - obj.Add("ClientName", row[2].ToString()); - obj.Add("ClientVersion", $"{row[3]}.{row[4]}"); - } - else - { - obj.Add("Client", row[2].ToString()); - obj.Add("ClientName", row[2].ToString()); - obj.Add("ClientVersion", ""); - } - - if (row[5].ToString() != "(unknown)") - { - obj.Add("Operation", row[5].ToString()); - } - - obj.Add("Downloads", (int)row[6]); - - ((JArray)childReport["Items"]).Add(obj); - } - - report.Add("Items", items); - - return report; - } - - private static int TotalDownloads(JObject report) - { - JToken token; - if (report.TryGetValue("Items", out token)) - { - if (token is JArray) - { - int total = 0; - for (int i = 0; i < ((JArray)token).Count; i++) - { - total += TotalDownloads((JObject)((JArray)token)[i]); - } - report["Downloads"] = total; - return total; - } - else - { - int total = 0; - foreach (KeyValuePair child in ((JObject)token)) - { - total += TotalDownloads((JObject)child.Value); - } - report["Downloads"] = total; - return total; - } - } - return (int)report["Downloads"]; - } - - private static void SortItems(JObject report) - { - List> scratch = new List>(); - - foreach (KeyValuePair child in ((JObject)report["Items"])) - { - scratch.Add(new Tuple((int)child.Value["Downloads"], new JObject((JObject)child.Value))); - } - - scratch.Sort((x, y) => { return x.Item1 == y.Item1 ? 0 : x.Item1 < y.Item1 ? 1 : -1; }); - - JArray items = new JArray(); - - foreach (Tuple item in scratch) - { - items.Add(item.Item2); - } - - report["Items"] = items; - } - - private void CreateEmptyPackageReport(string packageId) - { - Log.Info($"CreateEmptyPackageReport for {packageId}"); - - // All blob names use lower case identifiers in the NuGet Gallery Azure Blob Storage - - string name = PackageReportDetailBaseName + packageId.ToLowerInvariant(); - - CreateBlob(name + ".json", JsonContentType, ReportHelpers.ToStream(new JObject())); - } - - private void ClearInactivePackageReports() - { - Log.Info("ClearInactivePackageReports"); - - IList packageIds = GetInactivePackageIds(); - - Log.Info($"Creating {packageIds.Count} empty Reports"); - - string[] bag = new string[packageIds.Count]; - - int index = 0; - foreach (string packageId in packageIds) - { - bag[index++] = packageId; - } - - Parallel.ForEach(bag, packageId => - { - CreateEmptyPackageReport(packageId); - }); - } - - private IList GetInactivePackageIds() - { - string sql = ResourceHelper.GetBatchFromSqlFile("NuGetGallery.Operations.Scripts.DownloadReport_ListInactive.sql"); - - IList packageIds = new List(); - - using (SqlConnection connection = new SqlConnection(ConnectionString.ConnectionString)) - { - connection.Open(); - - SqlCommand command = new SqlCommand(sql, connection); - command.CommandType = CommandType.Text; - command.CommandTimeout = 60 * 5; - - SqlDataReader reader = command.ExecuteReader(); - - while (reader.Read()) - { - string packageId = reader.GetValue(0).ToString(); - packageIds.Add(packageId); - } - } - - return packageIds; - } - - private void ConfirmExport(Tuple packageId) - { - Log.Info($"ConfirmPackageExported for {packageId.Item1}"); - - using (SqlConnection connection = new SqlConnection(ConnectionString.ConnectionString)) - { - connection.Open(); - - SqlCommand command = new SqlCommand("ConfirmPackageExported", connection); - command.CommandType = CommandType.StoredProcedure; - command.CommandTimeout = 60 * 5; - command.Parameters.AddWithValue("PackageId", packageId.Item1); - command.Parameters.AddWithValue("DirtyCount", packageId.Item2); - - command.ExecuteNonQuery(); - } - } - - private Tuple> ExecuteSql(string filename, params Tuple[] parameters) - { - string sql = ResourceHelper.GetBatchFromSqlFile(filename); - - List rows = new List(); - string[] columns; - - using (SqlConnection connection = new SqlConnection(ConnectionString.ConnectionString)) - { - connection.Open(); - - SqlCommand command = new SqlCommand(sql, connection); - command.CommandType = CommandType.Text; - command.CommandTimeout = 60 * 5; - - foreach (Tuple parameter in parameters) - { - command.Parameters.Add(parameter.Item1, SqlDbType.NVarChar, parameter.Item2).Value = parameter.Item3; - } - - SqlDataReader reader = command.ExecuteReader(); - - columns = new string[reader.FieldCount]; - - for (int i = 0; i < reader.FieldCount; i++) - { - columns[i] = reader.GetName(i); - } - - while (reader.Read()) - { - object[] row = new object[reader.FieldCount]; - - for (int i = 0; i < reader.FieldCount; i++) - { - row[i] = reader.IsDBNull(i) ? null : reader.GetValue(i); - } - - rows.Add(row); - } - } - - return new Tuple>(columns, rows); - } - - private void CreateContainerIfNotExists() - { - CloudBlobClient blobClient = StorageAccount.CreateCloudBlobClient(); - CloudBlobContainer container = blobClient.GetContainerReference("stats"); - - container.CreateIfNotExists(); // this can throw if the container was just deleted a few seconds ago - - container.SetPermissions(new BlobContainerPermissions { PublicAccess = BlobContainerPublicAccessType.Blob }); - } - - private Uri CreateBlob(string name, string contentType, Stream content) - { - CloudBlobClient blobClient = StorageAccount.CreateCloudBlobClient(); - CloudBlobContainer container = blobClient.GetContainerReference("stats"); - CloudBlockBlob blockBlob = container.GetBlockBlobReference("popularity/" + name); - - blockBlob.Properties.ContentType = contentType; - blockBlob.UploadFromStream(content); - - return blockBlob.Uri; - } - - private void WithRetry(Action action) - { - int attempts = 10; - - while (attempts-- > 0) - { - try - { - action(); - break; - } - catch (Exception) - { - if (attempts == 1) - { - throw; - } - else - { - SqlConnection.ClearAllPools(); - Log.Info($"Retry attempts remaining {attempts}"); - Thread.Sleep(20 * 1000); - } - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/AddCuratedFeedManagerTask.cs b/src/NuGetGallery.Operations/Tasks/CuratedFeeds/AddCuratedFeedManagerTask.cs deleted file mode 100644 index 72870044b7..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/AddCuratedFeedManagerTask.cs +++ /dev/null @@ -1,63 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks.CuratedFeeds -{ - [Command("addcuratedfeedmanager", "Adds an existing user as a manager to an existing feed", AltName = "acfm")] - public class AddCuratedFeedManagerTask : DatabaseTask - { - [Option("The name of the feed", AltName = "f")] - public string FeedName { get; set; } - - [Option("The name of the user", AltName = "u")] - public string UserName { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - ArgCheck.Required(FeedName, "FeedName"); - ArgCheck.Required(UserName, "UserName"); - } - - public override void ExecuteCommand() - { - WithConnection((connection, db) => - { - Log.Info("Adding {0} as manager of {1}", UserName, FeedName); - - string results = db.Query(@"BEGIN TRAN - IF NOT EXISTS ( - SELECT * - FROM CuratedFeedManagers cfm - JOIN Users u ON u.[Key] = cfm.UserKey - JOIN CuratedFeeds cf ON cf.[Key] = cfm.CuratedFeedKey - WHERE cf.Name = @FeedName - AND u.Username = @UserName - ) - INSERT INTO CuratedFeedManagers(CuratedFeedKey, UserKey) - OUTPUT 'success' - SELECT - (SELECT [Key] FROM CuratedFeeds WHERE Name = @FeedName) AS CuratedFeedKey, - (SELECT [Key] FROM Users WHERE Username = @UserName) AS UserKey - " + (WhatIf ? "ROLLBACK TRAN" : "COMMIT TRAN"), - new { FeedName, UserName }).FirstOrDefault(); - - if (results == "success") - { - Log.Info("Added {0} as manager of {1}", UserName, FeedName); - } - else - { - Log.Warn("{0} is already a manager of {1}", UserName, FeedName); - } - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/CopyCuratedFeedTask.cs b/src/NuGetGallery.Operations/Tasks/CuratedFeeds/CopyCuratedFeedTask.cs deleted file mode 100644 index 06577a6ab4..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/CopyCuratedFeedTask.cs +++ /dev/null @@ -1,163 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks.CuratedFeeds -{ - [Command("copycuratedfeed", "Copies all the content of a curated feed to a new feed with a different name", AltName="ccf")] - public class CopyCuratedFeedTask : DatabaseTask - { - // Queries - private const string UpdateManagersBaseQuery = @" - DECLARE @newId int - SELECT @newId = [Key] FROM CuratedFeeds WHERE Name = @DestinationFeed - - DECLARE @results TABLE ( - [Action] nvarchar(10), - CuratedFeedKey int, - UserKey int - ) - - MERGE INTO CuratedFeedManagers AS t - USING ( - SELECT @newId, cfm.UserKey - FROM CuratedFeedManagers cfm - JOIN CuratedFeeds cf ON cfm.CuratedFeedKey = cf.[Key] - WHERE cf.Name = @SourceFeed) AS s(CuratedFeedKey, UserKey) - ON (s.CuratedFeedKey = t.CuratedFeedKey AND s.UserKey = t.UserKey) - WHEN NOT MATCHED BY TARGET - THEN INSERT(CuratedFeedKey, UserKey) VALUES(s.CuratedFeedKey, s.UserKey) - OUTPUT $action, inserted.CuratedFeedKey, inserted.UserKey INTO @results; - - SELECT r.*, cf.Name, u.Username - FROM @results r - JOIN CuratedFeeds cf ON cf.[Key] = r.CuratedFeedKey - JOIN Users u ON u.[Key] = r.UserKey"; - private const string UpdateManagersWhatIfQuery = @"BEGIN TRAN -" + UpdateManagersBaseQuery + @" -ROLLBACK TRAN"; - private const string UpdateManagersRealQuery = @"BEGIN TRAN -" + UpdateManagersBaseQuery + @" -COMMIT TRAN"; - - private const string UpdatePackagesBaseQuery = @" - DECLARE @newId int - SELECT @newId = [Key] FROM CuratedFeeds WHERE Name = @DestinationFeed - - DECLARE @results TABLE ( - [Action] nvarchar(10), - CuratedFeedKey int, - PackageRegistrationKey int - ) - - MERGE INTO CuratedPackages AS t - USING ( - SELECT @newId, cp.Notes, cp.PackageRegistrationKey, cp.AutomaticallyCurated, cp.Included - FROM CuratedPackages cp - JOIN CuratedFeeds cf ON cp.CuratedFeedKey = cf.[Key] - WHERE cf.Name = @SourceFeed) AS s(CuratedFeedKey, Notes, PackageRegistrationKey, AutomaticallyCurated, Included) - ON (s.CuratedFeedKey = t.CuratedFeedKey AND s.PackageRegistrationKey = t.PackageRegistrationKey) - WHEN NOT MATCHED BY TARGET - THEN INSERT(CuratedFeedKey, Notes, PackageRegistrationKey, AutomaticallyCurated, Included) VALUES(s.CuratedFeedKey, s.Notes, s.PackageRegistrationKey, s.AutomaticallyCurated, s.Included) - OUTPUT $action AS [Action], (CASE $action - WHEN 'DELETE' THEN deleted.CuratedFeedKey - ELSE inserted.CuratedFeedKey - END) AS CuratedFeedKey, (CASE $action - WHEN 'DELETE' THEN deleted.PackageRegistrationKey - ELSE inserted.PackageRegistrationKey - END) AS PackageRegistrationKey INTO @results; - - SELECT r.*, pr.Id, cf.Name - FROM @results r - JOIN PackageRegistrations pr ON r.[PackageRegistrationKey] = pr.[Key] - JOIN CuratedFeeds cf ON r.CuratedFeedKey = cf.[Key]"; - private const string UpdatePackagesWhatIfQuery = @"BEGIN TRAN -" + UpdatePackagesBaseQuery + @" -ROLLBACK TRAN"; - private const string UpdatePackagesRealQuery = @"BEGIN TRAN -" + UpdatePackagesBaseQuery + @" -COMMIT TRAN"; - - - [Option("The name of the source feed", AltName = "s")] - public string SourceFeed { get; set; } - - [Option("The name of the destination feed", AltName = "d")] - public string DestinationFeed { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - ArgCheck.Required(SourceFeed, "SourceFeed"); - ArgCheck.Required(DestinationFeed, "DestinationFeed"); - } - - public override void ExecuteCommand() - { - WithConnection((connection, db) => - { - // Check for the source feed - int count = db.Execute( - "SELECT COUNT(*) FROM CuratedFeeds WHERE Name = @name", - new { name = SourceFeed }); - if (count == 0) - { - Log.Error("Source Feed '{0}' does not exist", SourceFeed); - return; - } - - // Check for the destination feed - Log.Info("Ensuring Feed '{0}' exists", DestinationFeed); - db.Execute(@" - IF NOT EXISTS (SELECT * FROM CuratedFeeds WHERE Name = @name) - INSERT INTO CuratedFeeds(Name) VALUES(@name)", - new { name = DestinationFeed }); - - // Update Managers - var param = new { SourceFeed, DestinationFeed }; - Log.Info("Updating Managers"); - var managerResults = db.Query( - WhatIf ? UpdateManagersWhatIfQuery : UpdateManagersRealQuery, - param); - foreach (var managerResult in managerResults) - { - Log.Info("{2} {0} {1}", managerResult.Action, managerResult.Username, managerResult.Name); - } - - // Update Packages - Log.Info("Updating Packages"); - var packageResults = db.Query( - WhatIf ? UpdatePackagesWhatIfQuery : UpdatePackagesRealQuery, - param); - foreach (var packageResult in packageResults) - { - Log.Info("{2} {0} {1}", packageResult.Action, packageResult.Id, packageResult.Name); - } - }); - } - - public class UpdateManagerResult - { - public string Action { get; set; } - public int CuratedFeedKey { get; set; } - public int UserKey { get; set; } - public string Username { get; set; } - public string Name { get; set; } - } - - public class UpdatePackageResult - { - public string Action { get; set; } - public int CuratedFeedKey { get; set; } - public int PackageRegistrationKey { get; set; } - public string Id { get; set; } - public string Name { get; set; } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/CreateCuratedFeed.cs b/src/NuGetGallery.Operations/Tasks/CuratedFeeds/CreateCuratedFeed.cs deleted file mode 100644 index e7646f44ef..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/CreateCuratedFeed.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 System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations.Tasks.CuratedFeeds -{ - [Command("createcuratedfeed", "Creates a new, empty, curated feed", AltName = "ncf", MinArgs=1, MaxArgs=1)] - public class CreateCuratedFeedTask : DatabaseTask - { - public override void ExecuteCommand() - { - WithConnection((connection, db) => - { - Log.Info("Creating Curated Feed: {0}", Arguments[0]); - - string results; - if(!WhatIf) { - results = db.Query(@" - IF NOT EXISTS (SELECT * FROM CuratedFeeds WHERE Name = @Name) - INSERT INTO CuratedFeeds(Name) - OUTPUT inserted.Name - VALUES(@Name)", - new { Name = Arguments[0] }).FirstOrDefault(); - } else { - results = Arguments[0]; - } - - if (results != null) - { - Log.Info("Created Curated Feed: {0}", results); - } - else - { - Log.Warn("Curated Feed already exists: {0}", Arguments[0]); - } - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/CurateWebmatrixPackagesTask.cs b/src/NuGetGallery.Operations/Tasks/CuratedFeeds/CurateWebmatrixPackagesTask.cs deleted file mode 100644 index 8ca924c666..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/CurateWebmatrixPackagesTask.cs +++ /dev/null @@ -1,170 +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 System.Data.SqlClient; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AnglicanGeek.DbExecutor; -using NuGet.Packaging; - -namespace NuGetGallery.Operations.CuratedFeeds -{ - [Command("curatewebmatrix", "Runs the WebMatrix Curator on the specified storage server", AltName = "cwm", IsSpecialPurpose = true)] - public class CurateWebmatrixPackagesTask : DatabaseAndStorageTask - { - private readonly string _tempFolder; - - public CurateWebmatrixPackagesTask() - { - _tempFolder = Path.Combine(Path.GetTempPath(), "NuGetGalleryOps"); - Directory.CreateDirectory(_tempFolder); - } - - public override void ExecuteCommand() - { - Log.Trace("Getting latest packages..."); - var packages = GetLatestStablePackages(); - Log.Trace("Getting previously curated packages..."); - var alreadyCuratedPackageIds = GetAlreadyCuratedPackageIds(); - Log.Trace("Calculating minimum difference set..."); - var packageIdsToCurate = packages.Keys.Except(alreadyCuratedPackageIds).ToList(); - - var totalCount = packageIdsToCurate.Count; - var processedCount = 0; - Log.Trace( - "Curating {0} packages for the WebMatrix curated on '{1}',", - totalCount, - ConnectionString); - - Parallel.ForEach(packageIdsToCurate, new ParallelOptions { MaxDegreeOfParallelism = 10 }, packageIdToCurate => - { - var package = packages[packageIdToCurate]; - - try - { - var downloadPath = DownloadPackage(package); - - bool shouldBeIncluded; - using (var nugetPackage = new PackageArchiveReader(File.OpenRead(downloadPath))) - { - var nuspecReader = nugetPackage.GetNuspecReader(); - var metadata = nuspecReader.GetMetadata() - .ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase); - - string tags; - shouldBeIncluded = metadata.TryGetValue("tags", out tags) && tags.ToLowerInvariant().Contains("aspnetwebpages"); - - if (!shouldBeIncluded) - { - shouldBeIncluded = true; - foreach (var file in nugetPackage.GetFiles()) - { - var fi = new FileInfo(file); - if (fi.Extension == ".ps1" || fi.Extension == ".t4") - { - shouldBeIncluded = false; - break; - } - } - } - - if (shouldBeIncluded) - { - AddPackageToCuratedFeed(package); - } - } - - File.Delete(downloadPath); - - Interlocked.Increment(ref processedCount); - Log.Info( - "{2} package '{0}.{1}' ({3} of {4}).", - package.Id, - package.Version, - shouldBeIncluded ? "Curated" : "Ignored", - processedCount, - totalCount); - } - catch(Exception ex) - { - Interlocked.Increment(ref processedCount); - Log.Error( - "Error curating package '{0}.{1}' ({2} of {3}): {4}.", - package.Id, - package.Version, - processedCount, - totalCount, - ex.Message); - } - }); - } - - string DownloadPackage(Package package) - { - var cloudClient = CreateBlobClient(); - - var packagesBlobContainer = Util.GetPackagesBlobContainer(cloudClient); - - var packageFileName = Util.GetPackageFileName(package.Id, package.Version); - - var downloadPath = Path.Combine(_tempFolder, packageFileName); - - var blob = packagesBlobContainer.GetBlockBlobReference(packageFileName); - blob.DownloadToFile(downloadPath); - - return downloadPath; - } - - IEnumerable GetAlreadyCuratedPackageIds() - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - return dbExecutor.Query(@" - SELECT pr.Id - FROM CuratedPackages cp - JOIN CuratedFeeds cf ON cf.[Key] = cp.CuratedFeedKey - JOIN PackageRegistrations pr on pr.[Key] = cp.PackageRegistrationKey - WHERE cf.Name = @name", new { name = "webmatrix" }); - } - } - - IDictionary GetLatestStablePackages() - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - var packages = dbExecutor.Query(@" - SELECT pr.Id, p.Version, p.Hash - FROM Packages p - JOIN PackageRegistrations pr on pr.[Key] = p.PackageRegistrationKey - WHERE p.IsLatestStable = 1"); - return packages.ToDictionary(p => p.Id); - } - } - - void AddPackageToCuratedFeed(Package package) - { - if (!WhatIf) - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - dbExecutor.Execute(@" - INSERT INTO CuratedPackages - (CuratedFeedKey, PackageRegistrationKey, AutomaticallyCurated, Included) - VALUES - ((SELECT [Key] FROM CuratedFeeds WHERE Name = 'webmatrix'), (SELECT [Key] FROM PackageRegistrations WHERE Id = @id), @automaticallyCurated, @included)", - new { id = package.Id, automaticallyCurated = true, included = true }); - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/DeleteCuratedFeedTask.cs b/src/NuGetGallery.Operations/Tasks/CuratedFeeds/DeleteCuratedFeedTask.cs deleted file mode 100644 index f8be3a0ee9..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/DeleteCuratedFeedTask.cs +++ /dev/null @@ -1,44 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations.Tasks.CuratedFeeds -{ - [Command("deletecuratedfeed", "Deletes ALL data for a curated feed", AltName = "dcf", MinArgs = 1, MaxArgs = 1)] - public class DeleteCuratedFeedTask : DatabaseTask - { - public override void ExecuteCommand() - { - WithConnection((connection, db) => - { - Log.Info("Deleting Curated Packages in: {0}", Arguments[0]); - int results = db.Execute(@"BEGIN TRAN - DELETE FROM CuratedPackages - WHERE [CuratedFeedKey] = (SELECT [Key] FROM CuratedFeeds WHERE Name = @Name) - " + (WhatIf ? "ROLLBACK TRAN" : "COMMIT TRAN"), - new { Name = Arguments[0] }); - Log.Info("Deleted {0} packages", results); - - Log.Info("Deleting Curated Feed Managers for: {0}", Arguments[0]); - results = db.Execute(@"BEGIN TRAN - DELETE FROM CuratedFeedManagers - WHERE [CuratedFeedKey] = (SELECT [Key] FROM CuratedFeeds WHERE Name = @Name) - " + (WhatIf ? "ROLLBACK TRAN" : "COMMIT TRAN"), - new { Name = Arguments[0] }); - Log.Info("Deleted {0} managers", results); - - Log.Info("Deleting Curated Feed: {0}", Arguments[0]); - results = db.Execute(@"BEGIN TRAN - DELETE FROM CuratedFeeds - WHERE Name = @Name - " + (WhatIf ? "ROLLBACK TRAN" : "COMMIT TRAN"), - new { Name = Arguments[0] }); - Log.Info("Deleted {0} feeds", results); - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/ListCuratedFeedManagersTask.cs b/src/NuGetGallery.Operations/Tasks/CuratedFeeds/ListCuratedFeedManagersTask.cs deleted file mode 100644 index e5948fcbfd..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/ListCuratedFeedManagersTask.cs +++ /dev/null @@ -1,33 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations.Tasks.CuratedFeeds -{ - [Command("listcuratedfeedmanagers", "Lists the managers for a curated feed", AltName = "lcfm", MinArgs = 1, MaxArgs = 1)] - public class ListCuratedFeedManagersTask : DatabaseTask - { - public override void ExecuteCommand() - { - WithConnection((connection, db) => - { - Log.Info("Managers of {0}:", Arguments[0]); - var results = db.Query(@" - SELECT u.Username - FROM CuratedFeedManagers cfm - JOIN CuratedFeeds cf ON cfm.CuratedFeedKey = cf.[Key] - JOIN Users u ON cfm.UserKey = u.[Key] - WHERE cf.Name = @Name", - new { Name = Arguments[0] }); - foreach (var manager in results) - { - Log.Info("* {0}", manager); - } - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/ListCuratedFeedsTask.cs b/src/NuGetGallery.Operations/Tasks/CuratedFeeds/ListCuratedFeedsTask.cs deleted file mode 100644 index 6e105e3e81..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/ListCuratedFeedsTask.cs +++ /dev/null @@ -1,26 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations.Tasks.CuratedFeeds -{ - [Command("listcuratedfeeds", "Lists all available curated feeds", AltName = "lcf")] - public class ListCuratedFeedTask : DatabaseTask - { - public override void ExecuteCommand() - { - WithConnection((connection, db) => - { - Log.Info("Curated Feeds:"); - foreach (var feed in db.Query("SELECT Name FROM CuratedFeeds")) - { - Log.Info("* {0}", feed); - } - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/RemoveCuratedFeedManagerTask.cs b/src/NuGetGallery.Operations/Tasks/CuratedFeeds/RemoveCuratedFeedManagerTask.cs deleted file mode 100644 index a07ee1a007..0000000000 --- a/src/NuGetGallery.Operations/Tasks/CuratedFeeds/RemoveCuratedFeedManagerTask.cs +++ /dev/null @@ -1,56 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks.CuratedFeeds -{ - [Command("removecuratedfeedmanager", "Removes an existing manager from a curated feed", AltName = "rcfm")] - public class RemoveCuratedFeedManagerTask : DatabaseTask - { - [Option("The name of the feed", AltName = "f")] - public string FeedName { get; set; } - - [Option("The name of the user", AltName = "u")] - public string UserName { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - ArgCheck.Required(FeedName, "FeedName"); - ArgCheck.Required(UserName, "UserName"); - } - - public override void ExecuteCommand() - { - WithConnection((connection, db) => - { - Log.Info("Removing {0} from {1}", UserName, FeedName); - - int results = db.Execute(@"BEGIN TRAN - DELETE CuratedFeedManagers - FROM CuratedFeedManagers AS cfm - JOIN Users u ON u.[Key] = cfm.UserKey - JOIN CuratedFeeds cf ON cf.[Key] = cfm.CuratedFeedKey - WHERE cf.Name = @FeedName - AND u.Username = @UserName - " + (WhatIf ? "ROLLBACK TRAN" : "COMMIT TRAN"), - new { FeedName, UserName }); - - if (results > 0) - { - Log.Info("Removed {0} from {1}", UserName, FeedName); - } - else - { - Log.Warn("{0} is not a manager of {1}", UserName, FeedName); - } - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/DataManagement/DeleteDuplicatePackageVersionsTask.cs b/src/NuGetGallery.Operations/Tasks/DataManagement/DeleteDuplicatePackageVersionsTask.cs deleted file mode 100644 index 256b573060..0000000000 --- a/src/NuGetGallery.Operations/Tasks/DataManagement/DeleteDuplicatePackageVersionsTask.cs +++ /dev/null @@ -1,161 +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 System.Data.SqlClient; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Mail; -using System.Text; -using System.Threading.Tasks; -using AnglicanGeek.DbExecutor; -using Microsoft.WindowsAzure.Storage; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("deleteduplicatepackageversions", "Deletes Duplicate Package Versions", AltName = "ddpv", IsSpecialPurpose = true)] - public class DeleteDuplicatePackageVersionsTask : DatabaseAndStorageTask - { - [Option("Storage account in which to place audit records and backups, usually provided by the environment")] - public CloudStorageAccount BackupStorage { get; set; } - - [Option("Set this flag to write the deletion audit record ONLY and not proceed with the deletion itself")] - public bool AuditOnly { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (BackupStorage == null && CurrentEnvironment != null) - { - BackupStorage = CurrentEnvironment.BackupStorage; - } - ArgCheck.RequiredOrConfig(BackupStorage, "BackupStorage"); - } - public override void ExecuteCommand() - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - - // Query for packages - Log.Info("Gathering list of packages..."); - var packages = dbExecutor.Query(@" - SELECT p.[Key], p.PackageRegistrationKey, r.Id, p.Version, p.Hash, p.LastUpdated, p.Published, p.Listed, p.IsLatestStable - FROM Packages p - INNER JOIN PackageRegistrations r ON p.PackageRegistrationKey = r.[Key]"); - - // Group by Id and and SemVer - Log.Info("Grouping by Package ID and Actual Version..."); - var groups = packages.GroupBy(p => new { p.Id, Version = NuGetVersionNormalizer.Normalize(p.Version) }); - - // Find any groups with more than one entry - Log.Info("Finding Duplicates..."); - var dups = groups.Where(g => g.Count() > 1); - - // Print them out - int dupsUnlistedCount = 0; - int latestCount = 0; - foreach (var dup in dups) - { - ProcessDuplicate(dup.Key.Id, dup.Key.Version, dup.ToList(), ref dupsUnlistedCount, ref latestCount); - } - var totalDupes = dups.Count(); - Log.Info("Found {0} Packages with duplicates.", totalDupes); - Log.Info(" {0} of them have no listed duplicates.", dupsUnlistedCount); - Log.Info(" {0} of them have multiple listed duplicates.", totalDupes - dupsUnlistedCount); - if (latestCount > 0) - { - Log.Warn(" {0} of them are the latest version of the relevant package", latestCount); - } - else - { - Log.Info(" NONE of them are the latest version of the relevant package"); - } - } - } - - private void ProcessDuplicate(string id, string normalVersion, List packages, ref int unlistedCount, ref int latestCount) - { - // Are any of these the latest version? - var latest = packages.Where(p => p.Latest).ToList(); - if (latest.Count > 0) - { - latestCount++; - Log.Error("Unable to process: {0}@{1}, it is the latest version of {0}", id, normalVersion); - } - else - { - // Is there only one listed version? - var listed = packages.Where(p => p.Listed).ToList(); - if (listed.Count == 1) - { - unlistedCount++; - Log.Info("Cleaning {0}@{1} by removing unlisted versions", id, normalVersion); - foreach (var package in packages.Where(p => !p.Listed)) - { - Log.Trace("Deleting {0}@{1}...", package.Id, package.Version); - DeletePackageVersion(package, "unlisted duplicate"); - } - } - else - { - // Select the most recent pacakge - var selected = packages.OrderByDescending(p => p.Published).FirstOrDefault(); - if (selected == null) - { - Log.Error("Weird. There wasn't a most recent upload of {0}@{1}?", id, normalVersion); - } - else - { - Log.Info("Cleaning {0}@{1} by removing older duplicate versions", id, normalVersion); - foreach (var package in packages.OrderByDescending(p => p.Published).Skip(1)) - { - Log.Trace("Deleting {0}@{1}...", package.Id, package.Version); - DeletePackageVersion(package, "older duplicate"); - } - } - } - } - } - - private void DeletePackageVersion(PackageSummary package, string subreason) - { - new DeletePackageVersionTask() - { - BackupStorage = BackupStorage, - StorageAccount = StorageAccount, - ConnectionString = ConnectionString, - AuditOnly = AuditOnly, - WhatIf = WhatIf, - PackageId = package.Id, - PackageVersion = package.Version, - Reason = $"duplicate package versions ({subreason})" - }.Execute(); - } - } - - public class PackageOwner - { - public string Username { get; set; } - public string EmailAddress { get; set; } - } - - public class PackageSummary - { - public int Key { get; set; } - public int PackageRegistrationKey { get; set; } - public string Id { get; set; } - public string Version { get; set; } - public string Hash { get; set; } - public bool Listed { get; set; } - public bool Latest { get; set; } - public DateTime LastUpdated { get; set; } - public DateTime Published { get; set; } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/DataManagement/NormalizePackageFilesTask.cs b/src/NuGetGallery.Operations/Tasks/DataManagement/NormalizePackageFilesTask.cs deleted file mode 100644 index 4b5ff0ccb9..0000000000 --- a/src/NuGetGallery.Operations/Tasks/DataManagement/NormalizePackageFilesTask.cs +++ /dev/null @@ -1,86 +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 System.Data; -using System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage.Blob; - -namespace NuGetGallery.Operations.Tasks.DataManagement -{ - [Command("normalizepackagefiles", "Copies package files to a file name based on their normalized package version", AltName = "npf")] - public class NormalizePackageFilesTask : DatabaseAndStorageTask - { - public override void ExecuteCommand() - { - WithConnection((c, db) => - { - Log.Trace("Collecting list of packages..."); - var packages = db.Query(@" - SELECT pr.Id, p.[Key], p.Version, p.NormalizedVersion - FROM Packages p - INNER JOIN PackageRegistrations pr ON pr.[Key] = p.PackageRegistrationKey - WHERE p.NormalizedVersion IS NOT NULL AND p.Version != p.NormalizedVersion") - .ToList(); - Log.Trace("Collected {0} packages to normalize", packages.Count); - - // Check for duplicates - Log.Trace("Scanning for duplicate data..."); - var dupes = packages - .GroupBy(p => Tuple.Create(p.Id, p.NormalizedVersion)) - .Where(g => g.Count() > 1) - .ToList(); - Log.Trace("Found {0} dupes:", dupes.Count); - foreach (var dupe in dupes) - { - Log.Debug(" * {0} {1}: {2}", dupe.Key.Item1, dupe.Key.Item2, String.Join(", ", dupe.Select(p => p.Version))); - } - var deduped = packages.Except(dupes.SelectMany(g => g)).ToList(); - Log.Trace("Ignoring dupes, {0} remaining", deduped.Count); - - // Copy packages - var blobs = CreateBlobClient(); - var container = blobs.GetContainerReference("packages"); - var copyTargets = new List(); - int counter = 0; - foreach (var package in deduped) - { - var blob = Util.GetPackageFileBlob(container, package.Id, package.Version); - if (blob.Exists()) - { - var normalizedBlob = Util.GetPackageFileBlob(container, package.Id, package.NormalizedVersion); - if (normalizedBlob.Exists()) - { - Log.Warn("Normalized Blob exists: {0}", normalizedBlob.Name); - } - else - { - if (!WhatIf) - { - normalizedBlob.StartCopyFromBlob(blob); - copyTargets.Add(normalizedBlob); - } - Log.Info("[{2}] {0} => {1}", blob.Name, normalizedBlob.Name, Util.GenerateStatusString(deduped.Count, ref counter)); - } - } - } - Log.Info("Copies started. Waiting for completion"); - - if (!WhatIf) - { - foreach (var copyTarget in copyTargets) - { - do - { - copyTarget.FetchAttributes(); - } while (copyTarget.CopyState.Status == CopyStatus.Pending); - Log.Info("{0} done.", copyTarget.Name); - } - } - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/DataManagement/NormalizePackageVersionsTask.cs b/src/NuGetGallery.Operations/Tasks/DataManagement/NormalizePackageVersionsTask.cs deleted file mode 100644 index 5c8185ec7a..0000000000 --- a/src/NuGetGallery.Operations/Tasks/DataManagement/NormalizePackageVersionsTask.cs +++ /dev/null @@ -1,93 +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 System.Data; -using System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations.Tasks.DataManagement -{ - [Command("normalizepackageversions", "Sets the NormalizedVersion column for packages which do not have a value for that column", AltName = "npv")] - public class NormalizePackageVersionsTask : DatabaseTask - { - private const string UpdateQuery = @" - UPDATE Packages - SET NormalizedVersion = ut.NormalizedVersion - OUTPUT - pr.Id, - INSERTED.[Version], - INSERTED.NormalizedVersion - FROM @updateTable ut - INNER JOIN Packages p ON ut.PackageKey = p.[Key] - INNER JOIN PackageRegistrations pr ON p.PackageRegistrationKey = pr.[Key]"; - private const string CommitQuery = @"BEGIN TRAN - " + UpdateQuery + @" - COMMIT TRAN"; - private const string WhatIfQuery = @"BEGIN TRAN - " + UpdateQuery + @" - ROLLBACK TRAN"; - - public override void ExecuteCommand() - { - WithConnection((c, db) => - { - Log.Trace("Collecting list of packages..."); - var packages = db.Query(@" - SELECT pr.Id, p.[Key], p.Version - FROM Packages p - INNER JOIN PackageRegistrations pr ON pr.[Key] = p.PackageRegistrationKey - WHERE p.NormalizedVersion IS NULL") - .ToList(); - Log.Trace("Collected {0} packages", packages.Count); - - DataTable output; - int count = 0; - WithTableType(c, "Temp_NormalizePackageVersionsInputType", "PackageKey int, NormalizedVersion nvarchar(64)", () => - { - // Build a table to hold the new data - var updateTable = new DataTable(); - updateTable.Columns.Add(new DataColumn("PackageKey", typeof(int))); - updateTable.Columns.Add(new DataColumn("NormalizedVersion", typeof(string))); - foreach (var package in packages) - { - string normalized = NuGetVersionNormalizer.Normalize(package.Version); - var row = updateTable.NewRow(); - row.SetField("PackageKey", package.Key); - row.SetField("NormalizedVersion", normalized); - updateTable.Rows.Add(row); - } - - // Run the query with the table parameter - var cmd = c.CreateCommand(); - cmd.CommandType = CommandType.Text; - cmd.CommandText = WhatIf ? WhatIfQuery : CommitQuery; - cmd.Parameters.Add(new SqlParameter("@updateTable", SqlDbType.Structured) - { - TypeName = "Temp_NormalizePackageVersionsInputType", - Value = updateTable - }); - Log.Trace("Updating Database..."); - var reader = cmd.ExecuteReader(); - Log.Trace("Database Update Complete"); - - // Load the results into a datatable and render them - output = new DataTable(); - output.Load(reader); - foreach (var row in output.Rows.Cast()) - { - string version = row.Field("Version"); - string normalized = row.Field("NormalizedVersion"); - if (!String.Equals(version, normalized, StringComparison.Ordinal)) - { - count++; - } - } - Log.Info("Updated {0} packages", count); - }); - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/DataManagement/PopulateCredentialsTask.cs b/src/NuGetGallery.Operations/Tasks/DataManagement/PopulateCredentialsTask.cs deleted file mode 100644 index c4c979256e..0000000000 --- a/src/NuGetGallery.Operations/Tasks/DataManagement/PopulateCredentialsTask.cs +++ /dev/null @@ -1,95 +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 System.Data; -using Dapper; -using NuGetGallery.Operations.Infrastructure; - -namespace NuGetGallery.Operations.Tasks.DataManagement -{ - [Command("populatecredentials", "Populates the Credentials table for users who are missing data", AltName = "pc")] - public class PopulateCredentialsTask : DatabaseTask - { - private const string WhatIfQuery = "BEGIN TRAN\r\n" + Query + "\r\nROLLBACK TRAN"; - private const string CommitQuery = "BEGIN TRAN\r\n" + Query + "\r\nCOMMIT TRAN"; - - private const string Query = @" - DECLARE @results TABLE( - Action nchar(10), - UserKey int, - Type nvarchar(64), - Value nvarchar(256) - ) - - MERGE INTO Credentials dest - USING @creds src - ON src.UserKey = dest.UserKey AND src.Type = dest.Type - WHEN NOT MATCHED THEN - INSERT(UserKey, Type, Value) - VALUES(src.UserKey, src.Type, src.Value) - OUTPUT - $action AS 'Action', - inserted.UserKey, - inserted.Type, - inserted.Value - INTO @results; - - SELECT COUNT(*) FROM @results -"; - - private readonly Dictionary _hashAlgorithmToCredType = new Dictionary { - {Constants.PBKDF2HashAlgorithmId, CredentialTypes.Password.Pbkdf2}, - {Constants.Sha1HashAlgorithmId, CredentialTypes.Password.Sha1} - }; - - public override void ExecuteCommand() - { - WithConnection(c => - { - // Get user credentials - var users = c.Query("SELECT [Key], HashedPassword, PasswordHashAlgorithm, ApiKey FROM Users"); - - // Build a table - var dt = new DataTable(); - dt.Columns.Add("UserKey", typeof(int)); - dt.Columns.Add("Type", typeof(string)); - dt.Columns.Add("Value", typeof(string)); - foreach (var user in users) - { - var row = dt.NewRow(); - row.SetField("UserKey", (int)user.Key); - row.SetField("Type", CredentialTypes.ApiKeyV1); - row.SetField("Value", ((Guid)user.ApiKey).ToString().ToLowerInvariant()); - dt.Rows.Add(row); - - - string passwordCredType; - if (!_hashAlgorithmToCredType.TryGetValue(user.PasswordHashAlgorithm, out passwordCredType)) - { - Log.Error("Unknown Hash Algorithm: {0}", user.PasswordHashAlgorithm); - } - else - { - row = dt.NewRow(); - row.SetField("UserKey", (int)user.Key); - row.SetField("Type", passwordCredType); - row.SetField("Value", (string)user.HashedPassword); - dt.Rows.Add(row); - } - } - - WithTableType(c, "Temp_PopulateCredentialsInputType", "UserKey int, Type nvarchar(64), Value nvarchar(256)", () => - { - // Update the DB - var updatedRowCount = c.Execute( - WhatIf ? WhatIfQuery : CommitQuery, - new TableValuedParameter("@creds", "Temp_PopulateCredentialsInputType", dt)); - - Log.Info("Inserted {0} credential records", updatedRowCount); - }); - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Database/CreateSqlUserTask.cs b/src/NuGetGallery.Operations/Tasks/Database/CreateSqlUserTask.cs deleted file mode 100644 index 7244cad2b8..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Database/CreateSqlUserTask.cs +++ /dev/null @@ -1,89 +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 System.Data.SqlClient; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("createsqluser", "Creates a new DB Owner for the gallery database", AltName="csu")] - public class CreateSqlUserTask : DatabaseTask - { - [Option("The user name to create, leave the blank for the default", AltName="u")] - public string UserName { get; set; } - - [Option("Set this switch to put the new Connection String in the clipboard", AltName="c")] - public bool Clip { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (String.IsNullOrEmpty(UserName) && CurrentEnvironment != null) - { - UserName = $"{CurrentEnvironment.EnvironmentName}-site-{DateTime.UtcNow:MMMdd-yyyy}"; - } - - ArgCheck.RequiredOrConfig(UserName, "UserName"); - } - - public override void ExecuteCommand() - { - // Generate password - var rng = new RNGCryptoServiceProvider(); - byte[] data = new byte[20]; - rng.GetBytes(data); - string password = Convert.ToBase64String(data); - - WithMasterConnection((c, db) => - { - if (!WhatIf) - { - db.Execute($"CREATE LOGIN [{UserName}] WITH PASSWORD='{password}';"); - } - Log.Info("Created Login: {0}", UserName); - }); - - WithConnection((c, db) => - { - if (!WhatIf) - { - db.Execute(String.Format("CREATE USER [{0}] FROM LOGIN [{0}];", UserName)); - } - Log.Info("Created User: {0}", UserName); - - if (!WhatIf) - { - db.Execute($"EXEC sp_addrolemember 'db_owner', '{UserName}';"); - } - Log.Info("Added User to db_owner role: {0}", UserName); - }); - - // Generate the new connection string - var newstr = new SqlConnectionStringBuilder(ConnectionString.ConnectionString); - newstr.UserID = $"{UserName}@{Util.GetDatabaseServerName(ConnectionString)}"; - newstr.Password = password; - - if (Clip) - { - var t = new Thread(() => Clipboard.SetText(newstr.ConnectionString)); - t.SetApartmentState(ApartmentState.STA); - t.Start(); - t.Join(); - Log.Info("Connection String for the new user is in the clipboard"); - } - else - { - Log.Info("Connection String for the new user: "); - Log.Info(newstr.ConnectionString); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Database/DeleteSqlUserTask.cs b/src/NuGetGallery.Operations/Tasks/Database/DeleteSqlUserTask.cs deleted file mode 100644 index 6f7e4fa0fa..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Database/DeleteSqlUserTask.cs +++ /dev/null @@ -1,76 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks.Database -{ - [Command("deletesqluser", "Lists SQL Users and access", AltName = "dsu")] - public class DeleteSqlUser : DatabaseTask - { - [Option("Semicolon-separated list of users to delete", AltName="u")] - public List Users { get; set; } - - public DeleteSqlUser() - { - Users = new List(); - } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (!Users.Any()) - { - Users = null; // Just trigger ArgCheck to fail - } - - ArgCheck.Required(Users, "Users"); - } - - public override void ExecuteCommand() - { - WithConnection((c, db) => - { - foreach (var user in Users) - { - if (db.Query("SELECT name FROM sys.database_principals WHERE name = @n", new { n = user }).Any()) - { - if (!WhatIf) - { - db.Execute($"DROP USER [{user}]"); - } - Log.Info("Deleted Database User: {0}", user); - } - else - { - Log.Info("No DB User found: {0}", user); - } - } - }); - - WithMasterConnection((c, db) => - { - foreach (var user in Users) - { - if (db.Query("SELECT name FROM sys.sql_logins WHERE name = @n", new { n = user }).Any()) - { - if (!WhatIf) - { - db.Execute($"DROP LOGIN [{user}]"); - } - Log.Info("Deleted SQL Login: {0}", user); - } - else - { - Log.Info("No SQL Login found: {0}", user); - } - } - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Database/ListSqlUserTask.cs b/src/NuGetGallery.Operations/Tasks/Database/ListSqlUserTask.cs deleted file mode 100644 index 90cdacdd59..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Database/ListSqlUserTask.cs +++ /dev/null @@ -1,62 +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 System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations.Tasks.Database -{ - [Command("listsqluser", "Lists SQL Users and access", AltName = "lsu")] - public class ListSqlUserTask : DatabaseTask - { - public override void ExecuteCommand() - { - ISet dbUsers = null; - ISet sqlLogins = null; - WithMasterConnection((c, db) => - { - sqlLogins = new HashSet(db.Query("SELECT name FROM sys.sql_logins")); - }); - - WithConnection((c, db) => - { - dbUsers = new HashSet(db.Query("SELECT name FROM sys.database_principals WHERE type = 'S'")); - }); - - Debug.Assert(dbUsers != null && sqlLogins != null); - - var sa = sqlLogins.SingleOrDefault(s => s.EndsWith("sa", StringComparison.Ordinal)); - Log.Info("SA Login Name: {0}", sa); - - var pairs = dbUsers.Where(s => sqlLogins.Contains(s)); - Log.Info("SQL Logins with an associated DB User:"); - foreach (var pair in pairs) - { - Log.Info("* {0}", pair); - } - - var orphanedLogins = sqlLogins.Except(dbUsers).Except(new [] { sa }); - if (orphanedLogins.Any()) - { - Log.Info("'Orphaned' Logins that should be deleted:"); - foreach (var login in orphanedLogins) - { - Log.Info("* {0}", login); - } - } - - var orphanedUsers = dbUsers.Except(sqlLogins).Except(new[] { "dbo", "guest", "INFORMATION_SCHEMA", "sys" }); - if (orphanedUsers.Any()) - { - Log.Info("DB Users without an attached SQL Login that should be deleted:"); - foreach (var user in orphanedUsers) - { - Log.Info("* {0}", user); - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/DeleteAllPackageVersionsTask.cs b/src/NuGetGallery.Operations/Tasks/DeleteAllPackageVersionsTask.cs deleted file mode 100644 index 6083bc89ec..0000000000 --- a/src/NuGetGallery.Operations/Tasks/DeleteAllPackageVersionsTask.cs +++ /dev/null @@ -1,89 +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.Data.SqlClient; -using AnglicanGeek.DbExecutor; -using Microsoft.WindowsAzure.Storage; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("deletefullpackage", "Delete all versions of the specified package", AltName = "dfp")] - public class DeleteAllPackageVersionsTask : DatabaseAndStorageTask - { - [Option("Storage account in which to place audit records and backups, usually provided by the environment")] - public CloudStorageAccount BackupStorage { get; set; } - - [Option("The ID of the package", AltName = "p")] - public string PackageId { get; set; } - - [Option("The reason for the deletion ('owner request', 'license violation', etc.)", AltName = "r")] - public string Reason { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (BackupStorage == null && CurrentEnvironment != null) - { - BackupStorage = CurrentEnvironment.BackupStorage; - } - ArgCheck.RequiredOrConfig(BackupStorage, "BackupStorage"); - - ArgCheck.Required(PackageId, "PackageId"); - } - - public override void ExecuteCommand() - { - Log.Info( - "Deleting package registration and all package versions for '{0}'.", - PackageId); - - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - - var packageRegistration = Util.GetPackageRegistration( - dbExecutor, - PackageId); - var packages = Util.GetPackages( - dbExecutor, - packageRegistration.Key); - - foreach(var package in packages) - { - var task = new DeletePackageVersionTask { - ConnectionString = ConnectionString, - BackupStorage = BackupStorage, - StorageAccount = StorageAccount, - PackageId = package.Id, - PackageVersion = package.Version, - Reason = Reason, - WhatIf = WhatIf - }; - task.ExecuteCommand(); - } - - Log.Info( - "Deleting package registration data for '{0}'", - packageRegistration.Id); - if (!WhatIf) - { - dbExecutor.Execute( - "DELETE por FROM PackageOwnerRequests por JOIN PackageRegistrations pr ON pr.[Key] = por.PackageRegistrationKey WHERE pr.[Key] = @packageRegistrationKey", - new { packageRegistrationKey = packageRegistration.Key }); - dbExecutor.Execute( - "DELETE pro FROM PackageRegistrationOwners pro JOIN PackageRegistrations pr ON pr.[Key] = pro.PackageRegistrationKey WHERE pr.[Key] = @packageRegistrationKey", - new { packageRegistrationKey = packageRegistration.Key }); - dbExecutor.Execute( - "DELETE FROM PackageRegistrations WHERE [Key] = @packageRegistrationKey", - new { packageRegistrationKey = packageRegistration.Key }); - } - } - - Log.Info( - "Deleted package registration and all package versions for '{0}'.", - PackageId); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/DeleteBrokenPackageBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/DeleteBrokenPackageBackupsTask.cs deleted file mode 100644 index 58d1ab982e..0000000000 --- a/src/NuGetGallery.Operations/Tasks/DeleteBrokenPackageBackupsTask.cs +++ /dev/null @@ -1,80 +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 System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage.Blob; - -namespace NuGetGallery.Operations -{ - [Command("deletebrokenpackagebackups", "Delete Package Backups which are broken", AltName = "dbpb", IsSpecialPurpose = true)] - public class DeleteBrokenPackageBackupsTask : StorageTask - { - public override void ExecuteCommand() - { - var storageName = StorageAccountName; - - Log.Trace("Getting all broken package backup files on storage account '{0}'.", storageName); - - var blobItems = GetPackageBackupBlobItems().ToList(); - - var blobDirectories = blobItems - .Select(bi => bi as CloudBlobDirectory) - .Where(directory => directory != null) - .ToList(); - - var totalCount = blobDirectories.Count; - var processedCount = 0; - Log.Trace( - "Deleting {0} broken package backup files (out of {1} total blob items) on storage account '{2}'.", - totalCount, - blobItems.Count, - storageName); - - Parallel.ForEach(blobDirectories, blobDirectory => - { - try - { - if (!WhatIf) - { - DeleteBlobDirectory(blobDirectory); - } - Interlocked.Increment(ref processedCount); - Log.Info( - $"Deleted broken package backup root directory '{blobDirectory.Uri.Segments.Last()}' ({processedCount} of {totalCount})."); - } - catch(Exception ex) - { - Interlocked.Increment(ref processedCount); - Log.Error( - $"Error deleting broken package backup root directory '{blobDirectory.Uri.Segments.Last()}': {processedCount} ({totalCount} of {ex.Message})."); - } - }); - } - - IEnumerable GetPackageBackupBlobItems() - { - var blobClient = CreateBlobClient(); - - var packageBackupsBlobContainer = Util.GetPackageBackupsBlobContainer(blobClient); - - return packageBackupsBlobContainer.ListBlobs(); - } - - static void DeleteBlobDirectory(CloudBlobDirectory blobDirectory) - { - foreach(var blobItem in blobDirectory.ListBlobs()) - { - var subDirectory = blobItem as CloudBlobDirectory; - if (subDirectory != null) - DeleteBlobDirectory(subDirectory); - - var blob = blobItem as ICloudBlob; - if (blob != null) - blob.Delete(); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/DeleteOldWarehouseBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/DeleteOldWarehouseBackupsTask.cs deleted file mode 100644 index bbd3657ab5..0000000000 --- a/src/NuGetGallery.Operations/Tasks/DeleteOldWarehouseBackupsTask.cs +++ /dev/null @@ -1,51 +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.Data.SqlClient; -using AnglicanGeek.DbExecutor; -using NuGetGallery.Operations.Common; -using NuGetGallery.Operations.Model; - -namespace NuGetGallery.Operations -{ - [Command("purgewarehousebackups", "Deletes old database backups", AltName = "pwh")] - public class DeleteOldWarehouseBackupsTask : WarehouseTask - { - public override void ExecuteCommand() - { - var dbServer = ConnectionString.DataSource; - var masterConnectionString = Util.GetMasterConnectionString(ConnectionString.ConnectionString); - - Log.Trace("Deleting old warehouse backups for server '{0}':", dbServer); - - using (var sqlConnection = new SqlConnection(masterConnectionString)) - { - sqlConnection.Open(); - - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - var dbs = dbExecutor.Query( - "SELECT name FROM sys.databases WHERE name LIKE 'WarehouseBackup_%' AND state = @state", - new { state = Util.OnlineState }); - - foreach (var db in dbs) - { - var timestamp = Util.GetDatabaseNameTimestamp(db); - var date = Util.GetDateTimeFromTimestamp(timestamp); - if (DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)) > date) - DeleteDatabaseBackup(db, dbExecutor); - } - } - } - } - - private void DeleteDatabaseBackup(Db db, SqlExecutor dbExecutor) - { - if (!WhatIf) - { - dbExecutor.Execute($"DROP DATABASE {db.Name}"); - } - Log.Info("Deleted database {0}.", db.Name); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/DeletePackageFileTask.cs b/src/NuGetGallery.Operations/Tasks/DeletePackageFileTask.cs deleted file mode 100644 index 904ce30234..0000000000 --- a/src/NuGetGallery.Operations/Tasks/DeletePackageFileTask.cs +++ /dev/null @@ -1,63 +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 Microsoft.WindowsAzure.Storage; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("deletepackagefile", "Deletes a specific package file", AltName = "dpf")] - public class DeletePackageFileTask : PackageVersionTask - { - [Option("Storage account in which to place audit records and backups, usually provided by the environment")] - public CloudStorageAccount BackupStorage { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(PackageId, "PackageId"); - ArgCheck.Required(PackageVersion, "PackageVersion"); - ArgCheck.Required(PackageHash, "PackageHash"); - - if (BackupStorage == null && CurrentEnvironment != null) - { - BackupStorage = CurrentEnvironment.BackupStorage; - } - ArgCheck.RequiredOrConfig(BackupStorage, "BackupStorage"); - } - - public override void ExecuteCommand() - { - new BackupPackageFileTask - { - BackupStorage = BackupStorage, - StorageAccount = StorageAccount, - PackageId = PackageId, - PackageVersion = PackageVersion, - PackageHash = PackageHash, - WhatIf = WhatIf - }.ExecuteCommand(); - - var blobClient = CreateBlobClient(); - var packagesBlobContainer = Util.GetPackagesBlobContainer(blobClient); - var packageFileBlob = Util.GetPackageFileBlob( - packagesBlobContainer, - PackageId, - PackageVersion); - var fileName = Util.GetPackageFileName( - PackageId, - PackageVersion); - if (packageFileBlob.Exists()) - { - Log.Info("Deleting package file '{0}'.", fileName); - if (!WhatIf) - { - packageFileBlob.DeleteIfExists(); - } - } - else - { - Log.Warn("Package file does not exist '{0}'.", fileName); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/DeletePackageVersionTask.cs b/src/NuGetGallery.Operations/Tasks/DeletePackageVersionTask.cs deleted file mode 100644 index d7bd801a4e..0000000000 --- a/src/NuGetGallery.Operations/Tasks/DeletePackageVersionTask.cs +++ /dev/null @@ -1,138 +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.Data; -using System.Data.SqlClient; -using AnglicanGeek.DbExecutor; -using Microsoft.WindowsAzure.Storage; -using NuGetGallery.Auditing; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("deletepackageversion", "Delete a specific package version", AltName = "dpv")] - public class DeletePackageVersionTask : DatabasePackageVersionTask - { - [Option("Storage account in which to place audit records and backups, usually provided by the environment")] - public CloudStorageAccount BackupStorage { get; set; } - - [Option("Set this flag to write the deletion audit record ONLY and not proceed with the deletion itself")] - public bool AuditOnly { get; set; } - - [Option("The reason for the deletion ('owner request', 'license violation', etc.)", AltName = "r")] - public string Reason { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (BackupStorage == null && CurrentEnvironment != null) - { - BackupStorage = CurrentEnvironment.BackupStorage; - } - ArgCheck.RequiredOrConfig(BackupStorage, "BackupStorage"); - - ArgCheck.Required(Reason, "Reason"); - } - - public override void ExecuteCommand() - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - - var package = Util.GetPackage( - dbExecutor, - PackageId, - PackageVersion); - - // Multiple queries? Yes. Do I care? No. - var packageRecord = new DataTable(); - using (SqlCommand cmd = sqlConnection.CreateCommand()) - { - cmd.CommandType = CommandType.Text; - cmd.CommandText = "SELECT * FROM Packages WHERE [Key] = @key"; - cmd.Parameters.AddWithValue("@key", package.Key); - var result = cmd.ExecuteReader(); - packageRecord.Load(result); - } - - var registrationRecord = new DataTable(); - using (SqlCommand cmd = sqlConnection.CreateCommand()) - { - cmd.CommandType = CommandType.Text; - cmd.CommandText = "SELECT * FROM PackageRegistrations WHERE [ID] = @id"; - cmd.Parameters.AddWithValue("@id", package.Id); - var result = cmd.ExecuteReader(); - registrationRecord.Load(result); - } - - // Write a delete audit record - var auditRecord = new PackageAuditRecord( - package.Id, - package.Version, - package.Hash, - packageRecord, - registrationRecord, - PackageAuditAction.Deleted, - Reason); - - if (WhatIf) - { - Log.Info("Would Write Audit Record to " + auditRecord.GetPath()); - } - else - { - Log.Info("Writing Audit Record"); - var uri = Util.SaveAuditRecord(BackupStorage, auditRecord).Result; - Log.Info("Successfully wrote audit record to: " + uri.AbsoluteUri); - } - - if (package == null) - { - Log.Error("Package version does not exist: '{0}.{1}'", PackageId, PackageVersion); - return; - } - - if (!AuditOnly) - { - Log.Info( - "Deleting package data for '{0}.{1}'", - package.Id, - package.Version); - - if (!WhatIf && !AuditOnly) - { - dbExecutor.Execute( - "DELETE pa FROM PackageAuthors pa JOIN Packages p ON p.[Key] = pa.PackageKey WHERE p.[Key] = @key", - new { key = package.Key }); - dbExecutor.Execute( - "DELETE pd FROM PackageDependencies pd JOIN Packages p ON p.[Key] = pd.PackageKey WHERE p.[Key] = @key", - new { key = package.Key }); - dbExecutor.Execute( - "DELETE pf FROM PackageFrameworks pf JOIN Packages p ON p.[Key] = pf.Package_Key WHERE p.[Key] = @key", - new { key = package.Key }); - dbExecutor.Execute( - "DELETE p FROM Packages p JOIN PackageRegistrations pr ON pr.[Key] = p.PackageRegistrationKey WHERE p.[Key] = @key", - new { key = package.Key }); - } - - new DeletePackageFileTask - { - BackupStorage = BackupStorage, - StorageAccount = StorageAccount, - PackageId = package.Id, - PackageVersion = package.NormalizedVersion, - PackageHash = package.Hash, - WhatIf = WhatIf - }.ExecuteCommand(); - } - else - { - Log.Info("Only wrote audit record. Package was NOT deleted."); - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/DeleteUserTask.cs b/src/NuGetGallery.Operations/Tasks/DeleteUserTask.cs deleted file mode 100644 index 339e224168..0000000000 --- a/src/NuGetGallery.Operations/Tasks/DeleteUserTask.cs +++ /dev/null @@ -1,96 +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.Data.SqlClient; -using System.Linq; -using AnglicanGeek.DbExecutor; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("deleteuser", "Delete a user's account and all of their packages", AltName = "du")] - public class DeleteUserTask : DatabaseAndStorageTask - { - [Option("The username of the user to delete", AltName = "u")] - public string Username { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(Username, "Username"); - } - - public override void ExecuteCommand() - { - Log.Info( - "Delete the user account and all packages for '{0}'.", - Username); - - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - - var user = Util.GetUser(dbExecutor, Username); - - if (user == null) - { - Log.Error("User was not found"); - return; - } - - Log.Info("User found with EmailAddress '{0}' and UnconfirmedEmailAddress '{1}'", - user.EmailAddress, user.UnconfirmedEmailAddress); - - var packageCount = user.PackageRegistrationIds.Count(); - var packageNumber = 0; - - foreach (var packageId in user.PackageRegistrationIds) - { - Log.Info("Deleting package '{0}' because '{1}' is the sole owner. ({2}/{3})", - packageId, Username, ++packageNumber, packageCount); - - var deletePackageTask = new DeleteAllPackageVersionsTask - { - ConnectionString = ConnectionString, - StorageAccount = StorageAccount, - PackageId = packageId, - WhatIf = WhatIf - }; - - deletePackageTask.Execute(); - } - - Log.Info("Deleting remaining package ownership records (from shared ownership)"); - - if (!WhatIf) - { - dbExecutor.Execute( - "DELETE pro FROM PackageRegistrationOwners pro WHERE pro.UserKey = @userKey", - new { userKey = user.Key }); - } - - Log.Info("Deleting package ownership requests"); - - if (!WhatIf) - { - dbExecutor.Execute( - "DELETE por FROM PackageOwnerRequests por WHERE @userKey IN (por.NewOwnerKey, por.RequestingOwnerKey)", - new { userKey = user.Key }); - } - - Log.Info("Deleting the user record itself"); - - if (!WhatIf) - { - dbExecutor.Execute( - "DELETE u FROM Users u WHERE u.[Key] = @userKey", - new { userKey = user.Key }); - } - - Log.Info( - "Deleted all packages owned solely by '{0}' as well as the user record." - , user.Username); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/ExportDatabaseTask.cs b/src/NuGetGallery.Operations/Tasks/ExportDatabaseTask.cs deleted file mode 100644 index 2b0106e2ad..0000000000 --- a/src/NuGetGallery.Operations/Tasks/ExportDatabaseTask.cs +++ /dev/null @@ -1,97 +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 System.Data.SqlClient; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using NuGetGallery.Operations.Common; -using NuGetGallery.Operations.SqlDac; - -namespace NuGetGallery.Operations -{ - [Command("exportdatabase", "Exports a copy of the database to blob storage", AltName = "xdb", MinArgs = 0, MaxArgs = 0)] - public class ExportDatabaseTask : DatabaseTask - { - [Option("Azure Storage Account in which the exported database should be placed", AltName = "s")] - public CloudStorageAccount DestinationStorage { get; set; } - - [Option("Blob container in which the backup should be placed", AltName = "c")] - public string DestinationContainer { get; set; } - - [Option("URL of the SQL DAC endpoint to talk to", AltName = "dac")] - public Uri SqlDacEndpoint { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (CurrentEnvironment != null) - { - if (DestinationStorage == null) - { - DestinationStorage = CurrentEnvironment.BackupStorage; - } - if (SqlDacEndpoint == null) - { - SqlDacEndpoint = CurrentEnvironment.SqlDacEndpoint; - } - } - - ArgCheck.RequiredOrConfig(DestinationStorage, "DestinationStorage"); - ArgCheck.RequiredOrConfig(SqlDacEndpoint, "SqlDacEndpoint"); - ArgCheck.Required(DestinationContainer, "DestinationContainer"); - } - - public override void ExecuteCommand() - { - Log.Info("Exporting {0} on {1} to {2}", ConnectionString.InitialCatalog, Util.GetDatabaseServerName(ConnectionString), DestinationStorage.Credentials.AccountName); - - string serverName = ConnectionString.DataSource; - if (serverName.StartsWith("tcp:", StringComparison.OrdinalIgnoreCase)) - { - serverName = serverName.Substring(4); - } - - WASDImportExport.ImportExportHelper helper = new WASDImportExport.ImportExportHelper(Log) - { - EndPointUri = SqlDacEndpoint.AbsoluteUri, - DatabaseName = ConnectionString.InitialCatalog, - ServerName = serverName, - UserName = ConnectionString.UserID, - Password = ConnectionString.Password, - StorageKey = Convert.ToBase64String(DestinationStorage.Credentials.ExportKey()) - }; - - // Prep the blob - string blobUrl = null; - if (!WhatIf) - { - var client = DestinationStorage.CreateCloudBlobClient(); - var container = client.GetContainerReference(DestinationContainer); - container.CreateIfNotExists(); - var blob = container.GetBlockBlobReference(ConnectionString.InitialCatalog + ".bacpac"); - if (blob.Exists()) - { - Log.Info("Skipping export of {0} because the blob already exists", blob.Name); - } - else - { - Log.Info("Starting export to {0}", blob.Uri.AbsoluteUri); - - // Export! - blobUrl = helper.DoExport(blob.Uri.AbsoluteUri, WhatIf); - } - } - - Log.Info("Exported to {0}", blobUrl); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/FixExternalPackageTask.cs b/src/NuGetGallery.Operations/Tasks/FixExternalPackageTask.cs deleted file mode 100644 index 938ec9e737..0000000000 --- a/src/NuGetGallery.Operations/Tasks/FixExternalPackageTask.cs +++ /dev/null @@ -1,53 +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.Data.SqlClient; -using System.Linq; -using System.Net.Http; -using AnglicanGeek.DbExecutor; - -namespace NuGetGallery.Operations -{ - [Command("fixexternalpackage", "Download the specified package which uses ExternalPackageUrl and transfer it to the storage server", AltName = "fep", IsSpecialPurpose = true)] - public class FixExternalPackageTask : DatabasePackageVersionTask - { - public override void ExecuteCommand() - { - // todo: move the data access from the website to a common lib and use that instead - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - var package = dbExecutor.Query( - "SELECT p.[Key], pr.Id, p.Version, p.ExternalPackageUrl FROM Packages p JOIN PackageRegistrations pr ON pr.[Key] = p.PackageRegistrationKey WHERE pr.Id = @id AND p.Version = @version AND p.ExternalPackageUrl IS NOT NULL", - new { id = PackageId, version = PackageVersion }) - .SingleOrDefault(); - if (package == null) - { - Log.Info("Package is stored locally: {0} {1}", PackageId, PackageVersion); - } - else - { - using (var httpClient = new HttpClient()) - using (var packageStream = httpClient.GetStreamAsync(package.ExternalPackageUrl).Result) - { - new UploadPackageTask - { - StorageAccount = StorageAccount, - PackageId = package.Id, - PackageVersion = package.Version, - PackageFile = packageStream, - WhatIf = WhatIf - }.ExecuteCommand(); - } - - if (!WhatIf) - { - dbExecutor.Execute( - "UPDATE Packages SET ExternalPackageUrl = NULL WHERE [Key] = @key", - new { key = package.Key }); - } - } - } - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Tasks/FixPackageFilesContentTypeTask.cs b/src/NuGetGallery.Operations/Tasks/FixPackageFilesContentTypeTask.cs deleted file mode 100644 index e11f365464..0000000000 --- a/src/NuGetGallery.Operations/Tasks/FixPackageFilesContentTypeTask.cs +++ /dev/null @@ -1,47 +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.Collections.Concurrent; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage.Blob; - -namespace NuGetGallery.Operations -{ - [Command("fixcontenttypes", "Fixes the content type of package files in the storage server", AltName = "fct", IsSpecialPurpose = true)] - public class FixPackageFilesContentTypeTask : StorageTask - { - public override void ExecuteCommand() - { - var blobClient = CreateBlobClient(); - - var packagesBlobContainer = Util.GetPackagesBlobContainer(blobClient); - - Log.Info("Listing all blobs..."); - var blobs = packagesBlobContainer.ListBlobs(); - Log.Info("Looking for broken blobs"); - ConcurrentBag broken = new ConcurrentBag(); - Parallel.ForEach(blobs, blob => - { - var packageFileBlob = packagesBlobContainer.GetBlockBlobReference(blob.Uri.ToString()); - packageFileBlob.FetchAttributes(); - if (packageFileBlob.Properties.ContentType != "application/zip") - { - broken.Add(packageFileBlob); - } - }); - Log.Info("Fixing {0} broken blobs..."); - int totalCount = broken.Count; - int processedCount = 0; - Parallel.ForEach(broken, packageFileBlob => - { - if (!WhatIf) - { - packageFileBlob.Properties.ContentType = "application/zip"; - packageFileBlob.SetProperties(); - } - Log.Info("Fixed '{0}' ({1} of {2}).", packageFileBlob.Uri.Segments.Last(), Interlocked.Increment(ref processedCount), totalCount); - }); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/HandleFailedPackageEditsTask.cs b/src/NuGetGallery.Operations/Tasks/HandleFailedPackageEditsTask.cs deleted file mode 100644 index 7c852dc368..0000000000 --- a/src/NuGetGallery.Operations/Tasks/HandleFailedPackageEditsTask.cs +++ /dev/null @@ -1,59 +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.Data.Entity; -using System.Linq; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("handlefailededits", "Handle Failed Package Edits", AltName = "hfe", MaxArgs = 0)] - public class HandleFailedPackageEditsTask : DatabaseTask - { - - [Option("Email account to be used to send the mail", AltName = "ua")] - public string UserAccount { get; set; } - - [Option("Email password to be used to send the mail", AltName = "p")] - public string Password { get; set; } - - [Option("Email host to be used to send the mail", AltName = "eh")] - public string EmailHost { get; set; } - public override void ExecuteCommand() - { - //Get all the failed edits. - var connectionString = ConnectionString.ConnectionString; - var entitiesContext = new EntitiesContext(connectionString, readOnly: true); - var failedEdits = entitiesContext.Set() - .Where(pe => pe.TriedCount == 3).Include(pe => pe.Package).Include(pe => pe.Package.PackageRegistration); - - - //For each ofthe failed edit, send out a support request mail. - foreach (PackageEdit edit in failedEdits) - { - Log.Info( - "Sending support request for '{0}'", - edit.Package.PackageRegistration.Id); - SendMailTask mailTask = new SendMailTask - { - ConnectionString = this.ConnectionString, - UserAccount = this.UserAccount, - Password = this.Password, - EmailHost = this.EmailHost, - ToList = this.UserAccount, - ReplyToList = this.UserAccount, - MailSubject = $" [NuGet Gallery] : Package Edit Request for {edit.Package.PackageRegistration.Id}", - MailContent = $"Package: {edit.Package.PackageRegistration.Id}
Version: {edit.Package.NormalizedVersion}
TimeStamp: {edit.Timestamp}
LastError: {edit.LastError}
Message sent from NuGet Gallery " - }; - try - { - mailTask.Execute(); - } - catch (Exception e) - { - Log.Error("Creating support request for package {0} failed with error {1}", edit.Package.PackageRegistration.Id, e.Message); - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/HandleQueuedPackageEditsTask.cs b/src/NuGetGallery.Operations/Tasks/HandleQueuedPackageEditsTask.cs deleted file mode 100644 index c70d8e9dbb..0000000000 --- a/src/NuGetGallery.Operations/Tasks/HandleQueuedPackageEditsTask.cs +++ /dev/null @@ -1,255 +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 System.Data.Entity; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Threading; -using Microsoft.WindowsAzure.Storage.Blob; -using NuGetGallery.Packaging; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("handlequeuededits", "Handle Queued Package Edits", AltName = "hqe", MaxArgs = 0)] - public class HandleQueuedPackageEditsTask : DatabaseAndStorageTask - { - static int[] SleepTimes = { 50, 750, 1500, 2500, 3750, 5250, 7000, 9000 }; - - public override void ExecuteCommand() - { - // Work to do: - // 0) Find Pending Edits in DB that have been attempted less than 3 times - // 1) Backup all old NUPKGS - // 2) Generate all new NUPKGs (in place), and tell gallery the edit is completed - var connectionString = ConnectionString.ConnectionString; - - // We group edits together by their package key and process them together - this is a read-only operation - var entitiesContext = new EntitiesContext(connectionString, readOnly: true); - var editsPerPackage = entitiesContext.Set() - .GroupBy(pe => pe.PackageKey); - - // Now that we have our list of packages with pending edits, we'll process the pending edits for each - // Note that we're not doing editing in parallel because - // a) any particular blob may use a large amount of memory to process. Let's not multiply that! - // b) we don't want multithreaded usage of the entitiesContext (and its implied transactions)! - foreach (IGrouping editsGroup in editsPerPackage) - { - if (editsGroup.Any((pe => pe.TriedCount < 3))) - { - ProcessPackageEdits(editsGroup.Key, editsGroup); - } - } - } - - private void ProcessPackageEdits(int packageKey, IEnumerable editsToDelete) - { - // Create a fresh entities context so that we work in isolation - var entitiesContext = new EntitiesContext(ConnectionString.ConnectionString, readOnly: false); - - // Get the most recent edit for this package - var edit = entitiesContext.Set() - .Where(pe => pe.PackageKey == packageKey && pe.TriedCount < 3) - .Include(pe => pe.Package) - .Include(pe => pe.Package.PackageRegistration) - .Include(pe => pe.User) - .OrderByDescending(pe => pe.Timestamp) - .First(); - - // List of Work to do: - // 1) Backup old blob, if the original has not been backed up yet - // 2) Downloads blob, create new NUPKG locally - // 3) Upload blob - // 4) Update the database - var blobClient = StorageAccount.CreateCloudBlobClient(); - var packagesContainer = Util.GetPackagesBlobContainer(blobClient); - - var latestPackageFileName = Util.GetPackageFileName(edit.Package.PackageRegistration.Id, edit.Package.Version); - var originalPackageFileName = Util.GetBackupOfOriginalPackageFileName(edit.Package.PackageRegistration.Id, edit.Package.Version); - - var originalPackageBackupBlob = packagesContainer.GetBlockBlobReference(originalPackageFileName); - var latestPackageBlob = packagesContainer.GetBlockBlobReference(latestPackageFileName); - - var edits = new List> - { - (m) => { m.Authors = edit.Authors; }, - (m) => { m.Copyright = edit.Copyright; }, - (m) => { m.Description = edit.Description; }, - (m) => { m.IconUrl = edit.IconUrl; }, - (m) => { m.LicenseUrl = edit.LicenseUrl; }, - (m) => { m.ProjectUrl = edit.ProjectUrl; }, - (m) => { m.ReleaseNotes = edit.ReleaseNotes; }, - (m) => { m.RequireLicenseAcceptance = edit.RequiresLicenseAcceptance; }, - (m) => { m.Summary = edit.Summary; }, - (m) => { m.Title = edit.Title; }, - (m) => { m.Tags = edit.Tags; }, - }; - - Log.Info( - "Processing Edit Key={0}, PackageId={1}, Version={2}, User={3}", - edit.Key, - edit.Package.PackageRegistration.Id, - edit.Package.Version, - edit.User.Username); - - if (!WhatIf) - { - edit.TriedCount += 1; - int nr = entitiesContext.SaveChanges(); - if (nr != 1) - { - throw new Exception( - $"Something went terribly wrong, only one entity should be updated but actually {nr} entities were updated"); - } - } - - try - { - ArchiveOriginalPackageBlob(originalPackageBackupBlob, latestPackageBlob); - using (var readWriteStream = new MemoryStream()) - { - // Download to memory - CloudBlockBlob downloadSourceBlob = WhatIf ? latestPackageBlob : originalPackageBackupBlob; - Log.Info("Downloading original package blob to memory {0}", downloadSourceBlob.Name); - downloadSourceBlob.DownloadToStream(readWriteStream); - - // Rewrite in memory - Log.Info("Rewriting nupkg package in memory", downloadSourceBlob.Name); - NupkgRewriter.RewriteNupkgManifest(readWriteStream, edits); - - // Get updated hash code, and file size - Log.Info("Computing updated hash code of memory stream"); - var newPackageFileSize = readWriteStream.Length; - var hashAlgorithm = HashAlgorithm.Create("SHA512"); - byte[] hashBytes = hashAlgorithm.ComputeHash(readWriteStream.GetBuffer()); - var newHash = Convert.ToBase64String(hashBytes); - - if (!WhatIf) - { - // Snapshot the blob - var blobSnapshot = latestPackageBlob.CreateSnapshot(); - - // Build up the changes in the entities context - edit.Apply(hashAlgorithm: "SHA512", hash: newHash, packageFileSize: newPackageFileSize); - foreach (var eachEdit in editsToDelete) - { - entitiesContext.DeleteOnCommit(eachEdit); - } - - // Upload the blob before doing SaveChanges(). If blob update fails, we won't do SaveChanges() and the edit can be retried. - // If SaveChanges() fails we can undo the blob upload. - try - { - Log.Info("Uploading blob from memory {0}", latestPackageBlob.Name); - readWriteStream.Position = 0; - latestPackageBlob.UploadFromStream(readWriteStream); - } - catch (Exception e) - { - Log.Error("(error) - package edit blob update failed."); - Log.ErrorException("(exception)", e); - Log.Error("(note) - blob snapshot URL = " + blobSnapshot.Uri); - throw; // To handler block that will record error in DB - } - - try - { - // SaveChanges tries to commit changes to DB - entitiesContext.SaveChanges(); - } - catch (Exception e) - { - // Commit changes to DB probably failed. - // Since our blob update wasn't part of the transaction (and doesn't AFAIK have a 'commit()' operator we can utilize for the type of blobs we are using) - // try, (single attempt) to roll back the blob update by restoring the previous snapshot. - Log.Error("(error) - package edit DB update failed. Trying to roll back the blob to its previous snapshot."); - Log.ErrorException("(exception)", e); - Log.Error("(note) - blob snapshot URL = " + blobSnapshot.Uri); - try - { - latestPackageBlob.StartCopyFromBlob(blobSnapshot); - } - catch (Exception e2) - { - // If blob rollback fails it is not be the end of the world - // - the package metadata mismatches the edit now, - // but there should still an edit in the queue, waiting to be rerun and put everything back in synch. - Log.Error("(error) - rolling back the package blob to its previous snapshot failed."); - Log.ErrorException("(exception)", e2); - Log.Error("(note) - blob snapshot URL = " + blobSnapshot.Uri); - } - - throw; // To handler block that will record error in DB - } - } - } - } - catch (Exception e) - { - if (!WhatIf) - { - try - { - Log.Info("Storing the error on package edit with key {0}", edit.Key); - - // Try to record the error into the PackageEdit database record - // so that we can actually diagnose failures. - // This must be done on a fresh context to ensure no conflicts. - var errorContext = new EntitiesContext(ConnectionString.ConnectionString, readOnly: false); - var errorEdit = errorContext.Set().Where(pe => pe.Key == edit.Key).FirstOrDefault(); - - if (errorEdit != null) - { - errorEdit.LastError = $"{e.GetType()} : {e}"; - errorContext.SaveChanges(); - } - else - { - Log.Info("The package edit with key {0} couldn't be found. It was likely canceled and deleted.", edit.Key); - } - } - catch (Exception errorException) - { - Log.ErrorException("(error) - couldn't save the last error on the edit that was being applied.", errorException); - } - } - } - } - - /// - /// Creates an archived copy of the original package blob if it doesn't already exist. - /// - private void ArchiveOriginalPackageBlob(CloudBlockBlob originalPackageBlob, CloudBlockBlob latestPackageBlob) - { - // Copy the blob to backup only if it isn't already successfully copied - if ((!originalPackageBlob.Exists()) || (originalPackageBlob.CopyState != null && originalPackageBlob.CopyState.Status != CopyStatus.Success)) - { - if (!WhatIf) - { - Log.Info("Backing up blob: {0} to {1}", latestPackageBlob.Name, originalPackageBlob.Name); - originalPackageBlob.StartCopyFromBlob(latestPackageBlob); - CopyState state = originalPackageBlob.CopyState; - - for (int i = 0; (state == null || state.Status == CopyStatus.Pending) && i < SleepTimes.Length; i++) - { - Log.Info("(sleeping for a copy completion)"); - Thread.Sleep(SleepTimes[i]); - originalPackageBlob.FetchAttributes(); // To get a refreshed CopyState - - //refresh state - state = originalPackageBlob.CopyState; - } - - if (state.Status != CopyStatus.Success) - { - string msg = $"Blob copy failed: CopyState={state.StatusDescription}"; - Log.Error("(error) " + msg); - throw new BlobBackupFailedException(msg); - } - } - } - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Tasks/IBackupDatabase.cs b/src/NuGetGallery.Operations/Tasks/IBackupDatabase.cs deleted file mode 100644 index 5b31a8eb40..0000000000 --- a/src/NuGetGallery.Operations/Tasks/IBackupDatabase.cs +++ /dev/null @@ -1,19 +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 System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NuGetGallery.Operations -{ - // this interface allows backup Jobs to combine Backup Task with Check Status tasks - public interface IBackupDatabase - { - SqlConnectionStringBuilder ConnectionString { get; } - string BackupName { get; } - bool SkippingBackup { get; } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/ListDatabaseBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/ListDatabaseBackupsTask.cs deleted file mode 100644 index a1fb7c4518..0000000000 --- a/src/NuGetGallery.Operations/Tasks/ListDatabaseBackupsTask.cs +++ /dev/null @@ -1,38 +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.Data.SqlClient; -using AnglicanGeek.DbExecutor; -using NuGetGallery.Operations.Model; - -namespace NuGetGallery.Operations -{ - [Command("listdatabasebackups", "List database backups at the specified database server", AltName = "ldb")] - public class ListDatabaseBackupsTask : DatabaseTask - { - public override void ExecuteCommand() - { - var dbServer = ConnectionString.DataSource; - var masterConnectionString = Util.GetMasterConnectionString(ConnectionString.ConnectionString); - - Log.Info("Listing backups for server '{0}':", dbServer); - - using (var sqlConnection = new SqlConnection(masterConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - - var dbs = dbExecutor.Query( - "SELECT name FROM sys.databases WHERE name LIKE 'Backup_%' AND state = @state", - new { state = Util.OnlineState }); - - foreach(var db in dbs) - { - var timestamp = Util.GetDatabaseNameTimestamp(db); - var date = Util.GetDateTimeFromTimestamp(timestamp); - - Log.Info("{0} ({1})", timestamp, date); - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/ListDeploymentSettingsTask.cs b/src/NuGetGallery.Operations/Tasks/ListDeploymentSettingsTask.cs deleted file mode 100644 index 40788860d3..0000000000 --- a/src/NuGetGallery.Operations/Tasks/ListDeploymentSettingsTask.cs +++ /dev/null @@ -1,92 +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 System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("settings", "Show deployment settings received from the Ops console", IsSpecialPurpose = true)] - public class ListDeploymentSettingsTask : OpsTask - { - private static readonly Dictionary _fullNames = new Dictionary() { - { SettingType.Db, "Main Database Connection String" }, - { SettingType.Warehouse, "Warehouse Database Connection String" }, - { SettingType.Storage, "Azure Storage Connection String" }, - { SettingType.Backup, "Backup Azure Storage Connection String" } - }; - - private static readonly Dictionary> _fetcher = new Dictionary>() { - { SettingType.Db, e => e.MainDatabase.ConnectionString }, - { SettingType.Warehouse, e => e.WarehouseDatabase.ConnectionString }, - { SettingType.Storage, e => e.MainStorage.ToString(exportSecrets: true) }, - { SettingType.Backup, e => e.BackupStorage.ToString(exportSecrets: true) } - }; - - [Option("Show all settings")] - public bool All { get; set; } - - [Option("Specify this argument to place one of the specified Connection Strings (Db, Warehouse, Storage, Backup) in the clipboard")] - public string Clip { get; set; } - - public enum SettingType - { - Db, - Warehouse, - Storage, - Backup - } - - public override void ExecuteCommand() - { - if (CurrentEnvironment == null || String.IsNullOrEmpty(CurrentEnvironment.EnvironmentName)) - { - Log.Warn("No current environment!"); - } - else if (!String.IsNullOrEmpty(Clip)) - { - // Parse the value - var values = Enum.GetValues(typeof(SettingType)) - .Cast() - .Where(st => st.ToString().StartsWith(Clip, StringComparison.OrdinalIgnoreCase)) - .ToList(); - if (values.Count == 0) - { - Log.Error("Unknown setting type '{0}'", Clip); - } else if (values.Count > 1) { - Log.Error("Ambiguous setting type '{0}'. Matches: {1}", Clip, String.Join(", ", values.Select(s=> "'" + s.ToString() + "'"))); - } else { - var value = values[0]; - Log.Info("Placing {0} in clipboard", _fullNames[value]); - - Thread t = new Thread(() => Clipboard.SetText(_fetcher[value](CurrentEnvironment), TextDataFormat.UnicodeText)); - t.SetApartmentState(ApartmentState.STA); - t.Start(); - t.Join(); - } - } - else if (!All) - { - Log.Info("Environment: {0}", EnvironmentName); - Log.Info(" Subscription: {0} ({1})", CurrentEnvironment.SubscriptionName, CurrentEnvironment.SubscriptionId); - Log.Info(" Main SQL: {0}", CurrentEnvironment.MainDatabase == null ? "" : CurrentEnvironment.MainDatabase.DataSource); - Log.Info(" Warehouse SQL: {0}", CurrentEnvironment.WarehouseDatabase == null ? "" : CurrentEnvironment.WarehouseDatabase.DataSource); - Log.Info(" Main Storage: {0}", CurrentEnvironment.MainStorage == null ? "" : CurrentEnvironment.MainStorage.Credentials.AccountName); - Log.Info(" Backup Storage: {0}", CurrentEnvironment.BackupStorage == null ? "" : CurrentEnvironment.BackupStorage.Credentials.AccountName); - Log.Info(" SQL DAC: {0}", CurrentEnvironment.SqlDacEndpoint?.AbsoluteUri ?? ""); - } - else - { - Log.Info("All settings for {0}", EnvironmentName); - foreach (var pair in CurrentEnvironment.Settings) - { - Log.Info("* {0} = {1}", pair.Key, pair.Value); - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/ListMigrationsTask.cs b/src/NuGetGallery.Operations/Tasks/ListMigrationsTask.cs deleted file mode 100644 index 444ce96ef8..0000000000 --- a/src/NuGetGallery.Operations/Tasks/ListMigrationsTask.cs +++ /dev/null @@ -1,34 +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.Linq; -using System.Data.Entity.Migrations; -using System.IO; -using System.Reflection; -using NuGetGallery.Operations.Common; -using System.Data.Entity.Migrations.Infrastructure; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("listmigrations", "Lists migrations in the specified assembly/database combination", AltName = "lm", MaxArgs = 0)] - public class ListMigrationsTask : MigrationsTask - { - protected override void ExecuteCommandCore(MigratorBase migrator) - { - Log.Info("Migration Status for {0} on {1}", - ConnectionString.InitialCatalog, - ConnectionString.DataSource); - - foreach (var migration in migrator.GetDatabaseMigrations().Reverse()) - { - Log.Info("A {0}", migration); - } - - foreach (var migration in migrator.GetPendingMigrations()) - { - Log.Info(" {0}", migration); - } - Log.Info("A = Applied"); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/ListPackageBlobBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/ListPackageBlobBackupsTask.cs deleted file mode 100644 index bb8e428b6d..0000000000 --- a/src/NuGetGallery.Operations/Tasks/ListPackageBlobBackupsTask.cs +++ /dev/null @@ -1,45 +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 Microsoft.WindowsAzure.Storage.Blob; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("listpackageblobbackups", "List the URLS of all the backup blobs for a package whose nupkgs are backed up", AltName = "lpbb", MaxArgs = 0)] - public class ListPackageBlobBackupsTask : BackupStorageTask - { - [Option("Id of the package - Id ending in * will do a wildcarded prefix search on the package Id.")] - public string PackageId { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(PackageId, "PackageId"); - } - - public override void ExecuteCommand() - { - var blobClient = CreateBlobClient(); - var storageName = StorageAccountName; - - Log.Trace("Getting all package backup files for package id '{1}' on storage account '{0}'.", storageName, PackageId); - var packageBackupsBlobContainer = Util.GetPackageBackupsBlobContainer(blobClient); - Log.Trace("Container name is '{0}'", packageBackupsBlobContainer.Name); - - var packageIdIsh = PackageId.EndsWith("*", StringComparison.Ordinal) ? PackageId.TrimEnd('*') : PackageId + "/"; - var allBlobs = packageBackupsBlobContainer.ListBlobs(prefix: packageIdIsh); - bool empty = true; - foreach (var blob in allBlobs) - { - empty = false; - Log.Trace(blob.Uri); - } - - if (empty) - { - Log.Trace("No matching blobs found"); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/LowerCaseAllPackageBlobsTask.cs b/src/NuGetGallery.Operations/Tasks/LowerCaseAllPackageBlobsTask.cs deleted file mode 100644 index 244f1ac059..0000000000 --- a/src/NuGetGallery.Operations/Tasks/LowerCaseAllPackageBlobsTask.cs +++ /dev/null @@ -1,44 +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.Threading.Tasks; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("lowercaseallpackageblobs", "Standardize all blob names as lowercase so that retrieving a package doesn't need to be case insensitive or disambiguated with use of the database", AltName = "lca", MaxArgs = 0, IsSpecialPurpose = true)] - public class LowerCaseAllPackageBlobsTask : StorageTask - { - public override void ExecuteCommand() - { - var blobContainer = Util.GetPackagesBlobContainer(CreateBlobClient()); - var allBlobsLazy = blobContainer.ListBlobs(); - Parallel.ForEach(allBlobsLazy, blob => - { - int p = blob.Uri.AbsolutePath.LastIndexOf('/'); - string blobName = blob.Uri.AbsolutePath.Substring(p); - string lowerCaseName = blobName.ToLowerInvariant(); - if (string.Equals(blobName, lowerCaseName, StringComparison.Ordinal)) - { - Log.Info("already lower case: " + blobName); - } - else - { - var newBlob = blobContainer.GetBlockBlobReference(lowerCaseName); - if (newBlob.Exists()) - { - Log.Info("already converted: " + lowerCaseName); - } - else - { - Log.Info("async blob copy" + blobName + " => " + lowerCaseName); - { - newBlob.StartCopyFromBlob(blob.Uri); - } - } - } - }); - - Log.Info("Finished processing all blobs"); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Monitoring/CleanJobLogsTask.cs b/src/NuGetGallery.Operations/Tasks/Monitoring/CleanJobLogsTask.cs deleted file mode 100644 index 70bf00b191..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Monitoring/CleanJobLogsTask.cs +++ /dev/null @@ -1,58 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using NuGetGallery.Operations.Infrastructure; - -namespace NuGetGallery.Operations.Tasks.Monitoring -{ - [Command("cleanjoblogs", "Cleans old job logs", AltName="cjl")] - public class CleanJobLogsTask : DiagnosticsStorageTask - { - [Option("The maximum allowed age. Provide a value as expected by TimeSpan.Parse. Default: 07.00:00 (7 days)")] - public TimeSpan? MaxAge { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (MaxAge == null) - { - MaxAge = TimeSpan.FromDays(7); - } - } - - public override void ExecuteCommand() - { - // Start by fetching the latest log - var joblogs = JobLog.LoadJobLogs(StorageAccount); - - // Iterate over each log - foreach (var joblog in joblogs) - { - Log.Info("Cleaning {0}", joblog.JobName); - foreach (var blob in joblog.Blobs.Where(b => (DateTime.UtcNow - b.ArchiveTimestamp) > MaxAge.Value)) - { - try - { - if (!WhatIf) - { - // Only delete if it matches. - blob.Blob.DeleteIfExists( - accessCondition: AccessCondition.GenerateIfMatchCondition(blob.Blob.Properties.ETag)); - } - Log.Info("Deleted {0}", blob.Blob.Name); - } - catch (Exception ex) - { - Log.ErrorException("Failed to delete " + blob.Blob.Name, ex); - } - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Monitoring/ListJobLogsTask.cs b/src/NuGetGallery.Operations/Tasks/Monitoring/ListJobLogsTask.cs deleted file mode 100644 index 660813f7a7..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Monitoring/ListJobLogsTask.cs +++ /dev/null @@ -1,30 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using NuGetGallery.Operations.Common; -using NuGetGallery.Operations.Infrastructure; - -namespace NuGetGallery.Operations.Tasks.Monitoring -{ - [Command("listjoblogs", "Lists available job logs", AltName="ljl")] - public class ListJobLogsTask : DiagnosticsStorageTask - { - public override void ExecuteCommand() - { - var joblogs = JobLog.LoadJobLogs(StorageAccount); - - // List logs! - Log.Info("Available Logs: "); - foreach (var log in joblogs) - { - Log.Info("* {0}", log.JobName); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Monitoring/TailJobLogTask.cs b/src/NuGetGallery.Operations/Tasks/Monitoring/TailJobLogTask.cs deleted file mode 100644 index c85ef3a7e3..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Monitoring/TailJobLogTask.cs +++ /dev/null @@ -1,104 +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 System.Globalization; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using NLog; -using NuGetGallery.Operations.Infrastructure; - -namespace NuGetGallery.Operations.Tasks.Monitoring -{ - [Command("tailjoblog", "Show the last few entries from a job log and optionally polls for additional results", AltName = "tjl", MinArgs = 1, MaxArgs = 1)] - public class TailJobLogTask : DiagnosticsStorageTask - { - private DateTimeOffset _lastEntryUtc = DateTimeOffset.MinValue; - - [Option("The number of entries to retrieve from the log", AltName = "n")] - public int? NumberOfEntries { get; set; } - - [Option("Set this switch to poll for additional log entries.", AltName = "f")] - public bool Follow { get; set; } - - [Option("The time interval at which to poll for new job log entries", AltName = "p")] - public TimeSpan? PollingPeriod { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - NumberOfEntries = NumberOfEntries ?? 10; - PollingPeriod = PollingPeriod ?? TimeSpan.FromSeconds(5); - } - - public override void ExecuteCommand() - { - var jobName = Arguments[0]; - - // Start by fetching the latest log - var joblogs = JobLog.LoadJobLogs(StorageAccount); - - // Grab the log - var candidates = joblogs.Where(l => l.JobName.StartsWith(jobName, StringComparison.OrdinalIgnoreCase)).ToList(); - if (!candidates.Any()) - { - Log.Error("No logs match: {0}", jobName); - } - else if (candidates.Count > 1) - { - Log.Error("Multiple logs match: {0}. Found: {1}", jobName, String.Join(", ", candidates.Select(c => c.JobName))); - } - else - { - // Grab the requested entries - var log = candidates.Single(); - var entries = log.OrderedEntries().Take(NumberOfEntries.Value).Reverse(); - Log.Info("The following are from the Log for: {0}", log.JobName); - - var logger = LogManager.GetLogger("joblog." + log.JobName); - foreach (var entry in entries) - { - WriteEntry(logger, entry); - _lastEntryUtc = entry.Timestamp; - } - - if (Follow) - { - FollowLog(log); - } - } - } - - private void FollowLog(JobLog log) - { - // Wait for PollingPeriod seconds - Thread.Sleep(PollingPeriod.Value); - - // Grab new entries - var entries = log.OrderedEntries().TakeWhile(l => l.Timestamp > _lastEntryUtc).Take(NumberOfEntries.Value); - var logger = LogManager.GetLogger("joblog." + log.JobName); - foreach (var entry in entries) - { - WriteEntry(logger, entry); - _lastEntryUtc = entry.Timestamp; - } - } - - private static void WriteEntry(Logger logger, JobLogEntry entry) - { - LogEventInfo evt = new LogEventInfo( - entry.FullEvent.Level, - entry.FullEvent.LoggerName, - CultureInfo.CurrentCulture, - entry.FullEvent.FormattedMessage, - new object[0]) - { - TimeStamp = entry.Timestamp.LocalDateTime - }; - logger.Log(evt); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/OpsTask.cs b/src/NuGetGallery.Operations/Tasks/OpsTask.cs deleted file mode 100644 index 55325e4953..0000000000 --- a/src/NuGetGallery.Operations/Tasks/OpsTask.cs +++ /dev/null @@ -1,122 +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 System.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using NLog; -using NuGetGallery.Operations.Common; -using NuGetGallery.Operations.Infrastructure; - -namespace NuGetGallery.Operations -{ - public abstract class OpsTask : ICommand - { - private const string CommandSuffix = "Task"; - - private CommandAttribute _commandAttribute; - private List _arguments = new List(); - private Logger _logger; - - public Logger Log - { - get { return _logger ?? (_logger = LogManager.GetLogger("task." + GetType().Name)); } - set { _logger = value; } - } - - public CommandAttribute CommandAttribute - { - get - { - if (_commandAttribute == null) - { - _commandAttribute = GetCommandAttribute(); - } - return _commandAttribute; - } - } - - public IList Arguments - { - get { return _arguments; } - } - - [Import] - public HelpCommand HelpCommand { get; set; } - - public DeploymentEnvironment CurrentEnvironment - { - get - { - return DeploymentEnvironment.FromEnvironment(); - } - } - - public string EnvironmentName { get { return CurrentEnvironment?.EnvironmentName; } } - - [Option("Gets help for this command", AltName = "?")] - public bool Help { get; set; } - - [Option("Instead of performing any write operations, the command will just output what it WOULD do. Read operations are still performed.", AltName = "!")] - public bool WhatIf { get; set; } - - protected internal IDbExecutorFactory DbFactory { get; set; } - - protected OpsTask() - { - DbFactory = new SqlDbExecutorFactory(); - } - - public void Execute() - { - if (!String.IsNullOrEmpty(EnvironmentName)) - { - Log.Info("Running against {0} environment", EnvironmentName); - } - - if (WhatIf) - { - Log.Info("Running in WhatIf mode"); - } - if (Help) - { - HelpCommand.ViewHelpForCommand(CommandAttribute.CommandName); - } - else - { - ValidateArguments(); - ExecuteCommand(); - } - } - - public abstract void ExecuteCommand(); - - public virtual void ValidateArguments() - { - } - - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method does quite a bit of processing.")] - public virtual CommandAttribute GetCommandAttribute() - { - var attributes = GetType().GetCustomAttributes(typeof(CommandAttribute), true); - if (attributes.Any()) - { - return (CommandAttribute)attributes.FirstOrDefault(); - } - - // Use the command name minus the suffix if present and default description - string name = GetType().Name; - int idx = name.LastIndexOf(CommandSuffix, StringComparison.OrdinalIgnoreCase); - if (idx >= 0) - { - name = name.Substring(0, idx); - } - if (!String.IsNullOrEmpty(name)) - { - return new CommandAttribute(name, TaskResources.DefaultCommandDescription); - } - return null; - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/PopulatePackageFrameworksTask.cs b/src/NuGetGallery.Operations/Tasks/PopulatePackageFrameworksTask.cs deleted file mode 100644 index 049a2dbf98..0000000000 --- a/src/NuGetGallery.Operations/Tasks/PopulatePackageFrameworksTask.cs +++ /dev/null @@ -1,344 +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 System.Data.SqlClient; -using System.IO; -using System.Linq; -using System.Threading; -using AnglicanGeek.DbExecutor; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; -using NuGet.Packaging; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("populatepackageframeworks", "Populate the Package Frameworks index in the database from the data in the storage server", AltName = "pfx", IsSpecialPurpose = true)] - public class PopulatePackageFrameworksTask : DatabaseAndStorageTask - { - private static readonly int _padLength = Enum.GetValues(typeof(PackageReportState)).Cast().Select(p => p.ToString().Length).Max(); - private static readonly JsonSerializer _serializer = new JsonSerializer() - { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - TypeNameHandling = TypeNameHandling.None, - Formatting = Formatting.Indented, - DateFormatHandling = DateFormatHandling.IsoDateFormat, - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - }; - - private readonly string _tempFolder; - - [Option("directory in which to put resume data and other work", AltName = "w")] - public string WorkDirectory { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - ArgCheck.Required(WorkDirectory, "WorkDirectory"); - } - - public PopulatePackageFrameworksTask() - { - _tempFolder = Path.Combine(Path.GetTempPath(), "NuGetGalleryOps"); - Directory.CreateDirectory(_tempFolder); - } - - public override void ExecuteCommand() - { - if (!Directory.Exists(WorkDirectory)) - { - Directory.CreateDirectory(WorkDirectory); - } - - Log.Info("Getting all package metadata..."); - var packages = GetAllPackages(); - - var totalCount = packages.Count; - var processedCount = 0; - Log.Info( - "Populating frameworks for {0} packages on '{1}',", - totalCount, - ConnectionString); - - - packages - .AsParallel() - .AsOrdered() - .WithDegreeOfParallelism(10) - .ForAll(package => - { - // Allocate a processed count number for this package - var thisPackageId = Interlocked.Increment(ref processedCount); - - try - { - var reportPath = Path.Combine(WorkDirectory, package.Id + "_" + package.Version + ".json"); - var bustedReportPath = Path.Combine(WorkDirectory, package.Id + "_" + package.Version + "_" + package.Hash + ".json"); - - var report = new PackageFrameworkReport() - { - Id = package.Id, - Version = package.Version, - Key = package.Key, - Hash = package.Hash, - Created = package.Created.Value, - State = PackageReportState.Unresolved - }; - - if (File.Exists(bustedReportPath)) - { - File.Move(bustedReportPath, reportPath); - } - - if (File.Exists(reportPath)) - { - using (var reader = File.OpenText(reportPath)) - { - report = (PackageFrameworkReport)_serializer.Deserialize(reader, typeof(PackageFrameworkReport)); - } - ResolveReport(report); - } - else - { - try - { - var downloadPath = DownloadPackage(package); - - using (var nugetPackage = new PackageArchiveReader(File.OpenRead(downloadPath))) - { - var supportedFrameworks = GetSupportedFrameworks(nugetPackage); - report.PackageFrameworks = supportedFrameworks.ToArray(); - report = PopulateFrameworks(package, report); - } - - File.Delete(downloadPath); - - // Resolve the report - ResolveReport(report); - } - catch (Exception ex) - { - report.State = PackageReportState.Error; - report.Error = ex.ToString(); - } - } - - using (var writer = File.CreateText(reportPath)) - { - _serializer.Serialize(writer, report); - } - - Log.Info("[{2}/{3} {4}%] {6} Package: {0}@{1} (created {5})", - package.Id, - package.Version, - thisPackageId.ToString("000000"), - totalCount.ToString("000000"), - (((double)thisPackageId / (double)totalCount) * 100).ToString("000.00"), - package.Created.Value, - report.State.ToString().PadRight(_padLength, ' ')); - } - catch (Exception ex) - { - Log.Error("[{2}/{3} {4}%] Error For Package: {0}@{1}: {5}", - package.Id, - package.Version, - thisPackageId.ToString("000000"), - totalCount.ToString("000000"), - (((double)thisPackageId / (double)totalCount) * 100).ToString("000.00"), - ex.ToString()); - } - }); - } - - private void ResolveReport(PackageFrameworkReport report) - { - bool error = false; - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - foreach (var operation in report.Operations) - { - if (!WhatIf) - { - if (operation.Type == PackageFrameworkOperationType.Add) - { - try - { - dbExecutor.Execute(@" - INSERT INTO PackageFrameworks(TargetFramework, Package_Key) - VALUES (@targetFramework, @packageKey)", - new - { - targetFramework = operation.Framework, - packageKey = report.Key - }); - Log.Info(" + Id={0}, Key={1}, Fx={2}", report.Id, report.Key, operation.Framework); - operation.Applied = true; - } - catch (Exception ex) - { - error = true; - operation.Applied = false; - operation.Error = ex.ToString(); - } - } - else if (operation.Type == PackageFrameworkOperationType.Remove) - { - try - { - dbExecutor.Execute(@" - DELETE FROM PackageFrameworks - WHERE TargetFramework = @targetFramework AND Package_Key = @packageKey", - new - { - targetFramework = operation.Framework, - packageKey = report.Key - }); - Log.Info(" - Id={0}, Key={1}, Fx={2}", report.Id, report.Key, operation.Framework); - operation.Applied = true; - } - catch (Exception ex) - { - error = true; - operation.Applied = false; - operation.Error = ex.ToString(); - } - } - } - } - } - - if (error) - { - report.State = PackageReportState.Error; - } - else if (report.Operations.All(o => o.Applied)) - { - report.State = PackageReportState.Resolved; - } - } - - string DownloadPackage(Package package) - { - var cloudClient = CreateBlobClient(); - - var packagesBlobContainer = Util.GetPackagesBlobContainer(cloudClient); - - var packageFileName = Util.GetPackageFileName(package.Id, package.Version); - - var downloadPath = Path.Combine(_tempFolder, packageFileName); - - var blob = packagesBlobContainer.GetBlockBlobReference(packageFileName); - blob.DownloadToFile(downloadPath); - - return downloadPath; - } - - IList GetAllPackages() - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - var packages = dbExecutor.Query(@" - SELECT p.[Key], pr.Id, p.Version, p.Hash, p.Created - FROM Packages p - JOIN PackageRegistrations pr on pr.[Key] = p.PackageRegistrationKey - ORDER BY p.Created DESC"); - return packages.ToList(); - } - } - - PackageFrameworkReport PopulateFrameworks( - Package package, - PackageFrameworkReport report) - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - - // Get all target frameworks in the db for this package - report.DatabaseFrameworks = new HashSet(dbExecutor.Query(@" - SELECT TargetFramework - FROM PackageFrameworks - WHERE Package_Key = @packageKey", - new - { - packageKey = package.Key - })).ToArray(); - - var adds = report.PackageFrameworks.Except(report.DatabaseFrameworks).Select(targetFramework => - new PackageFrameworkOperation() - { - Type = PackageFrameworkOperationType.Add, - Framework = targetFramework, - Applied = false, - Error = "Not Started" - }); - var rems = report.DatabaseFrameworks.Except(report.PackageFrameworks).Select(targetFramework => - new PackageFrameworkOperation() - { - Type = PackageFrameworkOperationType.Remove, - Framework = targetFramework, - Applied = false, - Error = "Not Started" - }); - - report.Operations = Enumerable.Concat(adds, rems).ToArray(); - } - - return report; - } - - private static IEnumerable GetSupportedFrameworks(PackageArchiveReader packageArchiveReader) - { - return packageArchiveReader - .GetSupportedFrameworks() - .Select(fn => fn.ToShortNameOrNull()) - .ToArray(); - } - - public class PackageFrameworkReport - { - public string Id { get; set; } - public string Version { get; set; } - public int Key { get; set; } - public string Hash { get; set; } - public DateTime Created { get; set; } - public string[] DatabaseFrameworks { get; set; } - public string[] PackageFrameworks { get; set; } - public PackageFrameworkOperation[] Operations { get; set; } - public string Error { get; set; } - - [JsonConverter(typeof(StringEnumConverter))] - public PackageReportState State { get; set; } - } - - public class PackageFrameworkOperation - { - [JsonConverter(typeof(StringEnumConverter))] - public PackageFrameworkOperationType Type { get; set; } - public string Framework { get; set; } - public bool Applied { get; set; } - public string Error { get; set; } - } - - public enum PackageFrameworkOperationType - { - Add, - Remove - } - - public enum PackageReportState { - Unresolved, - Resolved, - Error - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/ReplacePackageFileTask.cs b/src/NuGetGallery.Operations/Tasks/ReplacePackageFileTask.cs deleted file mode 100644 index 2db73433f9..0000000000 --- a/src/NuGetGallery.Operations/Tasks/ReplacePackageFileTask.cs +++ /dev/null @@ -1,65 +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.Data.SqlClient; -using System.IO; -using AnglicanGeek.DbExecutor; -using NuGet; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("replacepackagefile", "Replaces a specific package file with a file you specify", AltName = "rpf")] - public class ReplacePackageFileTask : DatabasePackageVersionTask - { - [Option("The file to replace the package with", AltName = "r")] - public Stream ReplacementFile { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(ReplacementFile, "ReplacementFile"); - } - - public override void ExecuteCommand() - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - - var package = Util.GetPackage( - dbExecutor, - PackageId, - PackageVersion); - - if (package == null) - { - Log.Info("Package '{0}.{1}' does not exist; exiting."); - return; - } - - new BackupPackageFileTask { - StorageAccount = StorageAccount, - PackageId = package.Id, - PackageVersion = package.Version, - PackageHash = package.Hash - }.ExecuteCommand(); - - var hash = Util.GenerateHash(ReplacementFile.AsSeekableStream()); - Log.Info("Updating hash for package '{0}.{1}' to '{2}'", package.Id, package.Version, hash); - dbExecutor.Execute( - "UPDATE Packages SET Hash = @hash WHERE [Key] = @key", - new { @key = package.Key, hash }); - - Log.Info("Uploading replacement file for package '{0}.{1}'", package.Id, package.Version); - ReplacementFile.Position = 0; - new UploadPackageTask { - StorageAccount = StorageAccount, - PackageId = package.Id, - PackageVersion = package.Version, - PackageFile = ReplacementFile - }.ExecuteCommand(); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/RestoreDatabaseTask.cs b/src/NuGetGallery.Operations/Tasks/RestoreDatabaseTask.cs deleted file mode 100644 index 92947c88fd..0000000000 --- a/src/NuGetGallery.Operations/Tasks/RestoreDatabaseTask.cs +++ /dev/null @@ -1,111 +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.Data.SqlClient; -using System.Threading; -using AnglicanGeek.DbExecutor; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("restoredb", "Restore a database backup which already resides on the specified database server", AltName = "rdb")] - public class RestoreDatabaseTask : DatabaseTask - { - [Option("Name of the backup file", AltName = "n")] - public string BackupName { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(BackupName, "BackupName"); - } - - public override void ExecuteCommand() - { - using (var masterDbConnection = new SqlConnection(Util.GetMasterConnectionString(ConnectionString.ConnectionString))) - using (var masterDbExecutor = new SqlExecutor(masterDbConnection)) - { - masterDbConnection.Open(); - - var restoreDbName = CopyDatabaseForRestore( - masterDbExecutor); - - using (var restoreDbConnection = new SqlConnection(Util.GetConnectionString(ConnectionString.ConnectionString, restoreDbName))) - using (var restoreDbExecutor = new SqlExecutor(restoreDbConnection)) - { - restoreDbConnection.Open(); - - PrepareDataForRestore( - restoreDbExecutor); - - RenameLiveDatabase( - masterDbExecutor); - - RenameDatabaseBackup( - masterDbExecutor, - restoreDbName); - } - } - } - - private string CopyDatabaseForRestore( - SqlExecutor masterDbExecutor) - { - var restoreDbName = $"Restore_{Util.GetTimestamp()}"; - Log.Info("Copying {0} to {1}.", BackupName, restoreDbName); - masterDbExecutor.Execute($"CREATE DATABASE {restoreDbName} AS COPY OF {BackupName}"); - Log.Info("Waiting for copy to complete."); - WaitForBackupCopy( - masterDbExecutor, - restoreDbName); - return restoreDbName; - } - - private void WaitForBackupCopy( - SqlExecutor masterDbExecutor, - string restoreDbName) - { - var timeToGiveUp = DateTime.UtcNow.AddHours(1).AddSeconds(30); - while (DateTime.UtcNow < timeToGiveUp) - { - if (Util.DatabaseExistsAndIsOnline( - masterDbExecutor, - restoreDbName)) - { - Log.Info("Copy is complete."); - return; - } - Thread.Sleep(1 * 60 * 1000); - } - } - - private void PrepareDataForRestore( - IDbExecutor dbExecutor) - { - Log.Info("Deleting incomplete jobs."); - dbExecutor.Execute("DELETE FROM WorkItems WHERE Completed IS NULL"); - Log.Info("Deleted incomplete jobs."); - } - - private void RenameDatabaseBackup( - IDbExecutor masterDbExecutor, - string restoreDbName) - { - Log.Info("Renaming {0} to NuGetGallery.", restoreDbName); - var sql = $"ALTER DATABASE {restoreDbName} MODIFY Name = NuGetGallery"; - masterDbExecutor.Execute(sql); - Log.Info("Renamed {0} to NuGetGallery.", restoreDbName); - } - - private void RenameLiveDatabase( - IDbExecutor masterDbExecutor) - { - var timestamp = Util.GetTimestamp(); - var liveDbName = "Live_" + timestamp; - Log.Info("Renaming NuGetGallery to {0}.", liveDbName); - var sql = $"ALTER DATABASE NuGetGallery MODIFY Name = {liveDbName}"; - masterDbExecutor.Execute(sql); - Log.Info("Renamed NuGetGallery to {0}.", liveDbName); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/RestorePackagesTask.cs b/src/NuGetGallery.Operations/Tasks/RestorePackagesTask.cs deleted file mode 100644 index e3bbf7b016..0000000000 --- a/src/NuGetGallery.Operations/Tasks/RestorePackagesTask.cs +++ /dev/null @@ -1,131 +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 System.Data.SqlClient; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AnglicanGeek.DbExecutor; - -namespace NuGetGallery.Operations -{ - [Command("restorepackages", "Restore Packages from Backup", AltName = "rps")] - public class RestorePackagesTask : DatabaseAndStorageTask - { - private readonly string _tempFolder; - - public RestorePackagesTask() - { - _tempFolder = Path.Combine(Path.GetTempPath(), "NuGetGalleryOps"); - Directory.CreateDirectory(_tempFolder); - } - - string DownloadPackageBackup( - string id, - string version, - string hash) - { - var blobClient = CreateBlobClient(); - var packageBackupsBlobContainer = Util.GetPackageBackupsBlobContainer(blobClient); - var packageBackupFileName = Util.GetPackageBackupFileName( - id, - version, - hash); - var packageBackupBlob = packageBackupsBlobContainer.GetBlockBlobReference(packageBackupFileName); - var downloadPath = Path.Combine(_tempFolder, packageBackupFileName); - packageBackupBlob.DownloadToFile(downloadPath); - return downloadPath; - } - - public override void ExecuteCommand() - { - Log.Info("Getting list of packages to restore; this will take some time."); - var packages = GetPackages(); - var packageBlobFileNames = GetPackageBlobFileNames(); - var packageFileNamesToRestore = packages.Keys.Except(packageBlobFileNames).ToList(); - - var totalCount = packageFileNamesToRestore.Count; - var processedCount = 0; - Log.Info( - "Restoring {0} packages in storage account '{1}'.", - totalCount, - StorageAccountName); - - Parallel.ForEach(packageFileNamesToRestore, new ParallelOptions { MaxDegreeOfParallelism = 10 }, packageFileNameToRestore => - { - var package = packages[packageFileNameToRestore]; - try - { - var downloadPath = DownloadPackageBackup( - package.Id, - package.Version, - package.Hash); - UploadPackage( - package.Id, - package.Version, - downloadPath); - File.Delete(downloadPath); - Interlocked.Increment(ref processedCount); - Log.Info( - "Restored package '{0}.{1}' ({2} of {3}).", - package.Id, - package.Version, - processedCount, - totalCount); - } - catch (Exception ex) - { - Interlocked.Increment(ref processedCount); - Log.Info( - "Error restoring package '{0}.{1}' ({2} of {3}): {4}.", - package.Id, - package.Version, - processedCount, - totalCount, - ex.Message); - } - }); - } - - private IEnumerable GetPackageBlobFileNames() - { - var blobClient = CreateBlobClient(); - var packagesBlobContainer = Util.GetPackagesBlobContainer(blobClient); - return packagesBlobContainer.ListBlobs().Select(bi => bi.Uri.Segments.Last()); - } - - IDictionary GetPackages() - { - using (var sqlConnection = new SqlConnection(ConnectionString.ConnectionString)) - using (var dbExecutor = new SqlExecutor(sqlConnection)) - { - sqlConnection.Open(); - var packages = dbExecutor.Query(@" - SELECT pr.Id, p.Version, p.Hash - FROM Packages p - JOIN PackageRegistrations pr ON pr.[Key] = p.PackageRegistrationKey - WHERE p.ExternalPackageUrl IS NULL - ORDER BY Id, Version, Hash"); - return packages.ToDictionary(p => Util.GetPackageFileName(p.Id, p.Version)); - } - } - - void UploadPackage( - string id, - string version, - string downloadPath) - { - var blobClient = CreateBlobClient(); - var packagesBlobContainer = Util.GetPackagesBlobContainer(blobClient); - var packageFileName = Util.GetPackageFileName( - id, - version); - var packageBlob = packagesBlobContainer.GetBlockBlobReference(packageFileName); - packageBlob.UploadFile(downloadPath); - packageBlob.Properties.ContentType = "application/zip"; - packageBlob.SetProperties(); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/RunMigrationsTask.cs b/src/NuGetGallery.Operations/Tasks/RunMigrationsTask.cs deleted file mode 100644 index 0fafa7f81d..0000000000 --- a/src/NuGetGallery.Operations/Tasks/RunMigrationsTask.cs +++ /dev/null @@ -1,132 +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 System.Data.Entity.Migrations; -using System.Data.Entity.Migrations.Infrastructure; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("runmigrations", "Executes migrations against a database", AltName = "rm", MaxArgs = 0)] - public class RunMigrationsTask : MigrationsTask - { - private static readonly Regex MigrationIdRegex = new Regex(@"^(?\d+)_(?.*)$"); - - [Option("The target to migrate the database to. Timestamp does not need to be specified.", AltName = "m")] - public string TargetMigration { get; set; } - - [Option("Set this to generate a SQL File instead of running the migration. The file will be dropped in the current directory and named [Source]-[TargetMigration].sql")] - public bool Sql { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(TargetMigration, "TargetMigration"); - } - - protected override void ExecuteCommandCore(MigratorBase migrator) - { - if (Sql) - { - ScriptMigrations(migrator); - } - else - { - RunMigrations(migrator); - } - } - - private void ScriptMigrations(MigratorBase migrator) - { - var scriptingMigrator = new MigratorScriptingDecorator(migrator); - - string start; - string target; - if (migrator.GetDatabaseMigrations().Any(s => IsMigration(s, TargetMigration))) - { - // Down migration, start is null, target is the target - start = null; - target = migrator.GetDatabaseMigrations().Single(s => IsMigration(s, TargetMigration)); - } - else - { - // Up migration, go from start to target. - start = migrator.GetDatabaseMigrations().FirstOrDefault(); - target = migrator.GetLocalMigrations().Single(s => IsMigration(s, TargetMigration)); - } - - string startName = start ?? migrator.GetDatabaseMigrations().FirstOrDefault(); - string scriptFileName = $"{startName}-{target}.sql"; - if (File.Exists(scriptFileName)) { - Log.Error("File already exists: {0}", scriptFileName); - return; - } - - // Generate script - Log.Info("Scripting migration from {0} to {1}", startName, target); - if (!WhatIf) - { - string script = scriptingMigrator.ScriptUpdate(start, target); - - // Write the script - File.WriteAllText(scriptFileName, script); - } - Log.Info("Wrote script to {0}", scriptFileName); - } - - private void RunMigrations(MigratorBase migrator) - { - // We only support UP right now. - // Find the target migration and collect everything between the start and it - var toApply = new List(); - - // TakeWhile won't work because it doesn't include the actual target :( - foreach (var migration in migrator.GetPendingMigrations()) - { - toApply.Add(migration); - if (IsMigration(migration, TargetMigration)) - { - break; - } - } - - if (!toApply.Any(s => IsMigration(s, TargetMigration))) - { - Log.Error("{0} is not a pending migration. Only the UP direction can be run in this way. Use the -Sql option to script downwards migrations.", TargetMigration); - return; - } - - // We have a list of migrations to apply, apply them one-by-one - foreach (var migration in toApply) - { - Log.Info("Applying {0}", migration); - if (!WhatIf) - { - migrator.Update(migration); - } - } - Log.Info("All requested migrations applied"); - } - - private static bool IsMigration(string migrationId, string target) - { - // Get the shortname - var match = MigrationIdRegex.Match(migrationId); - if (!match.Success) - { - return String.Equals(migrationId, target, StringComparison.OrdinalIgnoreCase); - } - else - { - var name = match.Groups["name"].Value; - return String.Equals(name, target, StringComparison.OrdinalIgnoreCase); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/SanitizeDatabaseTask.cs b/src/NuGetGallery.Operations/Tasks/SanitizeDatabaseTask.cs deleted file mode 100644 index f14483a535..0000000000 --- a/src/NuGetGallery.Operations/Tasks/SanitizeDatabaseTask.cs +++ /dev/null @@ -1,84 +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 System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Web.Helpers; -using AnglicanGeek.DbExecutor; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("sanitizedatabase", "Cleans Personally-Identified Information out of a database without destroying data", AltName = "sdb", MinArgs = 0, MaxArgs = 0)] - public class SanitizeDatabaseTask : DatabaseTask - { - private const string SanitizeUsersQuery = @" - UPDATE Users - SET ApiKey = NEWID(), - EmailAddress = [Username] + '@' + @emailDomain, - UnconfirmedEmailAddress = NULL, - HashedPassword = CAST(NEWID() AS NVARCHAR(MAX)), - EmailAllowed = 1, - EmailConfirmationToken = NULL, - PasswordResetToken = NULL, - PasswordResetTokenExpirationDate = NULL, - PasswordHashAlgorithm = 'PBKDF2' - WHERE [Key] NOT IN (SELECT ur.UserKey FROM UserRoles ur INNER JOIN Roles r ON r.[Key] = ur.RoleKey WHERE r.Name = 'Admins')"; - - private static readonly string[] AllowedPrefixes = new[] { - "Export_" // Only exports can be sanitized - }; - - [Option("Domain name to use for sanitized email addresses, username@[emaildomain]", AltName = "e")] - public string EmailDomain { get; set; } - - [Option("Forces the command to run, even against a non-backup/export database", AltName = "f")] - public bool Force { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - EmailDomain = String.IsNullOrEmpty(EmailDomain) ? - "example.com" : - EmailDomain; - } - - public override void ExecuteCommand() - { - // Verify the name - if (!Force && !AllowedPrefixes.Any(p => ConnectionString.InitialCatalog.StartsWith(p, StringComparison.OrdinalIgnoreCase))) - { - Log.Error("Cannot sanitize database named '{0}' without -Force argument", ConnectionString.InitialCatalog); - return; - } - Log.Info("Ready to sanitize {0} on {1}", ConnectionString.InitialCatalog, Util.GetDatabaseServerName(ConnectionString)); - - // All we need to sanitize is the user table. Package data is public (EVEN unlisted ones) and not PII - if (WhatIf) - { - Log.Trace("Would execute the following SQL:"); - Log.Trace(SanitizeUsersQuery); - Log.Trace("With @emailDomain = " + EmailDomain); - } - else - { - using (SqlConnection connection = new SqlConnection(ConnectionString.ConnectionString)) - using (SqlExecutor dbExecutor = new SqlExecutor(connection)) - { - connection.Open(); - try - { - var count = dbExecutor.Execute(SanitizeUsersQuery, new { emailDomain = EmailDomain }); - Log.Info("Sanitization complete. {0} Users affected", count); - } - catch (Exception ex) - { - Log.Error(ex.ToString()); - } - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/SendMailTask.cs b/src/NuGetGallery.Operations/Tasks/SendMailTask.cs deleted file mode 100644 index e5ddea19c0..0000000000 --- a/src/NuGetGallery.Operations/Tasks/SendMailTask.cs +++ /dev/null @@ -1,193 +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 System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Data.SqlClient; -using AnglicanGeek.DbExecutor; -using NuGetGallery.Operations.Infrastructure; -using System.Net.Mail; -using System.Net; -using System.Security.Cryptography.X509Certificates; -using System.Net.Security; -using System.Net.Mime; -using System.Web; -using System.Web.Helpers; -using System.Web.UI; -using System.IO; -using System.Data; - -namespace NuGetGallery.Operations -{ - [Command("sendmail", "sends mail to the package owners", AltName = "sm", MaxArgs = 0)] - public class SendMailTask : DatabaseTask - { - [Option("Email account to be used to send the mail", AltName = "ua")] - public string UserAccount { get; set; } - - [Option("Email password to be used to send the mail", AltName = "p")] - public string Password { get; set; } - - [Option("Email host to be used to send the mail", AltName = "eh")] - public string EmailHost { get; set; } - - [Option("Comma separated list of To addresses", AltName = "to")] - public string ToList { get; set; } - - [Option("Comma separated list of ReplyTo addresses", AltName = "replyto")] - public string ReplyToList { get; set; } - - [Option("Subject of the email", AltName = "ms")] - public string MailSubject { get; set; } - - [Option("Body of the email", AltName = "mc")] - public string MailContent { get; set; } - - [Option("Comma separated list of packages whose owners need to be contacted", AltName = "pn")] - public string PackageIds { get; set; } - - [Option("Full path to the file which has the formatted mail content. Used if MailContent Arg is not specified.", AltName = "mcf")] - public string MailContentFilePath { get; set; } - - [Option("Full path to the file which has the list of package names - with individual package name represented in a line. USed if PackageIds Arg is not specified", AltName = "pnf")] - public string PackageIdsFilePath { get; set; } - - - public override void ExecuteCommand() - { - System.Threading.Thread.Sleep(30 * 1000); - //Construct the SMTP host. - SmtpClient sc = new SmtpClient("smtphost"); - NetworkCredential nc = new NetworkCredential(UserAccount, Password); - sc.UseDefaultCredentials = false; - sc.Credentials = nc; - sc.Host = EmailHost; - sc.EnableSsl = true; - sc.Port = 25; - sc.EnableSsl = true; - ServicePointManager.ServerCertificateValidationCallback = delegate(object s, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { return true; }; - System.Net.Mail.MailMessage message = new System.Net.Mail.MailMessage(); - - //Set the from and to mail addressess. - message.From = new MailAddress(UserAccount, "NuGet Gallery Support"); - - string[] replyTo = ReplyToList.Split(new char[] { ',' }); - foreach(string replyToAddress in replyTo) - message.ReplyToList.Add(new MailAddress(replyToAddress,replyToAddress)); - - string[] to = ToList.Split(new char[] { ',' }); - foreach (string toAddress in to) - message.To.Add(new MailAddress(toAddress, toAddress)); - - //Get the list of packages if present and add the owner email Ids to Bcc. - List PackagesList = GetPackageIds(); - if(PackagesList != null && PackagesList.Count > 0) - message.Bcc.AddRange(GetOwnerMailAddressess(this.ConnectionString.ToString(),PackagesList)); - - message.Subject = string.Format(MailSubject); - message.IsBodyHtml = true; - message.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(@"

" + GetMailContent() + "", new ContentType("text/html"))); - - try - { - sc.Send(message); - } - catch (Exception ex) - { - Console.WriteLine(NuGetGallery.Strings.ErrorInSendingMail, ex.Message); - } - - } - - #region PrivateMethods - private List GetPackageIds() - { - //Get the list of packages from either PackageIds or PackageIdsFilePath Argument. - List PackagesList = new List(); - if (!string.IsNullOrEmpty(PackageIds)) - { - PackagesList = PackageIds.Split(new char[] { ',' }).ToList(); - } - else if (!string.IsNullOrEmpty(PackageIdsFilePath)) - { - StreamReader packages = new StreamReader(PackageIdsFilePath); - while (packages.EndOfStream == false) - { - PackagesList.Add(packages.ReadLine()); - } - packages.Close(); - } - return PackagesList; - } - - private string GetMailContent() - { - //Get the content from the specified argument. - string body = string.Empty; - if(!string.IsNullOrEmpty(MailContent)) - { - body = MailContent; - } - else - { - StreamReader sr = new StreamReader(MailContentFilePath); - body = sr.ReadToEnd(); - sr.Close(); - } - //wrap it using htmlwriter so that the format of the text is preserved. - stringwriter = new StringWriter(); - htmlWriter = new HtmlTextWriter(stringwriter); - htmlWriter.RenderBeginTag(HtmlTextWriterTag.Pre); - htmlWriter.Write(body); - htmlWriter.RenderEndTag(); - htmlWriter.WriteLine(""); - return stringwriter.ToString(); - } - - private MailAddressCollection GetOwnerMailAddressess(string connectionString,List packageNames) - { - MailAddressCollection ownerEmailCollection = new MailAddressCollection(); - foreach(string package in packageNames) - { - //Get the owner mail address for each of the package. - string sqlemailaddress = @" - SELECT - [EmailAddress], - [Username], - [EmailAllowed] - FROM [dbo].[Users] where [key] IN (Select pro.[UserKey] from [dbo].[PackageRegistrationOwners] pro JOIN [dbo].[PackageRegistrations] pr on pro.[PackageRegistrationKey] = pr.[Key] where pr.[Id] = '{0}') - "; - - string emailaddress = string.Empty; - string username = string.Empty; - bool emailallowed = true; - using (SqlConnection connection = new SqlConnection(connectionString)) - { - connection.Open(); - SqlCommand command = new SqlCommand(string.Format(sqlemailaddress, package), connection); - command.CommandType = CommandType.Text; - SqlDataReader reader = command.ExecuteReader(); - while (reader.Read()) - { - emailaddress = (string)reader.GetValue(0); - username = (string)reader.GetValue(1); - emailallowed = (bool)reader.GetValue(2); - //Include it only if EmailAllowed option is turned on. - if(emailallowed) - ownerEmailCollection.Add(new MailAddress(emailaddress, username)); - } - } - } - return ownerEmailCollection; - } - #endregion PrivateMethods - - #region PrivateMembers - private static StringWriter stringwriter; - private static HtmlTextWriter htmlWriter; - - #endregion PrivateMembers - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Subscription/CreateCloudServiceTask.cs b/src/NuGetGallery.Operations/Tasks/Subscription/CreateCloudServiceTask.cs deleted file mode 100644 index 8ebd9cc827..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Subscription/CreateCloudServiceTask.cs +++ /dev/null @@ -1,54 +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 System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure; -using Microsoft.WindowsAzure.Management.Scheduler.Models; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks.Subscription -{ - [Command("createcloudservice", "Creates a new Cloud Service in the Subscription", AltName = "ccs", MinArgs = 1, MaxArgs = 1)] - public class CreateCloudServiceTask : SubscriptionTask - { - [Option("A description of the cloud service", AltName="d")] - public string Description { get; set; } - - [Option("An email address to attach to the cloud service", AltName = "e")] - public string Email { get; set; } - - [Option("The region in which to place the cloud service", AltName = "r")] - public string Region { get; set; } - - [Option("A label for the cloud service", AltName = "l")] - public string Label { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(Description, "Description"); - ArgCheck.Required(Email, "Email"); - ArgCheck.Required(Region, "Region"); - ArgCheck.Required(Label, "Label"); - } - - public override void ExecuteCommand() - { - var cred = new CertificateCloudCredentials(SubscriptionId, ManagementCertificate); - var csman = CloudContext.Clients.CreateCloudServiceManagementClient(cred); - var result = csman.CloudServices.CreateAsync( - Arguments[0], new CloudServiceCreateParameters() - { - Label = Label, - GeoRegion = Region, - Email = Email, - Description = Description - }, CancellationToken.None).Result; - Log.Info("Created Cloud Service {0}", Arguments[0]); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Subscription/CreateJobCollectionTask.cs b/src/NuGetGallery.Operations/Tasks/Subscription/CreateJobCollectionTask.cs deleted file mode 100644 index 927ff99772..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Subscription/CreateJobCollectionTask.cs +++ /dev/null @@ -1,99 +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 System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure; -using Microsoft.WindowsAzure.Management.Scheduler.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks.Subscription -{ - [Command("createjobcollection", "Creates a new Scheduler Job Collection in the Subscription", AltName = "cjc", MinArgs = 1, MaxArgs = 1)] - public class CreateJobCollectionTask : SubscriptionTask - { - [Option("The cloud service in which to create the collection", AltName = "cs")] - public string CloudService { get; set; } - - [Option("A label to give the job collection", AltName = "l")] - public string Label { get; set; } - - [Option("The plan for the collection", AltName = "p")] - public JobCollectionPlan Plan { get; set; } - - [Option("The maximum number of jobs in the collection", AltName = "maxj")] - public int? MaxJobCount { get; set; } - - [Option("The maximum job occurrence in the collection", AltName = "maxo")] - public int? MaxJobOccurrence { get; set; } - - [Option("The maximum recurrence frequency", AltName = "maxrf")] - public JobCollectionRecurrenceFrequency? MaxRecurrenceFrequency { get; set; } - - [Option("The maximum recurrence interval. Required if MaxRecurrenceFrequency is specified", AltName = "maxri")] - public int? MaxRecurrenceInterval { get; set; } - - public CreateJobCollectionTask() - { - Plan = JobCollectionPlan.Free; - } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - ArgCheck.Required(Label, "Label"); - ArgCheck.Required(CloudService, "CloudService"); - - if (MaxRecurrenceFrequency.HasValue) - { - ArgCheck.Required(MaxRecurrenceInterval, "MaxRecurrenceInterval"); - } - } - - public override void ExecuteCommand() - { - var cred = new CertificateCloudCredentials(SubscriptionId, ManagementCertificate); - var schman = CloudContext.Clients.CreateSchedulerManagementClient(cred); - - var createParams = new JobCollectionCreateParameters() - { - Label = Label, - IntrinsicSettings = new JobCollectionIntrinsicSettings() - { - Plan = Plan, - Quota = new JobCollectionQuota() - { - MaxJobCount = MaxJobCount, - MaxJobOccurrence = MaxJobOccurrence, - MaxRecurrence = MaxRecurrenceFrequency.HasValue ? new JobCollectionMaxRecurrence() - { - Frequency = MaxRecurrenceFrequency.Value, - Interval = MaxRecurrenceInterval.Value - } : null - } - } - }; - - if (WhatIf) - { - Log.Info("Would create job collection {0} in {1} with the following params:", Arguments[0], CloudService); - Log.Info(JsonConvert.SerializeObject(createParams, new StringEnumConverter())); - } - else - { - Log.Info("Creating job collection {0} in {1}...", Arguments[0], CloudService); - schman.JobCollections.CreateAsync( - CloudService, - Arguments[0], - createParams, - CancellationToken.None).Wait(); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/Subscription/ListCloudServicesTask.cs b/src/NuGetGallery.Operations/Tasks/Subscription/ListCloudServicesTask.cs deleted file mode 100644 index 8489dd0b79..0000000000 --- a/src/NuGetGallery.Operations/Tasks/Subscription/ListCloudServicesTask.cs +++ /dev/null @@ -1,27 +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 System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure; - -namespace NuGetGallery.Operations.Tasks.Subscription -{ - [Command("listcloudservices", "Lists available Cloud Services in the Subscription", AltName="lcs")] - public class ListCloudServicesTask : SubscriptionTask - { - public override void ExecuteCommand() - { - var cred = new CertificateCloudCredentials(SubscriptionId, ManagementCertificate); - var csman = CloudContext.Clients.CreateCloudServiceManagementClient(cred); - var svcs = csman.CloudServices.ListAsync(CancellationToken.None).Result; - foreach (var svc in svcs) - { - Log.Info("* {0} ({1}): {2}", svc.Name, svc.GeoRegion, svc.Description); - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/SynchronizePackageBackupsTask.cs b/src/NuGetGallery.Operations/Tasks/SynchronizePackageBackupsTask.cs deleted file mode 100644 index 5dd488977d..0000000000 --- a/src/NuGetGallery.Operations/Tasks/SynchronizePackageBackupsTask.cs +++ /dev/null @@ -1,58 +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 System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("syncpackagebackups", "Transfers package backups from the source storage server to the destination storage server", AltName = "spb")] - public class SynchronizePackageBackupsTask : OpsTask - { - [Option("Connection string to the source storage server", AltName = "ss")] - public CloudStorageAccount SourceStorage { get; set; } - - [Option("Connection string to the destination storage server", AltName = "ds")] - public CloudStorageAccount DestinationStorage { get; set; } - - private readonly string _tempFolder; - - public SynchronizePackageBackupsTask() - { - _tempFolder = Path.Combine(Path.GetTempPath(), "NuGetGalleryOps"); - Directory.CreateDirectory(_tempFolder); - } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (CurrentEnvironment != null) - { - if (DestinationStorage == null) - { - DestinationStorage = CurrentEnvironment.MainStorage; - } - } - ArgCheck.Required(SourceStorage, "SourceStorage"); - ArgCheck.RequiredOrConfig(DestinationStorage, "DestinationStorage"); - } - - public override void ExecuteCommand() - { - new CopyBlobsTask() { - SourceStorage = SourceStorage, - SourceContainer = "packagebackups", - DestinationStorage = DestinationStorage, - DestinationContainer = "packagebackups", - WhatIf = WhatIf, - Overwrite = false - }.Execute(); - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/TaskBases.cs b/src/NuGetGallery.Operations/Tasks/TaskBases.cs deleted file mode 100644 index 9142e5264c..0000000000 --- a/src/NuGetGallery.Operations/Tasks/TaskBases.cs +++ /dev/null @@ -1,351 +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 System.Linq; -using System.Data.Entity.Infrastructure; -using System.Data.Entity.Migrations; -using System.Data.Entity.Migrations.Infrastructure; -using System.Data.SqlClient; -using System.Reflection; -using AnglicanGeek.DbExecutor; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using NuGetGallery.Operations.Common; -using NuGetGallery.Infrastructure; -using Dapper; -using System.Security.Cryptography.X509Certificates; - -namespace NuGetGallery.Operations -{ - // Base classes to ensure consistent naming of options - - public abstract class StorageTaskBase : OpsTask - { - [Option("The connection string to the storage server", AltName = "st")] - public CloudStorageAccount StorageAccount { get; set; } - - protected string StorageAccountName - { - get { return StorageAccount.Credentials.AccountName; } - } - - protected CloudBlobClient CreateBlobClient() - { - return StorageAccount.CreateCloudBlobClient(); - } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (CurrentEnvironment != null && StorageAccount == null) - { - StorageAccount = GetStorageAccountFromEnvironment(CurrentEnvironment); - } - ArgCheck.RequiredOrConfig(StorageAccount, "StorageAccount"); - } - - protected abstract CloudStorageAccount GetStorageAccountFromEnvironment(DeploymentEnvironment environment); - } - - public abstract class StorageTask : StorageTaskBase - { - protected override CloudStorageAccount GetStorageAccountFromEnvironment(DeploymentEnvironment environment) - { - return environment.MainStorage; - } - } - - public abstract class BackupStorageTask : StorageTaskBase - { - protected override CloudStorageAccount GetStorageAccountFromEnvironment(DeploymentEnvironment environment) - { - return environment.BackupStorage; - } - } - - public abstract class DiagnosticsStorageTask : StorageTaskBase - { - protected override CloudStorageAccount GetStorageAccountFromEnvironment(DeploymentEnvironment environment) - { - return environment.DiagnosticsStorage; - } - } - - public abstract class DatabaseTaskBase : OpsTask - { - [Option("Connection string to the relevant database server", AltName = "db")] - public SqlConnectionStringBuilder ConnectionString { get; set; } - - [Option("Instead of -db, use this parameter to connect to a SQL LocalDb database of the specified name", AltName="ldb")] - public string LocalDbName { get; set; } - - protected string ServerName { get { return Util.GetDatabaseServerName(ConnectionString); } } - - - public override void ValidateArguments() - { - base.ValidateArguments(); - - // Load defaults from environment - if(ConnectionString == null) { - if (CurrentEnvironment != null) - { - ConnectionString = GetConnectionFromEnvironment(CurrentEnvironment); - } - } - - // Local Db Name overrides others - if (!String.IsNullOrEmpty(LocalDbName)) - { - ConnectionString = new SqlConnectionStringBuilder() - { - DataSource = @"(LocalDB)\v11.0", - IntegratedSecurity = true, - InitialCatalog = LocalDbName - }; - Log.Info("Using LocalDB connection: {0}", ConnectionString.ConnectionString); - } - - ArgCheck.RequiredOrConfig(ConnectionString, "ConnectionString"); - } - - protected void WithConnection(Action act) - { - WithConnection((c, _) => act(c)); - } - - protected void WithConnection(Action act) - { - WithConnection((c, e) => { act(c, e); return true; }); - } - - protected bool WithConnection(Func act) - { - using (var c = OpenConnection()) - using (var e = new SqlExecutor(c)) - { - return act(c, e); - } - } - - protected void WithMasterConnection(Action act) - { - WithMasterConnection((c, _) => act(c)); - } - - protected void WithMasterConnection(Action act) - { - WithMasterConnection((c, e) => { act(c, e); return true; }); - } - - protected bool WithMasterConnection(Func act) - { - using (var c = OpenMasterConnection()) - using (var e = new SqlExecutor(c)) - { - return act(c, e); - } - } - - protected static void WithTableType(SqlConnection connection, string name, string definition, Action act) - { - try - { - // Create the table-valued parameter type - connection.Execute(String.Format(@" - IF EXISTS ( - SELECT * - FROM sys.types - WHERE is_table_type = 1 - AND name = '{0}' - ) - BEGIN - DROP TYPE {0} - END - CREATE TYPE {0} AS TABLE ({1})", name, definition)); - - act(); - } - finally - { - // Clean up the table-valued parameter type - connection.Execute(String.Format(@" - IF EXISTS ( - SELECT * - FROM sys.types - WHERE is_table_type = 1 - AND name = '{0}' - ) - BEGIN - DROP TYPE {0} - END", name)); - } - - } - - protected SqlConnection OpenConnection() - { - var c = new SqlConnection(ConnectionString.ConnectionString); - c.Open(); - return c; - } - - protected SqlConnection OpenMasterConnection() - { - var cstr = Util.GetMasterConnectionString(ConnectionString.ConnectionString); - var c = new SqlConnection(cstr); - c.Open(); - return c; - } - - protected abstract SqlConnectionStringBuilder GetConnectionFromEnvironment(DeploymentEnvironment environment); - } - - public abstract class DatabaseTask : DatabaseTaskBase - { - protected override SqlConnectionStringBuilder GetConnectionFromEnvironment(DeploymentEnvironment environment) - { - return environment.MainDatabase; - } - } - - public abstract class WarehouseTask : DatabaseTaskBase - { - protected override SqlConnectionStringBuilder GetConnectionFromEnvironment(DeploymentEnvironment environment) - { - return environment.WarehouseDatabase; - } - } - - public abstract class MigrationsTask : DatabaseTask - { - public override void ExecuteCommand() - { - // Create the gateway instance - var gateway = new GalleryGateway(); - - // Get a migrator from it - DbMigrator migrator = gateway.CreateMigrator(ConnectionString.ConnectionString, "System.Data.SqlClient"); - - // Run the rest of the command - ExecuteCommandCore(migrator); - } - - protected abstract void ExecuteCommandCore(MigratorBase migrator); - } - - public abstract class DatabaseAndStorageTask : DatabaseTask - { - [Option("The connection string to the storage server", AltName = "st")] - public CloudStorageAccount StorageAccount { get; set; } - - protected string StorageAccountName - { - get { return StorageAccount.Credentials.AccountName; } - } - - protected CloudBlobClient CreateBlobClient() - { - return StorageAccount.CreateCloudBlobClient(); - } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (CurrentEnvironment != null && StorageAccount == null) - { - StorageAccount = CurrentEnvironment.MainStorage; - } - ArgCheck.RequiredOrConfig(StorageAccount, "StorageAccount"); - } - } - - public abstract class PackageVersionTask : StorageTask - { - [Option("The ID of the package", AltName = "p")] - public string PackageId { get; set; } - - [Option("The Version of the package", AltName = "v")] - public string PackageVersion { get; set; } - - [Option("The Hash of the package", AltName = "h")] - public string PackageHash { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(PackageId, "PackageId"); - ArgCheck.Required(PackageVersion, "PackageVersion"); - ArgCheck.Required(PackageHash, "PackageHash"); - } - } - - public abstract class DatabasePackageVersionTask : DatabaseAndStorageTask - { - [Option("The ID of the package", AltName = "p")] - public string PackageId { get; set; } - - [Option("The Version of the package", AltName = "v")] - public string PackageVersion { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(PackageId, "PackageId"); - ArgCheck.Required(PackageVersion, "PackageVersion"); - } - } - - public abstract class SubscriptionTask : OpsTask - { - [Option("The subscription ID to use", AltName = "s")] - public string SubscriptionId { get; set; } - - [Option("The management certificate thumbprint to use for authentication", AltName = "t")] - public string Thumbprint { get; set; } - - public X509Certificate2 ManagementCertificate { get; private set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (CurrentEnvironment != null) - { - if (String.IsNullOrEmpty(SubscriptionId)) - { - SubscriptionId = CurrentEnvironment.SubscriptionId; - } - } - ArgCheck.RequiredOrConfig(SubscriptionId, "SubscriptionId"); - - var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); - store.Open(OpenFlags.ReadOnly); - if (String.IsNullOrEmpty(Thumbprint)) - { - ManagementCertificate = store.Certificates - .Find(X509FindType.FindBySubjectName, SubscriptionId, validOnly: false) - .OfType() - .FirstOrDefault(); - if (ManagementCertificate == null) - { - throw new CommandLineException("Could not find the default management certificate for the subscription. Specify the -Thumbprint argument to select a certificate"); - } - } - else - { - ManagementCertificate = store.Certificates - .Find(X509FindType.FindByThumbprint, Thumbprint, validOnly: false) - .OfType() - .FirstOrDefault(); - if (ManagementCertificate == null) - { - throw new CommandLineException("Could not find a management certificate with the provided thumbprint."); - } - } - } - } -} diff --git a/src/NuGetGallery.Operations/Tasks/UpdateLicenseReportsTask.cs b/src/NuGetGallery.Operations/Tasks/UpdateLicenseReportsTask.cs deleted file mode 100644 index 7518fcee28..0000000000 --- a/src/NuGetGallery.Operations/Tasks/UpdateLicenseReportsTask.cs +++ /dev/null @@ -1,324 +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 System.Data; -using System.Data.SqlClient; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using AnglicanGeek.DbExecutor; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Schema; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations.Tasks -{ - [Command("updatelicensereports", "Updates the license reports from SonaType", AltName = "ulr")] - public class UpdateLicenseReportsTask : DatabaseTask - { - private static readonly JsonSchema sonatypeSchema = JsonSchema.Parse(@"{ 'type': 'object', - 'properties': { - 'next' : { 'type' : 'string' }, - 'events' : { - 'type': 'array', - 'items': { - 'type': 'object', - 'properties': { - 'sequence' : { 'type' : 'integer', 'required': true }, - 'packageId' : { 'type' : 'string', 'required': true }, - 'version' : { 'type' : 'string', 'required': true }, - 'licenses' : { 'type' : 'array', 'items': { 'type': 'string' } }, - 'reportUrl' : { 'type' : 'string' }, - 'comment' : { 'type' : 'string' } - } } } } }"); - - [Option("The base URL for the reporting service", AltName = "s")] - public Uri LicenseReportService { get; set; } - - [Option("The username for the reporting service", AltName = "u")] - public string LicenseReportUser { get; set; } - - [Option("The password for the reporting service", AltName = "p")] - public string LicenseReportPassword { get; set; } - - public NetworkCredential LicenseReportCredentials { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - - if (CurrentEnvironment != null) - { - LicenseReportCredentials = LicenseReportCredentials ?? CurrentEnvironment.LicenseReportServiceCredentials; - LicenseReportService = LicenseReportService ?? CurrentEnvironment.LicenseReportService; - } - - if (!String.IsNullOrEmpty(LicenseReportUser)) - { - if (!String.IsNullOrEmpty(LicenseReportPassword)) - { - LicenseReportCredentials = new NetworkCredential(LicenseReportUser, LicenseReportPassword); - } - else - { - LicenseReportCredentials = new NetworkCredential(LicenseReportUser, String.Empty); - } - } - else if (!String.IsNullOrEmpty(LicenseReportPassword)) - { - LicenseReportCredentials = new NetworkCredential(String.Empty, LicenseReportPassword); - } - - ArgCheck.RequiredOrConfig(LicenseReportCredentials, "LicenseReportUser"); - ArgCheck.RequiredOrConfig(LicenseReportService, "LicenseReportService"); - } - - private static PackageLicenseReport CreateReport(JObject messageEvent) - { - PackageLicenseReport report = new PackageLicenseReport(messageEvent["sequence"].Value()); - report.PackageId = messageEvent.Value("packageId"); - report.Version = messageEvent.Value("version"); - report.ReportUrl = messageEvent.Value("reportUrl"); - report.Comment = messageEvent.Value("comment"); - foreach (JValue l in messageEvent["licenses"]) - { - report.Licenses.Add(l.Value()); - } - return report; - } - - public override void ExecuteCommand() - { - Log.Info("Loading URL for the next license report"); - Uri nextLicenseReport = null; - try - { - if (!WithConnection((connection, executor) => - { - var nextReportUrl = executor.Query( - @"SELECT NextLicenseReport FROM GallerySettings").FirstOrDefault(); - if (String.IsNullOrEmpty(nextReportUrl)) - { - Log.Info("No NextLicenseReport value in GallerySettings. Using default"); - } - else if (!Uri.TryCreate(nextReportUrl, UriKind.Absolute, out nextLicenseReport)) - { - Log.Error("Invalid NextLicenseReport value in GallerySettings: {0}", nextReportUrl); - return false; - } - return true; - })) - { - return; - } - } - catch (Exception e) - { - Log.Error("Database error\n\nCallstack:\n" + e.ToString()); - return; - } - nextLicenseReport = nextLicenseReport ?? LicenseReportService; - - while (nextLicenseReport != null) - { - HttpWebResponse response = null; - int tries = 0; - while (tries < 10 && response == null) - { - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(nextLicenseReport); - if (LicenseReportCredentials != null) - { - request.Credentials = LicenseReportCredentials; - } - Log.Http(request); - - try - { - response = (HttpWebResponse)request.GetResponse(); - } - catch (WebException ex) - { - response = null; - var httpResp = ex.Response as HttpWebResponse; - if (httpResp != null) - { - Log.Http(httpResp); - return; - } - else if (ex.Status == WebExceptionStatus.Timeout || ex.Status == WebExceptionStatus.ConnectFailure) - { - // Try again in 10 seconds - tries++; - if (tries < 10) - { - Log.Warn("Timeout connecting to service. Sleeping for 30 seconds and trying again ({0}/10 tries)", tries); - Thread.Sleep(10 * 1000); - } - else - { - Log.Error("Timeout connecting to service. Tried 10 times. Aborting Job"); - throw; - } - } - else - { - Log.ErrorException(string.Format("WebException contacting service: {0}", ex.Status), ex); - throw; - } - } - } - - using (response) - { - Log.Http(response); - - if (response.StatusCode == HttpStatusCode.OK) - { - string content = (new StreamReader(response.GetResponseStream())).ReadToEnd(); - JObject sonatypeMessage = JObject.Parse(content); - if (!sonatypeMessage.IsValid(sonatypeSchema)) - { - Log.Error("License report is invalid"); - return; - } - - var events = sonatypeMessage["events"].Cast().ToList(); - for (int i = 0; i < events.Count; i++) - { - var messageEvent = events[i]; - PackageLicenseReport report = CreateReport(messageEvent); - - bool success = true; - try - { - WithConnection((connection) => - { - if (StoreReport(report, connection) == -1) - { - Log.Error("[{0:000}/{1:000}] Package Not Found {2} {3}", i + 1, events.Count, report.PackageId, report.Version); - success = false; - } - }); - } - catch (Exception e) - { - Log.Error("Database error\n\nCallstack:\n{0}", e.ToString()); - return; - } - if (success) - { - Log.Info("[{0:000}/{1:000}] Updated {2} {3}", i + 1, events.Count, report.PackageId, report.Version); - } - } - - if (sonatypeMessage["next"].Value().Length > 0) - { - var nextReportUrl = sonatypeMessage["next"].Value(); - if (!Uri.TryCreate(nextReportUrl, UriKind.Absolute, out nextLicenseReport)) - { - Log.Error("Invalid NextLicenseReport value from license report service: {0}", nextReportUrl); - return; - } - Log.Info("Found URL for the next license report: {0}", nextLicenseReport); - - // Record the next report to the database so we can check it again if we get aborted before finishing. - if (!WhatIf) - { - try - { - WithConnection((connection, executor) => - { - executor.Execute(@" - UPDATE GallerySettings - SET NextLicenseReport = @nextLicenseReport", - new { nextLicenseReport = nextLicenseReport.AbsoluteUri }); - }); - } - catch (Exception e) - { - Log.Error("Database error\n\nCallstack:\n{0}", e.ToString()); - return; - } - } - } - else - { - nextLicenseReport = null; - } - } - else if (response.StatusCode != HttpStatusCode.NoContent) - { - Log.Info("Report is not available"); - } - else - { - Log.Error("URL for the next license report caused HTTP status {0}", response.StatusCode); - return; - } - } - } - } - - private int StoreReport(PackageLicenseReport report, SqlConnection connection) - { - using (SqlCommand command = connection.CreateCommand()) - { - command.CommandText = "AddPackageLicenseReport"; - command.CommandType = CommandType.StoredProcedure; - - DataTable licensesNames = new DataTable(); - licensesNames.Columns.Add("Name", typeof(string)); - foreach (string license in report.Licenses.Select(l => l.Trim()).Distinct(StringComparer.OrdinalIgnoreCase)) - { - licensesNames.Rows.Add(license); - } - command.Parameters.AddWithValue("@licenseNames", licensesNames); - - command.Parameters.AddWithValue("@sequence", report.Sequence); - command.Parameters.AddWithValue("@packageId", report.PackageId); - command.Parameters.AddWithValue("@version", report.Version); - command.Parameters.AddWithValue("@reportUrl", report.ReportUrl ?? String.Empty); - command.Parameters.AddWithValue("@comment", report.Comment); - command.Parameters.AddWithValue("@whatIf", WhatIf); - - return (int)command.ExecuteScalar(); - } - } - - private class PackageLicenseReport - { - public int Sequence { set; get; } - - public string PackageId { set; get; } - - public string Version { set; get; } - - public string ReportUrl { set; get; } - - public string Comment { set; get; } - - public ICollection Licenses { private set; get; } - - public PackageLicenseReport(int sequence) - { - this.Sequence = sequence; - this.PackageId = null; - this.Version = null; - this.ReportUrl = null; - this.Comment = null; - this.Licenses = new LinkedList(); - } - - public override string ToString() - { - return "{ " + Sequence.ToString() + ", " - + String.Join(", ", new string[] { PackageId, Version, ReportUrl, Comment }) - + ", [ " + String.Join(", ", Licenses) + " ] }"; - } - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Tasks/UploadPackageTask.cs b/src/NuGetGallery.Operations/Tasks/UploadPackageTask.cs deleted file mode 100644 index 45af9f6c03..0000000000 --- a/src/NuGetGallery.Operations/Tasks/UploadPackageTask.cs +++ /dev/null @@ -1,47 +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.IO; -using NuGetGallery.Operations.Common; - -namespace NuGetGallery.Operations -{ - [Command("uploadpackage", "Upload a package to the storage server", AltName = "up")] - public class UploadPackageTask : StorageTask - { - [Option("The ID of the package", AltName = "p")] - public string PackageId { get; set; } - - [Option("The Version of the package", AltName = "v")] - public string PackageVersion { get; set; } - - [Option("The file to upload", AltName = "u")] - public Stream PackageFile { get; set; } - - public override void ValidateArguments() - { - base.ValidateArguments(); - ArgCheck.Required(PackageId, "PackageId"); - ArgCheck.Required(PackageVersion, "PackageVersion"); - ArgCheck.Required(PackageFile, "PackageFile"); - } - - public override void ExecuteCommand() - { - var client = CreateBlobClient(); - - var container = client.GetContainerReference("packages"); - - var fileName = $"{PackageId}.{PackageVersion}.nupkg"; - - var blob = container.GetBlockBlobReference(fileName); - if (!WhatIf) - { - blob.DeleteIfExists(); - blob.UploadFromStream(PackageFile); - blob.Properties.ContentType = "application/zip"; - blob.SetProperties(); - } - Log.Info("Uploaded new package blob: {0}", blob.Name); - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/User.cs b/src/NuGetGallery.Operations/User.cs deleted file mode 100644 index 00ea9ff3ec..0000000000 --- a/src/NuGetGallery.Operations/User.cs +++ /dev/null @@ -1,15 +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.Collections.Generic; -namespace NuGetGallery.Operations -{ - public class User - { - public int Key { get; set; } - public string Username { get; set; } - public string EmailAddress { get; set; } - public string UnconfirmedEmailAddress { get; set; } - - public IEnumerable PackageRegistrationIds { get; set; } - } -} diff --git a/src/NuGetGallery.Operations/Util.cs b/src/NuGetGallery.Operations/Util.cs deleted file mode 100644 index a613bd4456..0000000000 --- a/src/NuGetGallery.Operations/Util.cs +++ /dev/null @@ -1,383 +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 System.Data.SqlClient; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Threading.Tasks; -using System.Web; -using AnglicanGeek.DbExecutor; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using NLog; -using NuGetGallery.Auditing; -using NuGetGallery.Operations.Model; - -namespace NuGetGallery.Operations -{ - public static class Util - { - public const byte CopyingState = 7; - public const byte OnlineState = 0; - - public static bool BackupIsInProgress(SqlExecutor dbExecutor, string backupPrefix) - { - return dbExecutor.Query( - // Not worried about SQL Injection here :). This is an admin tool. - "SELECT name, state FROM sys.databases WHERE name LIKE '" + backupPrefix + "%' AND state = @state", - new { state = CopyingState }) - .Any(); - } - - public static string DownloadPackage( - CloudBlobContainer container, - string id, - string version, - string folder) - { - var fileName = $"{id}.{version}.nupkg"; - var path = Path.Combine(folder, fileName); - - var blob = container.GetBlockBlobReference(fileName); - blob.DownloadToFile(path); - - return path; - } - - public static string GetDatabaseNameTimestamp(Db database) - { - return GetDatabaseNameTimestamp(database.Name); - } - - public static string GetDatabaseNameTimestamp(string databaseName) - { - if (databaseName == null) throw new ArgumentNullException(nameof(databaseName)); - - return databaseName.Substring("Backup_".Length); - } - - public static DateTime GetDateTimeFromTimestamp(string timestamp) - { - DateTime result; - if (DateTime.TryParseExact(timestamp, "yyyyMMddHHmmss", CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out result)) - { - return result; - } - else if (DateTime.TryParseExact(timestamp, "yyyyMMMdd_HHmmZ", CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out result)) - { - return result; - } - return DateTime.MinValue; - } - - public static string GetDbName(string connectionString) - { - var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString); - return connectionStringBuilder.InitialCatalog; - } - - public static string GetDbServer(string connectionString) - { - var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString); - return connectionStringBuilder.DataSource; - } - - public static bool DatabaseExistsAndIsOnline( - IDbExecutor dbExecutor, - string restoreName) - { - var backupDbs = dbExecutor.Query( - "SELECT name, state FROM sys.databases WHERE name = @restoreName AND state = @state", - new { restoreName, state = OnlineState }) - .OrderByDescending(database => database.Name); - - return backupDbs.FirstOrDefault() != null; - } - - public static Db GetLastBackup(SqlExecutor dbExecutor, string backupNamePrefix) - { - var allBackups = dbExecutor.Query( - "SELECT name, state FROM sys.databases WHERE name LIKE '" + backupNamePrefix + "%' AND state = @state", - new { state = OnlineState }); - var orderedBackups = from db in allBackups - let t = OnlineDatabaseBackup.ParseTimestamp(db.Name) - where t != null - orderby t.Value descending - select db; - - return orderedBackups.FirstOrDefault(); - } - - public static DateTime GetLastBackupTime(SqlExecutor dbExecutor, string backupNamePrefix) - { - var lastBackup = GetLastBackup(dbExecutor, backupNamePrefix); - - if (lastBackup == null) - return DateTime.MinValue; - - var timestamp = lastBackup.Name.Substring(backupNamePrefix.Length); - - return GetDateTimeFromTimestamp(timestamp); - } - - public static string GetMasterConnectionString(string connectionString) - { - var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString) { InitialCatalog = "master" }; - return connectionStringBuilder.ToString(); - } - - public static string GetConnectionString(string connectionString, string databaseName) - { - var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString) { InitialCatalog = databaseName }; - return connectionStringBuilder.ToString(); - } - - public static string GetOpsConnectionString(string connectionString) - { - var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString) { InitialCatalog = "NuGetGalleryOps" }; - return connectionStringBuilder.ToString(); - } - - public static string GetTimestamp() - { - return DateTime.UtcNow.ToString("yyyyMMMdd_HHmm") + "Z"; - } - - internal static CloudBlobContainer GetPackageBackupsBlobContainer(CloudBlobClient blobClient) - { - var container = blobClient.GetContainerReference("packagebackups"); - container.CreateIfNotExists(); - container.SetPermissions(new BlobContainerPermissions { PublicAccess = BlobContainerPublicAccessType.Off }); - return container; - } - - internal static CloudBlobContainer GetPackagesBlobContainer(CloudBlobClient blobClient) - { - var container = blobClient.GetContainerReference("packages"); - return container; - } - - internal static string GetPackageFileName(string id, string version) - { - return $"{id.ToLowerInvariant()}.{version.ToLowerInvariant()}.nupkg"; - } - - internal static string GetTempFolder() - { - string ret = Path.Combine(Path.GetTempPath(), "NuGetGallery.Operations"); - if (!Directory.Exists(ret)) - { - Directory.CreateDirectory(ret); - } - - return ret; - } - - internal static string GetPackageBackupFileName( - string id, - string version, - string hash) - { - var hashBytes = Convert.FromBase64String(hash); - - return $"{id}/{version}/{HttpServerUtility.UrlTokenEncode(hashBytes)}.nupkg"; - } - - public static string GetBackupOfOriginalPackageFileName(string id, string version) - { - return string.Format( - "packagehistories/{0}/{0}.{1}.nupkg", - id.ToLowerInvariant(), - version.ToLowerInvariant()); - } - - internal static CloudBlockBlob GetPackageFileBlob( - CloudBlobContainer packagesBlobContainer, - string id, - string version) - { - var packageFileName = GetPackageFileName( - id, - version); - return packagesBlobContainer.GetBlockBlobReference(packageFileName); - } - - internal static Package GetPackage( - IDbExecutor dbExecutor, - string id, - string version) - { - return dbExecutor.Query( - "SELECT p.[Key], pr.Id, p.Version, p.NormalizedVersion, p.Hash FROM Packages p JOIN PackageRegistrations pr ON pr.[Key] = p.PackageRegistrationKey WHERE pr.Id = @id AND p.Version = @version", - new { id, version }).SingleOrDefault(); - } - - internal static PackageRegistration GetPackageRegistration( - IDbExecutor dbExecutor, - string id) - { - return dbExecutor.Query( - "SELECT [Key], Id FROM PackageRegistrations WHERE Id = @id", - new { id }).SingleOrDefault(); - } - - internal static IEnumerable GetPackages( - IDbExecutor dbExecutor, - int packageRegistrationKey) - { - return dbExecutor.Query( - "SELECT pr.Id, p.Version FROM Packages p JOIN PackageRegistrations PR on pr.[Key] = p.PackageRegistrationKey WHERE pr.[Key] = @packageRegistrationKey", - new { packageRegistrationKey }); - } - - internal static User GetUser( - IDbExecutor dbExecutor, - string username) - { - var user = dbExecutor.Query( - "SELECT u.[Key], u.Username, u.EmailAddress, u.UnconfirmedEmailAddress FROM Users u WHERE u.Username = @username", - new { username }).SingleOrDefault(); - - if (user != null) - { - user.PackageRegistrationIds = dbExecutor.Query( - "SELECT r.[Id] FROM PackageRegistrations r INNER JOIN PackageRegistrationOwners o ON o.PackageRegistrationKey = r.[Key] WHERE o.UserKey = @userKey AND NOT EXISTS(SELECT * FROM PackageRegistrationOwners other WHERE other.PackageRegistrationKey = r.[Key] AND other.UserKey != @userKey)", - new { userkey = user.Key }); - } - - return user; - } - - public static string GenerateHash(Stream input) - { - byte[] hashBytes; - - using (var hashAlgorithm = HashAlgorithm.Create("SHA512")) - { - hashBytes = hashAlgorithm.ComputeHash(input); - } - - var hash = Convert.ToBase64String(hashBytes); - return hash; - } - - public static string GetDatabaseServerName(SqlConnectionStringBuilder connectionStringBuilder) - { - var dataSource = connectionStringBuilder.DataSource; - if (dataSource.StartsWith("tcp:", StringComparison.OrdinalIgnoreCase)) - dataSource = dataSource.Substring(4); - var indexOfFirstPeriod = dataSource.IndexOf(".", StringComparison.Ordinal); - - if (indexOfFirstPeriod > -1) - return dataSource.Substring(0, indexOfFirstPeriod); - - return dataSource; - } - - public static Db GetDatabase( - IDbExecutor dbExecutor, - string databaseName) - { - var dbs = dbExecutor.Query( - "SELECT name, state FROM sys.databases WHERE name = @databaseName", - new { databaseName }); - - return dbs.SingleOrDefault(); - } - - public static IList CollectBlobs(Logger log, CloudBlobContainer container, string prefix, Func condition = null, int? countEstimate = null) - { - List list; - if (countEstimate.HasValue) - { - list = new List(countEstimate.Value); - } - else - { - list = new List(); - } - - BlobContinuationToken token = null; - do - { - var segment = container.ListBlobsSegmented( - prefix, - useFlatBlobListing: true, - blobListingDetails: BlobListingDetails.Copy, - maxResults: null, - currentToken: token, - options: new BlobRequestOptions(), - operationContext: new OperationContext()); - var oldCount = list.Count; - int total = 0; - foreach (var blob in segment.Results.OfType()) - { - if (condition == null || condition(blob)) - { - list.Add(blob); - } - total++; - } - - log.Info("Matched {0}/{1} blobs in current segment. Found {2} blobs so far...", list.Count - oldCount, total, list.Count); - token = segment.ContinuationToken; - } while (token != null); - - return list; - } - - public static IEnumerable EnumerateBlobs(Logger log, CloudBlobContainer container, string prefix, Func condition = null) - { - BlobContinuationToken token = null; - do - { - var segment = container.ListBlobsSegmented( - prefix, - useFlatBlobListing: true, - blobListingDetails: BlobListingDetails.Copy, - maxResults: null, - currentToken: token, - options: new BlobRequestOptions(), - operationContext: new OperationContext()); - foreach (var blob in segment.Results.OfType().Where(b => condition == null || condition(b))) - { - yield return blob; - } - - token = segment.ContinuationToken; - } while (token != null); - } - - internal static string GetPackageAuditBlobName(string id, string version, PackageAuditAction action) - { - // Audit Blob Name: - // /auditing/package/[id]/[version]/[action]-at-[datetime] - return String.Format("package/{0}/{1}/{3}-{2}.json", - id, version, action.ToString(), DateTime.UtcNow.ToString("O")); - } - - internal static async Task SaveAuditRecord(CloudStorageAccount storage, AuditRecord auditRecord) - { - string localIP = await AuditActor.GetLocalIP(); - CloudAuditingService audit = new CloudAuditingService( - Environment.MachineName, - localIP, - storage.CreateCloudBlobClient().GetContainerReference("auditing"), - onBehalfOfThunk: null); - return await audit.SaveAuditRecord(auditRecord); - } - - public static string GenerateStatusString(int total, ref int counter) - { - return String.Format( - "{0:000000}/{1:000000} {2:00.0}%", - ++counter, - total, - (((double)counter / (double)total) * 100.0)); - } - } -} diff --git a/src/NuGetGallery.Operations/app.config b/src/NuGetGallery.Operations/app.config deleted file mode 100644 index 1d93449885..0000000000 --- a/src/NuGetGallery.Operations/app.config +++ /dev/null @@ -1,100 +0,0 @@ - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/NuGetGallery.Operations/packages.config b/src/NuGetGallery.Operations/packages.config deleted file mode 100644 index 85d019c8c1..0000000000 --- a/src/NuGetGallery.Operations/packages.config +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/NuGetGallery/App_Code/ViewHelpers.cshtml b/src/NuGetGallery/App_Code/ViewHelpers.cshtml index 6493dce087..b952035748 100644 --- a/src/NuGetGallery/App_Code/ViewHelpers.cshtml +++ b/src/NuGetGallery/App_Code/ViewHelpers.cshtml @@ -122,7 +122,7 @@ @helper InstrumentationScript() { // Get instrumentation key - var config = DependencyResolver.Current.GetService(); + var config = DependencyResolver.Current.GetService(); var iKey = config == null ? string.Empty : config.Current.AppInsightsInstrumentationKey; var samplingPct = config == null ? 100 : config.Current.AppInsightsSamplingPercentage; @@ -159,7 +159,7 @@ { // Get Version info and gallery brand name var ver = ApplicationVersionHelper.GetVersion(); - var config = DependencyResolver.Current.GetService(); + var config = DependencyResolver.Current.GetService(); string brand = config == null ? "" : config.Current.Brand;

@@ -204,7 +204,7 @@ @helper AnalyticsScript() { - var config = DependencyResolver.Current.GetService(); + var config = DependencyResolver.Current.GetService(); if(config != null) { var propertyId = config.Current.GoogleAnalyticsPropertyId; if (propertyId != null) @@ -213,3 +213,84 @@ } } } + +@helper AccordionBar( + string groupName, + WebViewPage page, + string title, + string subtitle = null, + bool enabled = true, + string formModelStatePrefix = null, + Func actions = null, + Func content = null +) +{ +Func titleTemplate = null; +if (!string.IsNullOrEmpty(title)) +{ + titleTemplate = new Func(@@title); + } + + Func subtitleTemplate = null; + if (!string.IsNullOrEmpty(subtitle)) + { + subtitleTemplate = new Func(@@subtitle); + } + + @AccordionBar(groupName, + page, + titleTemplate, + subtitleTemplate, + enabled, + formModelStatePrefix, + actions, + content) +} + +@helper AccordionBar( + string groupName, + WebViewPage page, + Func title, + Func subtitle = null, + bool enabled = true, + string formModelStatePrefix = null, + Func actions = null, + Func content = null, + bool expanded = false +) +{ + @* Calculate Accordion Index *@ +string dataKey = "___AccordionCounter_" + groupName; +int lastId = (int)(HttpContext.Current.Items[dataKey] ?? 0); +int id = lastId + 1; +HttpContext.Current.Items[dataKey] = id; +string name = groupName + "-" + id.ToString(); +string actionsId = name + "-actions"; + +var hlp = new AccordionHelper(name, formModelStatePrefix, expanded, page); +

  • +
    + @if (actions != null) + { +
    + @actions(hlp) +
    + } + + @title(hlp) + + @if (subtitle != null) + { + + @subtitle(hlp) + + } +
    + @if (content != null) + { +
    + @content(hlp) +
    + } +
  • +} diff --git a/src/NuGetGallery/App_Data/Files/Content/Home.html b/src/NuGetGallery/App_Data/Files/Content/Home.html index 72d9c27958..c28bc7172e 100644 --- a/src/NuGetGallery/App_Data/Files/Content/Home.html +++ b/src/NuGetGallery/App_Data/Files/Content/Home.html @@ -5,11 +5,11 @@

    What is NuGet?

    Install NuGet - Manage NuGet Packages Dialog Window + Manage NuGet Packages Dialog Window
    diff --git a/src/NuGetGallery/App_Data/Files/Content/Privacy-Policy.md b/src/NuGetGallery/App_Data/Files/Content/Privacy-Policy.md index 79f1e5063a..da6fcb50f6 100644 --- a/src/NuGetGallery/App_Data/Files/Content/Privacy-Policy.md +++ b/src/NuGetGallery/App_Data/Files/Content/Privacy-Policy.md @@ -41,7 +41,7 @@ The .NET Foundation may use non-personal data that is aggregated for reporting a ### Information Sharing The .NET Foundation does not sell, rent, or lease any individual's personal information or lists of email addresses to anyone for marketing purposes, and we take commercially reasonable steps to maintain the security of this information. However, the .NET Foundation reserves the right to supply any such information to any organization into which the .NET Foundation may merge in the future or to which it may make any transfer in order to enable a third party to continue part or all of the Council's mission. We also reserve the right to release personal information to protect our systems or business, when we reasonably believe you to be in violation of our Terms of Use or if we reasonably believe you to have initiated or participated in any illegal activity. In addition, please be aware that in certain circumstances, the .NET Foundation may be obligated to release your personal information pursuant to judicial or other government subpoenas, warrants, or other orders. -In keeping with our open process, the .NET Foundation may maintain publicly accessible archives for Web site activities. For example, submitting the report abuse form may result in your description of the abuse becoming part of the publically accessible archives. In all such cases, we will ensure to the greatest degree possible, that personal information is protected. +In keeping with our open process, the .NET Foundation may maintain publicly accessible archives for Web site activities. For example, submitting the report abuse form may result in your description of the abuse becoming part of the publicly accessible archives. In all such cases, we will ensure to the greatest degree possible, that personal information is protected. Please remember that any information (including personal information) that you disclose in public areas of the Web site, such as NuGet package uploads, discussions, and social networking features, becomes public information that others may collect, circulate, and use. Because we cannot and do not control the acts of others, you should exercise caution when deciding to disclose information about yourself or others in public forums such as these. @@ -56,4 +56,4 @@ The .NET Foundation makes every effort to protect personal information by users From time to time the .NET Foundation may email you electronic newsletters, announcements, surveys or other information. If you prefer not to receive any or all of these communications, you may opt out by following the directions provided within the electronic newsletters and announcements. ### Contacting Us -Questions about this Privacy Statement can be directed to info@nuget.org. \ No newline at end of file +Questions about this Privacy Statement can be directed to info@nuget.org. diff --git a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs index 7b604e5629..2356c52d49 100644 --- a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs +++ b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs @@ -2,7 +2,10 @@ // 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.Data.Entity; +using System.IO; +using System.Linq; using System.Net; using System.Net.Mail; using System.Security.Principal; @@ -14,13 +17,15 @@ using Elmah; using Microsoft.WindowsAzure.ServiceRuntime; using NuGetGallery.Areas.Admin; +using NuGetGallery.Areas.Admin.Models; using NuGetGallery.Auditing; using NuGetGallery.Configuration; +using NuGetGallery.Configuration.SecretReader; using NuGetGallery.Diagnostics; using NuGetGallery.Infrastructure; +using NuGetGallery.Infrastructure.Authentication; using NuGetGallery.Infrastructure.Lucene; using NuGetGallery.Services; -using NuGetGallery.Areas.Admin.Models; namespace NuGetGallery { @@ -29,14 +34,35 @@ 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 configuration = new ConfigurationService(); + var diagnosticsService = new DiagnosticsService(); + builder.RegisterInstance(diagnosticsService) + .AsSelf() + .As() + .SingleInstance(); + + var configuration = new ConfigurationService(new SecretReaderFactory(diagnosticsService)); + builder.RegisterInstance(configuration) .AsSelf() .As(); + + builder.RegisterInstance(configuration) + .AsSelf() + .As(); + builder.Register(c => configuration.Current) .AsSelf() .As(); + // Force the read of this configuration, so it will be initialized on startup + builder.Register(c => configuration.Features) + .AsSelf() + .As(); + + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterInstance(LuceneCommon.GetDirectory(configuration.Current.LuceneIndexLocation)) .As() .SingleInstance(); @@ -56,6 +82,8 @@ protected override void Load(ContainerBuilder builder) .SingleInstance(); } + builder.RegisterType().AsSelf().As().SingleInstance(); + builder.RegisterType() .AsSelf() .As() @@ -183,12 +211,12 @@ protected override void Load(ContainerBuilder builder) var smtpUri = new SmtpUri(settings.Current.SmtpUri); var mailSenderConfiguration = new MailSenderConfiguration - { - DeliveryMethod = SmtpDeliveryMethod.Network, - Host = smtpUri.Host, - Port = smtpUri.Port, - EnableSsl = smtpUri.Secure - }; + { + DeliveryMethod = SmtpDeliveryMethod.Network, + Host = smtpUri.Host, + Port = smtpUri.Port, + EnableSsl = smtpUri.Secure + }; if (!string.IsNullOrWhiteSpace(smtpUri.UserName)) { @@ -203,17 +231,15 @@ protected override void Load(ContainerBuilder builder) else { var mailSenderConfiguration = new MailSenderConfiguration - { - DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory, - PickupDirectoryLocation = HostingEnvironment.MapPath("~/App_Data/Mail") - }; + { + DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory, + PickupDirectoryLocation = HostingEnvironment.MapPath("~/App_Data/Mail") + }; return new MailSender(mailSenderConfiguration); } }); - - builder.Register(c => mailSenderThunk.Value) .AsSelf() .As() @@ -229,17 +255,23 @@ protected override void Load(ContainerBuilder builder) .As() .InstancePerLifetimeScope(); + IAuditingService defaultAuditingService = null; + switch (configuration.Current.StorageType) { case StorageType.FileSystem: case StorageType.NotSpecified: - ConfigureForLocalFileSystem(builder); + ConfigureForLocalFileSystem(builder, configuration); + defaultAuditingService = GetAuditingServiceForLocalFileSystem(configuration); break; case StorageType.AzureStorage: ConfigureForAzureStorage(builder, configuration); + defaultAuditingService = GetAuditingServiceForAzureStorage(configuration); break; } + RegisterAuditingServices(builder, defaultAuditingService); + builder.RegisterType() .AsSelf() .As() @@ -285,7 +317,7 @@ protected override void Load(ContainerBuilder builder) .InstancePerLifetimeScope(); } - private static void ConfigureSearch(ContainerBuilder builder, ConfigurationService configuration) + private static void ConfigureSearch(ContainerBuilder builder, IGalleryConfigurationService configuration) { if (configuration.Current.ServiceDiscoveryUri == null) { @@ -307,36 +339,37 @@ private static void ConfigureSearch(ContainerBuilder builder, ConfigurationServi .InstancePerLifetimeScope(); } } - private static void ConfigureAutocomplete(ContainerBuilder builder, ConfigurationService configuration) + + private static void ConfigureAutocomplete(ContainerBuilder builder, IGalleryConfigurationService configuration) { if (configuration.Current.ServiceDiscoveryUri != null && !string.IsNullOrEmpty(configuration.Current.AutocompleteServiceResourceType)) { - builder.RegisterType() + builder.RegisterType() .AsSelf() - .As() + .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .AsSelf() - .As() + .As() .InstancePerLifetimeScope(); } else { - builder.RegisterType() + builder.RegisterType() .AsSelf() - .As() + .As() .InstancePerLifetimeScope(); - builder.RegisterType() + builder.RegisterType() .AsSelf() - .As() + .As() .InstancePerLifetimeScope(); } } - private static void ConfigureForLocalFileSystem(ContainerBuilder builder) + private static void ConfigureForLocalFileSystem(ContainerBuilder builder, IGalleryConfigurationService configuration) { builder.RegisterType() .AsSelf() @@ -353,11 +386,6 @@ private static void ConfigureForLocalFileSystem(ContainerBuilder builder) .As() .SingleInstance(); - builder.RegisterInstance(AuditingService.None) - .AsSelf() - .As() - .SingleInstance(); - // If we're not using azure storage, then aggregate stats comes from SQL builder.RegisterType() .AsSelf() @@ -365,7 +393,7 @@ private static void ConfigureForLocalFileSystem(ContainerBuilder builder) .InstancePerLifetimeScope(); } - private static void ConfigureForAzureStorage(ContainerBuilder builder, ConfigurationService configuration) + private static void ConfigureForAzureStorage(ContainerBuilder builder, IGalleryConfigurationService configuration) { builder.RegisterInstance(new CloudBlobClientWrapper(configuration.Current.AzureStorageConnectionString, configuration.Current.AzureStorageReadAccessGeoRedundant)) .AsSelf() @@ -401,7 +429,19 @@ private static void ConfigureForAzureStorage(ContainerBuilder builder, Configura .AsSelf() .As() .SingleInstance(); + } + + private static IAuditingService GetAuditingServiceForLocalFileSystem(IGalleryConfigurationService configuration) + { + var auditingPath = Path.Combine( + FileSystemFileStorageService.ResolvePath(configuration.Current.FileStorageDirectory), + FileSystemAuditingService.DefaultContainerName); + return new FileSystemAuditingService(auditingPath, AuditActor.GetAspNetOnBehalfOfAsync); + } + + private static IAuditingService GetAuditingServiceForAzureStorage(IGalleryConfigurationService configuration) + { string instanceId; try { @@ -412,12 +452,47 @@ private static void ConfigureForAzureStorage(ContainerBuilder builder, Configura instanceId = Environment.MachineName; } - var localIp = AuditActor.GetLocalIP().Result; + var localIp = AuditActor.GetLocalIpAddressAsync().Result; - builder.RegisterInstance(new CloudAuditingService(instanceId, localIp, configuration.Current.AzureStorageConnectionString, CloudAuditingService.AspNetActorThunk)) - .AsSelf() - .As() - .SingleInstance(); + return new CloudAuditingService(instanceId, localIp, configuration.Current.AzureStorageConnectionString, AuditActor.GetAspNetOnBehalfOfAsync); + } + + private static IAuditingService CombineServices(IEnumerable services) + { + if (!services.Any()) + { + return null; + } + + if (services.Count() == 1) + { + return services.First(); + } + + return new AggregateAuditingService(services); + } + + private static void RegisterAuditingServices(ContainerBuilder builder, IAuditingService defaultAuditingService) + { + var addInsDirectoryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin", "add-ins"); + + using (var serviceProvider = RuntimeServiceProvider.Create(addInsDirectoryPath)) + { + var auditingServices = serviceProvider.GetExportedValues(); + var services = new List(auditingServices); + + if (defaultAuditingService != null) + { + services.Add(defaultAuditingService); + } + + var service = CombineServices(services); + + builder.RegisterInstance(service) + .AsSelf() + .As() + .SingleInstance(); + } } } -} +} \ No newline at end of file diff --git a/src/NuGetGallery/App_Start/OwinStartup.cs b/src/NuGetGallery/App_Start/OwinStartup.cs index 438493eda7..5f83dc83e6 100644 --- a/src/NuGetGallery/App_Start/OwinStartup.cs +++ b/src/NuGetGallery/App_Start/OwinStartup.cs @@ -20,6 +20,7 @@ using NuGetGallery.Authentication.Providers; using NuGetGallery.Authentication.Providers.Cookie; using NuGetGallery.Configuration; +using NuGetGallery.Helpers; using NuGetGallery.Infrastructure; using Owin; @@ -49,7 +50,7 @@ public static void Configuration(IAppBuilder app) ServiceCenter.Current = _ => elmahServiceCenter; // Get config - var config = dependencyResolver.GetService(); + var config = dependencyResolver.GetService(); var auth = dependencyResolver.GetService(); // Setup telemetry @@ -58,16 +59,19 @@ public static void Configuration(IAppBuilder app) { TelemetryConfiguration.Active.InstrumentationKey = instrumentationKey; + var telemetryProcessorChainBuilder = TelemetryConfiguration.Active.TelemetryProcessorChainBuilder; + telemetryProcessorChainBuilder.Use(next => new TelemetryResponseCodeFilter(next)); + // 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), ... // https://azure.microsoft.com/en-us/documentation/articles/app-insights-sampling/ var instrumentationSamplingPercentage = config.Current.AppInsightsSamplingPercentage; if (instrumentationSamplingPercentage > 0 && instrumentationSamplingPercentage < 100) { - var telemetryProcessorChainBuilder = TelemetryConfiguration.Active.TelemetryProcessorChainBuilder; telemetryProcessorChainBuilder.UseSampling(instrumentationSamplingPercentage); - telemetryProcessorChainBuilder.Build(); } + + telemetryProcessorChainBuilder.Build(); } // Configure logging @@ -84,11 +88,11 @@ public static void Configuration(IAppBuilder app) } // Get the local user auth provider, if present and attach it first - Authenticator localUserAuther; - if (auth.Authenticators.TryGetValue(Authenticator.GetName(typeof(LocalUserAuthenticator)), out localUserAuther)) + Authenticator localUserAuthenticator; + if (auth.Authenticators.TryGetValue(Authenticator.GetName(typeof(LocalUserAuthenticator)), out localUserAuthenticator)) { // Configure cookie auth now - localUserAuther.Startup(config, app); + localUserAuthenticator.Startup(config, app).Wait(); } // Attach external sign-in cookie middleware @@ -111,7 +115,7 @@ public static void Configuration(IAppBuilder app) .Select(p => p.Value); foreach (var auther in nonCookieAuthers) { - auther.Startup(config, app); + auther.Startup(config, app).Wait(); } // Catch unobserved exceptions from threads before they cause IIS to crash: diff --git a/src/NuGetGallery/App_Start/Routes.cs b/src/NuGetGallery/App_Start/Routes.cs index 6bcc3fae08..330dac73ef 100644 --- a/src/NuGetGallery/App_Start/Routes.cs +++ b/src/NuGetGallery/App_Start/Routes.cs @@ -101,22 +101,22 @@ public static void RegisterUIRoutes(RouteCollection routes) var uploadPackageRoute = routes.MapRoute( RouteName.UploadPackage, - "packages/upload", + "packages/manage/upload", new { controller = "Packages", action = "UploadPackage" }); routes.MapRoute( RouteName.UploadPackageProgress, - "packages/upload-progress", + "packages/manage/upload-progress", new { controller = "Packages", action = "UploadPackageProgress" }); routes.MapRoute( RouteName.VerifyPackage, - "packages/verify-upload", + "packages/manage/verify-upload", new { controller = "Packages", action = "VerifyPackage" }); routes.MapRoute( RouteName.CancelUpload, - "packages/cancel-upload", + "packages/manage/cancel-upload", new { controller = "Packages", action = "CancelUpload" }); routes.MapRoute( @@ -284,7 +284,16 @@ public static void RegisterUIRoutes(RouteCollection routes) { controller = "Api", action = "VerifyPackageKey", - id = UrlParameter.Optional, + version = UrlParameter.Optional + }); + + routes.MapRoute( + "v1" + RouteName.CreatePackageVerificationKey, + "api/v1/package/create-verification-key/{id}/{version}", + new + { + controller = "Api", + action = "CreatePackageVerificationKey", version = UrlParameter.Optional }); @@ -379,7 +388,16 @@ public static void RegisterApiV2Routes(RouteCollection routes) { controller = "Api", action = "VerifyPackageKey", - id = UrlParameter.Optional, + version = UrlParameter.Optional + }); + + routes.MapRoute( + "v2" + RouteName.CreatePackageVerificationKey, + "api/v2/package/create-verification-key/{id}/{version}", + new + { + controller = "Api", + action = "CreatePackageVerificationKey", version = UrlParameter.Optional }); diff --git a/src/NuGetGallery/App_Start/RuntimeServiceProvider.cs b/src/NuGetGallery/App_Start/RuntimeServiceProvider.cs new file mode 100644 index 0000000000..a6f74c00f2 --- /dev/null +++ b/src/NuGetGallery/App_Start/RuntimeServiceProvider.cs @@ -0,0 +1,79 @@ +// 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.ComponentModel.Composition.Hosting; +using System.IO; +using System.Linq; + +namespace NuGetGallery +{ + internal sealed class RuntimeServiceProvider : IDisposable + { + private readonly CompositionContainer _compositionContainer; + private bool _isDisposed; + + private RuntimeServiceProvider(CompositionContainer compositionContainer) + { + _compositionContainer = compositionContainer; + } + + public void Dispose() + { + if (!_isDisposed) + { + _compositionContainer.Dispose(); + + _isDisposed = true; + } + } + + internal static RuntimeServiceProvider Create(string baseDirectoryPath) + { + if (string.IsNullOrEmpty(baseDirectoryPath)) + { + throw new ArgumentException(Strings.ParameterCannotBeNullOrEmpty, nameof(baseDirectoryPath)); + } + + var compositionContainer = CreateCompositionContainer(baseDirectoryPath); + + return new RuntimeServiceProvider(compositionContainer); + } + + internal IEnumerable GetExportedValues() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(RuntimeServiceProvider)); + } + + return _compositionContainer.GetExportedValues(); + } + + // Runtime loadable services are only allowed from subdirectories of the base directory path. + private static CompositionContainer CreateCompositionContainer(string baseDirectoryPath) + { + if (!Directory.Exists(baseDirectoryPath)) + { + return new CompositionContainer(); + } + + var subdirectoryPaths = Directory.GetDirectories(baseDirectoryPath); + + if (!subdirectoryPaths.Any()) + { + return new CompositionContainer(); + } + + var catalog = new AggregateCatalog(); + + foreach (var subdirectoryPath in subdirectoryPaths) + { + catalog.Catalogs.Add(new DirectoryCatalog(subdirectoryPath)); + } + + return new CompositionContainer(catalog); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/ApplicationInsights.config b/src/NuGetGallery/ApplicationInsights.config index c6e1c4cba5..1c5d739123 100644 --- a/src/NuGetGallery/ApplicationInsights.config +++ b/src/NuGetGallery/ApplicationInsights.config @@ -6,6 +6,28 @@ Note: If not present, please add Your Key to the top of this file. --> + + + + + + + + + + + + search|spider|crawl|Bot|Monitor|AlwaysOn + + + + + + + + + @@ -19,10 +41,6 @@ PerformanceCounter must be either \CategoryName(InstanceName)\CounterName or \CategoryName\CounterName - Counter names may only contain letters, round brackets, forward slashes, hyphens, underscores, spaces and dots. - You may provide an optional ReportAs attribute which will be used as the metric name when reporting counter data. - For the purposes of reporting, metric names will be sanitized by removing all invalid characters from the resulting metric name. - NOTE: performance counters configuration will be lost upon NuGet upgrade. The following placeholders are supported as InstanceName: @@ -31,6 +49,7 @@ ??APP_CLR_PROC?? - instance name of the application CLR process for .NET counters. --> + @@ -55,36 +74,9 @@ - - - 5 - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + --> \ No newline at end of file diff --git a/src/NuGetGallery/Areas/Admin/Controllers/ConfigController.cs b/src/NuGetGallery/Areas/Admin/Controllers/ConfigController.cs index 5e070df332..c5aad83cda 100644 --- a/src/NuGetGallery/Areas/Admin/Controllers/ConfigController.cs +++ b/src/NuGetGallery/Areas/Admin/Controllers/ConfigController.cs @@ -13,15 +13,16 @@ namespace NuGetGallery.Areas.Admin.Controllers { public partial class ConfigController : AdminControllerBase { - private readonly ConfigurationService _config; + private readonly IGalleryConfigurationService _config; private readonly AuthenticationService _auth; - public ConfigController(ConfigurationService config, AuthenticationService auth) + public ConfigController(IGalleryConfigurationService config, AuthenticationService auth) { _config = config; _auth = auth; } + [HttpGet] public virtual ActionResult Index() { var settings = (from p in typeof(IAppConfiguration).GetProperties(BindingFlags.Public | BindingFlags.Instance) diff --git a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs index 1ad28512d6..b98594ba9b 100644 --- a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs +++ b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs @@ -21,6 +21,7 @@ public DeleteController(IPackageService packageService) _packageService = packageService; } + [HttpGet] public virtual ActionResult Index() { var model = new DeletePackagesRequest @@ -32,12 +33,12 @@ public virtual ActionResult Index() private static readonly ReportPackageReason[] ReportMyPackageReasons = { ReportPackageReason.ContainsPrivateAndConfidentialData, - ReportPackageReason.PublishedWithWrongVersion, ReportPackageReason.ReleasedInPublicByAccident, ReportPackageReason.ContainsMaliciousCode, ReportPackageReason.Other }; + [HttpGet] public virtual ActionResult Search(string query) { // Search suports several options: diff --git a/src/NuGetGallery/Areas/Admin/Controllers/HomeController.cs b/src/NuGetGallery/Areas/Admin/Controllers/HomeController.cs index 3446bd3cac..602cf24bfa 100644 --- a/src/NuGetGallery/Areas/Admin/Controllers/HomeController.cs +++ b/src/NuGetGallery/Areas/Admin/Controllers/HomeController.cs @@ -15,11 +15,13 @@ public HomeController(IContentService content) _content = content; } + [HttpGet] public virtual ActionResult Index() { return View(); } + [HttpGet] [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Throw", Justification="This is an admin action")] [SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Justification = "This is an admin action")] public virtual ActionResult Throw() @@ -27,6 +29,7 @@ public virtual ActionResult Throw() throw new Exception("KA BOOM!"); } + [HttpGet] public virtual ActionResult ClearContentCache() { _content.ClearCache(); diff --git a/src/NuGetGallery/Areas/Admin/Controllers/SupportRequestController.cs b/src/NuGetGallery/Areas/Admin/Controllers/SupportRequestController.cs index 49486e6e99..a368dd4082 100644 --- a/src/NuGetGallery/Areas/Admin/Controllers/SupportRequestController.cs +++ b/src/NuGetGallery/Areas/Admin/Controllers/SupportRequestController.cs @@ -36,6 +36,7 @@ public SupportRequestController( _userService = userService; } + [HttpGet] public ViewResult Admins() { var viewModel = new SupportRequestAdminsViewModel(); @@ -43,6 +44,7 @@ public ViewResult Admins() return View(viewModel); } + [HttpGet] public ActionResult GetAdmins() { var admins = _supportRequestService.GetAllAdmins().Select(a => new SupportRequestAdminViewModel(a)); @@ -202,6 +204,7 @@ public async Task Index(int pageNumber = 1, int take = _defaultTak return View(viewModel); } + [HttpGet] public ActionResult History(int id) { var historyEntries = _supportRequestService.GetHistoryEntriesByIssueKey(id).OrderByDescending(h => h.EntryDate); diff --git a/src/NuGetGallery/Areas/Admin/DynamicData/Default.aspx.cs b/src/NuGetGallery/Areas/Admin/DynamicData/Default.aspx.cs index 7dcd56d3dd..5296fbcb9f 100644 --- a/src/NuGetGallery/Areas/Admin/DynamicData/Default.aspx.cs +++ b/src/NuGetGallery/Areas/Admin/DynamicData/Default.aspx.cs @@ -8,6 +8,12 @@ namespace NuGetGallery.Areas.Admin.DynamicData { public partial class _Default : Page { + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + ViewStateUserKey = User.Identity.Name; + } + protected void Page_Load(object sender, EventArgs e) { IList visibleTables = DynamicDataManager.DefaultModel.VisibleTables; diff --git a/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Details.aspx.cs b/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Details.aspx.cs index 21e7a54bbe..34995ebb9b 100644 --- a/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Details.aspx.cs +++ b/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Details.aspx.cs @@ -24,6 +24,7 @@ protected void Page_Init(object sender, EventArgs e) { args.Context = (ObjectContext)table.CreateContext(); }; + ViewStateUserKey = User.Identity.Name; } protected void Page_Load(object sender, EventArgs e) diff --git a/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Edit.aspx.cs b/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Edit.aspx.cs index dfc08bdf62..dada3d8c5a 100644 --- a/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Edit.aspx.cs +++ b/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Edit.aspx.cs @@ -24,6 +24,7 @@ protected void Page_Init(object sender, EventArgs e) { args.Context = (ObjectContext)table.CreateContext(); }; + ViewStateUserKey = User.Identity.Name; } protected void Page_Load(object sender, EventArgs e) diff --git a/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Insert.aspx.cs b/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Insert.aspx.cs index 203d881712..53c4e19153 100644 --- a/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Insert.aspx.cs +++ b/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/Insert.aspx.cs @@ -24,6 +24,7 @@ protected void Page_Init(object sender, EventArgs e) { args.Context = (ObjectContext)table.CreateContext(); }; + ViewStateUserKey = User.Identity.Name; } protected void Page_Load(object sender, EventArgs e) diff --git a/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/List.aspx.cs b/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/List.aspx.cs index f1854269ce..1dc027ec02 100644 --- a/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/List.aspx.cs +++ b/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/List.aspx.cs @@ -36,6 +36,7 @@ protected void Page_Init(object sender, EventArgs e) SearchPanel.Visible = false; GridQueryExtender.Expressions.Remove(searchExpression); } + ViewStateUserKey = User.Identity.Name; } protected void Page_Load(object sender, EventArgs e) diff --git a/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/ListDetails.aspx.cs b/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/ListDetails.aspx.cs index ea45ce1b45..a6cbc6ae8a 100644 --- a/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/ListDetails.aspx.cs +++ b/src/NuGetGallery/Areas/Admin/DynamicData/PageTemplates/ListDetails.aspx.cs @@ -20,7 +20,7 @@ protected void Page_Init(object sender, EventArgs e) FormView1.SetMetaTable(table); GridDataSource.EntityTypeFilter = table.EntityType.Name; DetailsDataSource.EntityTypeFilter = table.EntityType.Name; - + ViewStateUserKey = User.Identity.Name; } protected void Page_Load(object sender, EventArgs e) diff --git a/src/NuGetGallery/Areas/Admin/Models/SupportRequestDbContext.cs b/src/NuGetGallery/Areas/Admin/Models/SupportRequestDbContext.cs index 4aff888d25..854b9db378 100644 --- a/src/NuGetGallery/Areas/Admin/Models/SupportRequestDbContext.cs +++ b/src/NuGetGallery/Areas/Admin/Models/SupportRequestDbContext.cs @@ -26,7 +26,7 @@ public SupportRequestDbContext() /// /// The NuGet Gallery code should usually use this constructor, - /// so that we can configure the connection via the Cloud Service configuraton. + /// so that we can configure the connection via the Cloud Service configuration. /// public SupportRequestDbContext(string connectionString) : base(connectionString) diff --git a/src/NuGetGallery/Areas/Admin/ViewModels/SupportRequestViewModel.cs b/src/NuGetGallery/Areas/Admin/ViewModels/SupportRequestViewModel.cs index 32f751544a..2275d91323 100644 --- a/src/NuGetGallery/Areas/Admin/ViewModels/SupportRequestViewModel.cs +++ b/src/NuGetGallery/Areas/Admin/ViewModels/SupportRequestViewModel.cs @@ -43,6 +43,7 @@ public SupportRequestViewModel(Issue issue) public string UserEmail { get; set; } public int? PackageRegistrationKey { get; set; } public int? UserKey { get; set; } + public bool IsRelatedToPackage => !string.IsNullOrEmpty(PackageId) && !string.IsNullOrEmpty(PackageVersion); // Editable fields public int? AssignedTo { get; set; } diff --git a/src/NuGetGallery/Areas/Admin/Views/Home/Index.cshtml b/src/NuGetGallery/Areas/Admin/Views/Home/Index.cshtml index 6b92784b52..8c8d4f5429 100644 --- a/src/NuGetGallery/Areas/Admin/Views/Home/Index.cshtml +++ b/src/NuGetGallery/Areas/Admin/Views/Home/Index.cshtml @@ -43,7 +43,7 @@

    - Lucene Index Maintainance + Lucene Index Maintenance

    diff --git a/src/NuGetGallery/Areas/Admin/Views/Lucene/Index.cshtml b/src/NuGetGallery/Areas/Admin/Views/Lucene/Index.cshtml index bd6dccc250..5ab934c113 100644 --- a/src/NuGetGallery/Areas/Admin/Views/Lucene/Index.cshtml +++ b/src/NuGetGallery/Areas/Admin/Views/Lucene/Index.cshtml @@ -1,10 +1,10 @@ @model NuGetGallery.Areas.Admin.Models.LuceneInfoModel @{ - ViewBag.Title = "Lucene Maintainance"; + ViewBag.Title = "Lucene Maintenance"; TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); } -

    Lucene Maintainance

    +

    Lucene Maintenance

    @if (Model.LastUpdated == null) { diff --git a/src/NuGetGallery/Areas/Admin/Views/SupportRequest/Index.cshtml b/src/NuGetGallery/Areas/Admin/Views/SupportRequest/Index.cshtml index 29a16b4050..78276fd77e 100644 --- a/src/NuGetGallery/Areas/Admin/Views/SupportRequest/Index.cshtml +++ b/src/NuGetGallery/Areas/Admin/Views/SupportRequest/Index.cshtml @@ -144,6 +144,7 @@ Assigned to . Not assigned! +
    For package

    diff --git a/src/NuGetGallery/Authentication/AuthenticateExternalLoginResult.cs b/src/NuGetGallery/Authentication/AuthenticateExternalLoginResult.cs new file mode 100644 index 0000000000..1402bbecdb --- /dev/null +++ b/src/NuGetGallery/Authentication/AuthenticateExternalLoginResult.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.Security.Claims; +using NuGetGallery.Authentication.Providers; + +namespace NuGetGallery.Authentication +{ + public class AuthenticateExternalLoginResult + { + public AuthenticatedUser Authentication { get; set; } + public ClaimsIdentity ExternalIdentity { get; set; } + public Authenticator Authenticator { get; set; } + public Credential Credential { get; set; } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/AuthenticationService.cs b/src/NuGetGallery/Authentication/AuthenticationService.cs index 58c5461ba6..097aa53960 100644 --- a/src/NuGetGallery/Authentication/AuthenticationService.cs +++ b/src/NuGetGallery/Authentication/AuthenticationService.cs @@ -15,44 +15,108 @@ using NuGetGallery.Authentication.Providers.Ldap; using NuGetGallery.Configuration; using NuGetGallery.Diagnostics; +using NuGetGallery.Infrastructure.Authentication; using NuGetGallery.Services; +using static NuGetGallery.Constants; + namespace NuGetGallery.Authentication { public class AuthenticationService { - private readonly Dictionary> _credentialFormatters; + private Dictionary> _credentialFormatters; private readonly IDiagnosticsSource _trace; private readonly IAppConfiguration _config; + private readonly ICredentialBuilder _credentialBuilder; + private readonly ICredentialValidator _credentialValidator; + private readonly IDateTimeProvider _dateTimeProvider; + ///

    + /// This ctor is used for test only. + /// protected AuthenticationService() : this(null, null, null, AuditingService.None, Enumerable.Empty(), NullLdapService.Instance) { + Auditing = AuditingService.None; + Authenticators = new Dictionary(); + InitCredentialFormatters(); } - public AuthenticationService(IEntitiesContext entities, IAppConfiguration config, IDiagnosticsService diagnostics, AuditingService auditing, IEnumerable providers, ILdapService ldapService) + public AuthenticationService( + IEntitiesContext entities, IAppConfiguration config, IDiagnosticsService diagnostics, + IAuditingService auditing, IEnumerable providers, ICredentialBuilder credentialBuilder, + ICredentialValidator credentialValidator, IDateTimeProvider dateTimeProvider, + ILdapService ldapService) { - _credentialFormatters = new Dictionary>(StringComparer.OrdinalIgnoreCase) { - { "password", _ => Strings.CredentialType_Password }, - { "apikey", _ => Strings.CredentialType_ApiKey }, - { "external", FormatExternalCredentialType } - }; + if (entities == null) + { + throw new ArgumentNullException(nameof(entities)); + } + + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + if (diagnostics == null) + { + throw new ArgumentNullException(nameof(diagnostics)); + } + + if (auditing == null) + { + throw new ArgumentNullException(nameof(auditing)); + } + + if (providers == null) + { + throw new ArgumentNullException(nameof(providers)); + } + + if (credentialBuilder == null) + { + throw new ArgumentNullException(nameof(credentialBuilder)); + } + + if (credentialValidator == null) + { + throw new ArgumentNullException(nameof(credentialValidator)); + } + + if (dateTimeProvider == null) + { + throw new ArgumentNullException(nameof(dateTimeProvider)); + } + + InitCredentialFormatters(); Entities = entities; _config = config; Auditing = auditing; _trace = diagnostics.SafeGetSource("AuthenticationService"); Authenticators = providers.ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); + _credentialBuilder = credentialBuilder; + _credentialValidator = credentialValidator; + _dateTimeProvider = dateTimeProvider; this.Ldap = ldapService; } public IEntitiesContext Entities { get; private set; } public IDictionary Authenticators { get; private set; } - public AuditingService Auditing { get; private set; } + public IAuditingService Auditing { get; private set; } public ILdapService Ldap { get; private set; } - public virtual async Task Authenticate(string userNameOrEmail, string password) + private void InitCredentialFormatters() + { + _credentialFormatters = new Dictionary>(StringComparer.OrdinalIgnoreCase) { + { "password", _ => Strings.CredentialType_Password }, + { "apikey", _ => Strings.CredentialType_ApiKey }, + { "external", FormatExternalCredentialType } + }; + } + + public virtual async Task Authenticate(string userNameOrEmail, string password) { using (_trace.Activity("Authenticate:" + userNameOrEmail)) { @@ -61,52 +125,101 @@ public virtual async Task Authenticate(string userNameOrEmail // Check if the user exists if (user == null) { - var ldapUser = this.Ldap.ValidateUsernameAndPassword(userNameOrEmail, password); +//// <<<<<<< HEAD +//// var ldapUser = this.Ldap.ValidateUsernameAndPassword(userNameOrEmail, password); + +//// if (ldapUser != null) +//// { +//// _trace.Information("Creating user from LDAP credentials: " + userNameOrEmail); +//// var ldapCredential = CredentialBuilder.CreateExternalCredential(AuthenticationTypes.LdapUser, ldapUser.Username, ldapUser.Identity); +//// return await this.Register(ldapUser.Username, ldapUser.Email, ldapCredential); +//// } +//// else +//// { +//// _trace.Information("No such user: " + userNameOrEmail); +//// return null; +//// } +//// ======= + _trace.Information("No such user: " + userNameOrEmail); + + await Auditing.SaveAuditRecordAsync( + new FailedAuthenticatedOperationAuditRecord( + userNameOrEmail, AuditedAuthenticatedOperationAction.FailedLoginNoSuchUser)); + + return new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.BadCredentials); + } - if (ldapUser != null) - { - _trace.Information("Creating user from LDAP credentials: " + userNameOrEmail); - var ldapCredential = CredentialBuilder.CreateExternalCredential(AuthenticationTypes.LdapUser, ldapUser.Username, ldapUser.Identity); - return await this.Register(ldapUser.Username, ldapUser.Email, ldapCredential); - } - else - { - _trace.Information("No such user: " + userNameOrEmail); - return null; - } + int remainingMinutes; + + if (IsAccountLocked(user, out remainingMinutes)) + { + _trace.Information($"Login failed. User account {userNameOrEmail} is locked for the next {remainingMinutes} minutes."); + + return new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.AccountLocked, + authenticatedUser: null, lockTimeRemainingMinutes: remainingMinutes); +//// >>>>>>> v2017.03.27 } // Validate the password Credential matched; if (!ValidatePasswordCredential(user.Credentials, password, out matched)) { - var isValid = this.Ldap.ValidateCredentials(user.Credentials, password, out matched); - if (isValid) - { - _trace.Verbose("Successfully authenticated '" + user.Username + "' with '" + matched.Type + "' credential"); - return new AuthenticatedUser(user, matched); - } - - _trace.Information("Password validation failed: " + userNameOrEmail); - return null; +//// <<<<<<< HEAD +//// var isValid = this.Ldap.ValidateCredentials(user.Credentials, password, out matched); +//// if (isValid) +//// { +//// _trace.Verbose("Successfully authenticated '" + user.Username + "' with '" + matched.Type + "' credential"); +//// return new AuthenticatedUser(user, matched); +//// } +//// +//// _trace.Information("Password validation failed: " + userNameOrEmail); +//// return null; +//// ======= + _trace.Information($"Password validation failed: {userNameOrEmail}"); + + await UpdateFailedLoginAttempt(user); + + await Auditing.SaveAuditRecordAsync( + new FailedAuthenticatedOperationAuditRecord( + userNameOrEmail, AuditedAuthenticatedOperationAction.FailedLoginInvalidPassword)); + + return new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.BadCredentials); +//// >>>>>>> v2017.03.27 } var passwordCredentials = user .Credentials - .Where(c => c.Type.StartsWith(CredentialTypes.Password.Prefix, StringComparison.OrdinalIgnoreCase)) + .Where(c => CredentialTypes.IsPassword(c.Type)) .ToList(); - if (passwordCredentials.Count > 1 || !passwordCredentials.Any(c => String.Equals(c.Type, CredentialTypes.Password.Pbkdf2, StringComparison.OrdinalIgnoreCase))) + + if (passwordCredentials.Count > 1 || + !passwordCredentials.Any(c => string.Equals(c.Type, CredentialBuilder.LatestPasswordType, StringComparison.OrdinalIgnoreCase))) { await MigrateCredentials(user, passwordCredentials, password); } + // Reset failed login count upon successful login + await UpdateSuccessfulLoginAttempt(user); + // Return the result _trace.Verbose("Successfully authenticated '" + user.Username + "' with '" + matched.Type + "' credential"); - return new AuthenticatedUser(user, matched); + return new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.Success, new AuthenticatedUser(user, matched)); } } - public virtual AuthenticatedUser Authenticate(Credential credential) + public virtual async Task Authenticate(string apiKey) + { + return await AuthenticateInternal( + FindMatchingApiKey, + new Credential { Type = CredentialTypes.ApiKey.Prefix, Value = apiKey }); + } + + public virtual async Task Authenticate(Credential credential) + { + return await AuthenticateInternal(FindMatchingCredential, credential); + } + + private async Task AuthenticateInternal(Func matchCredential, Credential credential) { if (credential.Type.StartsWith(CredentialTypes.Password.Prefix, StringComparison.OrdinalIgnoreCase)) { @@ -116,27 +229,66 @@ public virtual AuthenticatedUser Authenticate(Credential credential) using (_trace.Activity("Authenticate Credential: " + credential.Type)) { - var matched = FindMatchingCredential(credential); + var matched = matchCredential(credential); if (matched == null) { _trace.Information("No user matches credential of type: " + credential.Type); + + await Auditing.SaveAuditRecordAsync( + new FailedAuthenticatedOperationAuditRecord(null, AuditedAuthenticatedOperationAction.FailedLoginNoSuchUser, attemptedCredential: credential)); + + return null; + } + + if (matched.HasExpired) + { + _trace.Verbose("Credential of type '" + matched.Type + "' for user '" + matched.User.Username + "' has expired on " + matched.Expires.Value.ToString("O", CultureInfo.InvariantCulture)); + + return null; + } + + if (matched.Type == CredentialTypes.ApiKey.V1 + && !matched.HasBeenUsedInLastDays(_config.ExpirationInDaysForApiKeyV1)) + { + // API key credential was last used a long, long time ago - expire it + await Auditing.SaveAuditRecordAsync( + new UserAuditRecord(matched.User, AuditedUserAction.ExpireCredential, matched)); + + matched.Expires = _dateTimeProvider.UtcNow; + await Entities.SaveChangesAsync(); + + _trace.Verbose( + "Credential of type '" + matched.Type + + "' for user '" + matched.User.Username + + "' was last used on " + matched.LastUsed.Value.ToString("O", CultureInfo.InvariantCulture) + + " and has now expired."); + return null; } + // update last used timestamp + matched.LastUsed = _dateTimeProvider.UtcNow; + await Entities.SaveChangesAsync(); + _trace.Verbose("Successfully authenticated '" + matched.User.Username + "' with '" + matched.Type + "' credential"); + return new AuthenticatedUser(matched.User, matched); } } - public virtual void CreateSession(IOwinContext owinContext, User user) + public virtual async Task CreateSessionAsync(IOwinContext owinContext, AuthenticatedUser user) { // Create a claims identity for the session - ClaimsIdentity identity = CreateIdentity(user, AuthenticationTypes.LocalUser); + ClaimsIdentity identity = CreateIdentity(user.User, AuthenticationTypes.LocalUser); // Issue the session token and clean up the external token if present owinContext.Authentication.SignIn(identity); owinContext.Authentication.SignOut(AuthenticationTypes.External); + + // Write an audit record + await Auditing.SaveAuditRecordAsync( + new UserAuditRecord(user.User, AuditedUserAction.Login, user.CredentialUsed)); } public virtual async Task Register(string username, string emailAddress, Credential credential) @@ -150,7 +302,7 @@ public virtual async Task Register(string username, string em .FirstOrDefault(u => u.Username == username || u.EmailAddress == emailAddress); if (existingUser != null) { - if (String.Equals(existingUser.Username, username, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(existingUser.Username, username, StringComparison.OrdinalIgnoreCase)) { throw new EntityException(Strings.UsernameNotAvailable, username); } @@ -160,18 +312,16 @@ public virtual async Task Register(string username, string em } } - var apiKey = Guid.NewGuid(); var newUser = new User(username) { EmailAllowed = true, UnconfirmedEmailAddress = emailAddress, EmailConfirmationToken = CryptographyService.GenerateToken(), NotifyPackagePushed = true, - CreatedUtc = DateTime.UtcNow + CreatedUtc = _dateTimeProvider.UtcNow }; - // Add a credential for the password and the API Key - newUser.Credentials.Add(CredentialBuilder.CreateV1ApiKey(apiKey)); + // Add a credential for the password newUser.Credentials.Add(credential); if (!_config.ConfirmEmailAddresses) @@ -180,7 +330,7 @@ public virtual async Task Register(string username, string em } // Write an audit record - await Auditing.SaveAuditRecord(new UserAuditRecord(newUser, UserAuditAction.Registered)); + await Auditing.SaveAuditRecordAsync(new UserAuditRecord(newUser, AuditedUserAction.Register, credential)); Entities.Users.Add(newUser); await Entities.SaveChangesAsync(); @@ -188,14 +338,6 @@ public virtual async Task Register(string username, string em return new AuthenticatedUser(newUser, credential); } - [Obsolete("Use Register(string, string, Credential) now")] - public virtual Task Register(string username, string password, string emailAddress) - { - var hashedPassword = CryptographyService.GenerateSaltedHash(password, Constants.PBKDF2HashAlgorithmId); - var passCred = new Credential(CredentialTypes.Password.Pbkdf2, hashedPassword); - return Register(username, emailAddress, passCred); - } - public virtual Task ReplaceCredential(string username, Credential credential) { var user = Entities @@ -217,7 +359,7 @@ public virtual async Task ReplaceCredential(User user, Credential credential) public virtual async Task ResetPasswordWithToken(string username, string token, string newPassword) { - if (String.IsNullOrEmpty(newPassword)) + if (string.IsNullOrEmpty(newPassword)) { throw new ArgumentNullException(nameof(newPassword)); } @@ -227,17 +369,19 @@ public virtual async Task ResetPasswordWithToken(string username, st .Include(u => u.Credentials) .SingleOrDefault(u => u.Username == username); - if (user != null && String.Equals(user.PasswordResetToken, token, StringComparison.Ordinal) && !user.PasswordResetTokenExpirationDate.IsInThePast()) + if (user != null && string.Equals(user.PasswordResetToken, token, StringComparison.Ordinal) && !user.PasswordResetTokenExpirationDate.IsInThePast()) { if (!user.Confirmed) { throw new InvalidOperationException(Strings.UserIsNotYetConfirmed); } - var cred = CredentialBuilder.CreatePbkdf2Password(newPassword); + var cred = _credentialBuilder.CreatePasswordCredential(newPassword); await ReplaceCredentialInternal(user, cred); user.PasswordResetToken = null; user.PasswordResetTokenExpirationDate = null; + user.FailedLoginCount = 0; + user.LastFailedLoginUtc = null; await Entities.SaveChangesAsync(); return cred; } @@ -282,15 +426,29 @@ public virtual async Task GeneratePasswordResetToken(User user, int expirationIn throw new InvalidOperationException(Strings.UserIsNotYetConfirmed); } - if (!String.IsNullOrEmpty(user.PasswordResetToken) && !user.PasswordResetTokenExpirationDate.IsInThePast()) + if (!string.IsNullOrEmpty(user.PasswordResetToken) && !user.PasswordResetTokenExpirationDate.IsInThePast()) { return; } user.PasswordResetToken = CryptographyService.GenerateToken(); - user.PasswordResetTokenExpirationDate = DateTime.UtcNow.AddMinutes(expirationInMinutes); + user.PasswordResetTokenExpirationDate = _dateTimeProvider.UtcNow.AddMinutes(expirationInMinutes); + + var passwordCredential = user.Credentials.FirstOrDefault( + credential => credential.Type.StartsWith(CredentialTypes.Password.Prefix, StringComparison.OrdinalIgnoreCase)); + + UserAuditRecord auditRecord; + + if (passwordCredential == null) + { + auditRecord = new UserAuditRecord(user, AuditedUserAction.RequestPasswordReset); + } + else + { + auditRecord = new UserAuditRecord(user, AuditedUserAction.RequestPasswordReset, passwordCredential); + } - await Auditing.SaveAuditRecord(new UserAuditRecord(user, UserAuditAction.RequestedPasswordReset)); + await Auditing.SaveAuditRecordAsync(auditRecord); await Entities.SaveChangesAsync(); } @@ -307,8 +465,10 @@ public virtual async Task ChangePassword(User user, string oldPassword, st } // Replace/Set password credential - var cred = CredentialBuilder.CreatePbkdf2Password(newPassword); - await ReplaceCredentialInternal(user, cred); + var passwordCredential = _credentialBuilder.CreatePasswordCredential(newPassword); + await ReplaceCredentialInternal(user, passwordCredential); + + // Save changes await Entities.SaveChangesAsync(); return true; } @@ -319,7 +479,7 @@ public virtual ActionResult Challenge(string providerName, string redirectUrl) if (!Authenticators.TryGetValue(providerName, out provider)) { - throw new InvalidOperationException(String.Format( + throw new InvalidOperationException(string.Format( CultureInfo.CurrentCulture, Strings.UnknownAuthenticationProvider, providerName)); @@ -327,7 +487,7 @@ public virtual ActionResult Challenge(string providerName, string redirectUrl) if (!provider.BaseConfig.Enabled) { - throw new InvalidOperationException(String.Format( + throw new InvalidOperationException(string.Format( CultureInfo.CurrentCulture, Strings.AuthenticationProviderDisabled, providerName)); @@ -338,7 +498,7 @@ public virtual ActionResult Challenge(string providerName, string redirectUrl) public virtual async Task AddCredential(User user, Credential credential) { - await Auditing.SaveAuditRecord(new UserAuditRecord(user, UserAuditAction.AddedCredential, credential)); + await Auditing.SaveAuditRecordAsync(new UserAuditRecord(user, AuditedUserAction.AddCredential, credential)); user.Credentials.Add(credential); await Entities.SaveChangesAsync(); } @@ -347,35 +507,63 @@ public virtual CredentialViewModel DescribeCredential(Credential credential) { var kind = GetCredentialKind(credential.Type); Authenticator auther = null; + if (kind == CredentialKind.External) { string providerName = credential.Type.Split('.')[1]; - if (!Authenticators.TryGetValue(providerName, out auther)) - { - auther = null; - } + Authenticators.TryGetValue(providerName, out auther); } - return new CredentialViewModel() + var credentialViewModel = new CredentialViewModel { + Key = credential.Key, Type = credential.Type, TypeCaption = FormatCredentialType(credential.Type), Identity = credential.Identity, - Value = kind == CredentialKind.Token ? credential.Value : String.Empty, + Created = credential.Created, + Expires = credential.Expires, Kind = kind, - AuthUI = auther?.GetUI() + AuthUI = auther?.GetUI(), + // Set the description as the value for legacy API keys + Description = credential.Description, + Value = kind == CredentialKind.Token && credential.Description == null ? credential.Value : null, + Scopes = credential.Scopes.Select(s => new ScopeViewModel(s.Subject, NuGetScopes.Describe(s.AllowedAction))).ToList(), + ExpirationDuration = credential.ExpirationTicks != null ? new TimeSpan?(new TimeSpan(credential.ExpirationTicks.Value)) : null }; + + credentialViewModel.HasExpired = credential.HasExpired || + (credentialViewModel.IsNonScopedV1ApiKey && + !credential.HasBeenUsedInLastDays(_config.ExpirationInDaysForApiKeyV1)); + + credentialViewModel.Description = credentialViewModel.IsNonScopedV1ApiKey + ? Strings.NonScopedApiKeyDescription : credentialViewModel.Description; + + return credentialViewModel; } public virtual async Task RemoveCredential(User user, Credential cred) { - await Auditing.SaveAuditRecord(new UserAuditRecord(user, UserAuditAction.RemovedCredential, cred)); + await Auditing.SaveAuditRecordAsync(new UserAuditRecord(user, AuditedUserAction.RemoveCredential, cred)); user.Credentials.Remove(cred); Entities.Credentials.Remove(cred); await Entities.SaveChangesAsync(); } - public async virtual Task ReadExternalLoginCredential(IOwinContext context) + public virtual async Task EditCredentialScopes(User user, Credential cred, ICollection newScopes) + { + foreach (var oldScope in cred.Scopes.ToArray()) + { + Entities.Scopes.Remove(oldScope); + } + + cred.Scopes = newScopes; + + await Entities.SaveChangesAsync(); + + await Auditing.SaveAuditRecordAsync(new UserAuditRecord(user, AuditedUserAction.EditCredential, cred)); + } + + public virtual async Task ReadExternalLoginCredential(IOwinContext context) { var result = await context.Authentication.AuthenticateAsync(AuthenticationTypes.External); if (result == null) @@ -419,18 +607,18 @@ public async virtual Task ReadExternalLoginCred Authentication = null, ExternalIdentity = result.Identity, Authenticator = auther, - Credential = CredentialBuilder.CreateExternalCredential(authenticationType, idClaim.Value, nameClaim.Value + emailSuffix) + Credential = _credentialBuilder.CreateExternalCredential(authenticationType, idClaim.Value, nameClaim.Value + emailSuffix) }; } - public async virtual Task AuthenticateExternalLogin(IOwinContext context) + public virtual async Task AuthenticateExternalLogin(IOwinContext context) { var result = await ReadExternalLoginCredential(context); // Authenticate! if (result.Credential != null) { - result.Authentication = Authenticate(result.Credential); + result.Authentication = await Authenticate(result.Credential); } return result; @@ -477,23 +665,23 @@ private async Task ReplaceCredentialInternal(User user, Credential credential) if (toRemove.Any()) { - await Auditing.SaveAuditRecord(new UserAuditRecord( - user, UserAuditAction.RemovedCredential, toRemove)); + await Auditing.SaveAuditRecordAsync(new UserAuditRecord( + user, AuditedUserAction.RemoveCredential, toRemove)); } user.Credentials.Add(credential); - await Auditing.SaveAuditRecord(new UserAuditRecord( - user, UserAuditAction.AddedCredential, credential)); + await Auditing.SaveAuditRecordAsync(new UserAuditRecord( + user, AuditedUserAction.AddCredential, credential)); } private static CredentialKind GetCredentialKind(string type) { - if (type.StartsWith("apikey", StringComparison.OrdinalIgnoreCase)) + if (CredentialTypes.IsApiKey(type)) { return CredentialKind.Token; } - else if (type.StartsWith("password", StringComparison.OrdinalIgnoreCase)) + else if (CredentialTypes.IsPassword(type)) { return CredentialKind.Password; } @@ -535,9 +723,28 @@ private Credential FindMatchingCredential(Credential credential) .Set() .Include(u => u.User) .Include(u => u.User.Roles) + .Include(u => u.Scopes) .Where(c => c.Type == credential.Type && c.Value == credential.Value) .ToList(); + return ValidateFoundCredentials(results, credential.Type); + } + + private Credential FindMatchingApiKey(Credential apiKeyCredential) + { + var results = Entities + .Set() + .Include(u => u.User) + .Include(u => u.User.Roles) + .Include(u => u.Scopes) + .Where(c => c.Type.StartsWith(CredentialTypes.ApiKey.Prefix) && c.Value == apiKeyCredential.Value) + .ToList(); + + return ValidateFoundCredentials(results, "ApiKey"); + } + + private Credential ValidateFoundCredentials(List results, string credentialType) + { if (results.Count == 0) { return null; @@ -548,11 +755,11 @@ private Credential FindMatchingCredential(Credential credential) } else { - // Don't put the credential itself in trace, but do put the Key for lookup later. - string message = String.Format( + // Don't put the credential itself in trace, but do put the key for lookup later. + string message = string.Format( CultureInfo.CurrentCulture, Strings.MultipleMatchingCredentials, - credential.Type, + credentialType, results.First().Key); _trace.Error(message); throw new InvalidOperationException(message); @@ -587,50 +794,76 @@ private User FindByUserNameOrEmail(string userNameOrEmail) return user; } - public static bool ValidatePasswordCredential(IEnumerable creds, string password, out Credential matched) + private async Task UpdateFailedLoginAttempt(User user) { - matched = creds.FirstOrDefault(c => ValidatePasswordCredential(c, password)); - return matched != null; + user.FailedLoginCount += 1; + user.LastFailedLoginUtc = _dateTimeProvider.UtcNow; + + await Entities.SaveChangesAsync(); } - private static readonly Dictionary> _validators = new Dictionary>(StringComparer.OrdinalIgnoreCase) { - { CredentialTypes.Password.Pbkdf2, (password, cred) => CryptographyService.ValidateSaltedHash(cred.Value, password, Constants.PBKDF2HashAlgorithmId) }, - { CredentialTypes.Password.Sha1, (password, cred) => CryptographyService.ValidateSaltedHash(cred.Value, password, Constants.Sha1HashAlgorithmId) } - }; + private async Task UpdateSuccessfulLoginAttempt(User user) + { + user.FailedLoginCount = 0; + user.LastFailedLoginUtc = null; + + await Entities.SaveChangesAsync(); + } - public static bool ValidatePasswordCredential(Credential cred, string password) + private bool IsAccountLocked(User user, out int remainingMinutes) { - Func validator; - if (!_validators.TryGetValue(cred.Type, out validator)) + if (user.FailedLoginCount > 0) { - return false; + var currentTime = _dateTimeProvider.UtcNow; + var unlockTime = CalculateAccountUnlockTime(user.FailedLoginCount, user.LastFailedLoginUtc.Value); + + if (unlockTime > currentTime) + { + remainingMinutes = (int)Math.Ceiling((unlockTime - currentTime).TotalMinutes); + return true; + } } - return validator(password, cred); + + remainingMinutes = 0; + return false; + } + + private DateTime CalculateAccountUnlockTime(int failedLoginCount, DateTime lastFailedLogin) + { + int lockoutPeriodInMinutes = (int)Math.Pow(AccountLockoutMultiplierInMinutes, (int) ((double)failedLoginCount/AllowedLoginAttempts) - 1); + + return lastFailedLogin + TimeSpan.FromMinutes(lockoutPeriodInMinutes); + } + + public virtual bool ValidatePasswordCredential(IEnumerable creds, string password, out Credential matched) + { + matched = creds.FirstOrDefault(c => _credentialValidator.ValidatePasswordCredential(c, password)); + return matched != null; } private async Task MigrateCredentials(User user, List creds, string password) { var toRemove = creds.Where(c => - !String.Equals( + !string.Equals( c.Type, - CredentialTypes.Password.Pbkdf2, + CredentialBuilder.LatestPasswordType, StringComparison.OrdinalIgnoreCase)) .ToList(); - // Remove any non PBKDF2 credentials + // Remove any non latest credentials foreach (var cred in toRemove) { creds.Remove(cred); user.Credentials.Remove(cred); Entities.DeleteOnCommit(cred); } - await Auditing.SaveAuditRecord(new UserAuditRecord(user, UserAuditAction.RemovedCredential, toRemove)); + await Auditing.SaveAuditRecordAsync(new UserAuditRecord(user, AuditedUserAction.RemoveCredential, toRemove)); // Now add one if there are no credentials left if (creds.Count == 0) { - var newCred = CredentialBuilder.CreatePbkdf2Password(password); - await Auditing.SaveAuditRecord(new UserAuditRecord(user, UserAuditAction.AddedCredential, newCred)); + var newCred = _credentialBuilder.CreatePasswordCredential(password); + await Auditing.SaveAuditRecordAsync(new UserAuditRecord(user, AuditedUserAction.AddCredential, newCred)); user.Credentials.Add(newCred); } @@ -638,12 +871,4 @@ private async Task MigrateCredentials(User user, List creds, string await Entities.SaveChangesAsync(); } } - - public class AuthenticateExternalLoginResult - { - public AuthenticatedUser Authentication { get; set; } - public ClaimsIdentity ExternalIdentity { get; set; } - public Authenticator Authenticator { get; set; } - public Credential Credential { get; set; } - } } \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/AuthenticationTypes.cs b/src/NuGetGallery/Authentication/AuthenticationTypes.cs index 47a7fa4174..58bba94289 100644 --- a/src/NuGetGallery/Authentication/AuthenticationTypes.cs +++ b/src/NuGetGallery/Authentication/AuthenticationTypes.cs @@ -1,9 +1,5 @@ // 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.Web; namespace NuGetGallery.Authentication { diff --git a/src/NuGetGallery/Authentication/ForceSslWhenAuthenticatedMiddleware.cs b/src/NuGetGallery/Authentication/ForceSslWhenAuthenticatedMiddleware.cs index 85b458fc7e..3391bde936 100644 --- a/src/NuGetGallery/Authentication/ForceSslWhenAuthenticatedMiddleware.cs +++ b/src/NuGetGallery/Authentication/ForceSslWhenAuthenticatedMiddleware.cs @@ -47,7 +47,12 @@ public override async Task Invoke(IOwinContext context) // Invoke the rest of the pipeline await Next.Invoke(context); - var cookieOptions = new CookieOptions() { HttpOnly = true }; + var cookieOptions = new CookieOptions() + { + HttpOnly = true, + Secure = context.Request.IsSecure + }; + if (context.Authentication.AuthenticationResponseGrant != null) { _logger.WriteVerbose("Auth Grant found, writing Force SSL cookie"); @@ -59,10 +64,7 @@ public override async Task Invoke(IOwinContext context) { _logger.WriteVerbose("Auth Revoke found, removing Force SSL cookie"); // We're revoking authentication, so remove the force ssl cookie - context.Response.Cookies.Delete(CookieName, new CookieOptions() - { - HttpOnly = true - }); + context.Response.Cookies.Delete(CookieName, cookieOptions); } } } diff --git a/src/NuGetGallery/Authentication/NuGetClaims.cs b/src/NuGetGallery/Authentication/NuGetClaims.cs index fc21781451..df1289a110 100644 --- a/src/NuGetGallery/Authentication/NuGetClaims.cs +++ b/src/NuGetGallery/Authentication/NuGetClaims.cs @@ -1,9 +1,5 @@ // 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.Web; namespace NuGetGallery.Authentication { @@ -12,5 +8,7 @@ public static class NuGetClaims // Normally public consts are bad, but here we can't change the claim URL without messing // things up, so we should encourage that by using a const. public const string ApiKey = "https://claims.nuget.org/apikey"; + + public const string Scope = "https://claims.nuget.org/scope"; } } \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/NuGetPackagePattern.cs b/src/NuGetGallery/Authentication/NuGetPackagePattern.cs new file mode 100644 index 0000000000..75f66d5f22 --- /dev/null +++ b/src/NuGetGallery/Authentication/NuGetPackagePattern.cs @@ -0,0 +1,26 @@ +// 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.Text.RegularExpressions; + +namespace NuGetGallery.Authentication +{ + public static class NuGetPackagePattern + { + public const string AllInclusivePattern = "*"; + + /// + /// Compares the string against a given pattern. + /// + /// The string. + /// The pattern to match, where "*" means any sequence of characters, and "?" means any single character. + /// true if the string matches the given pattern; otherwise false. + public static bool MatchesPackagePattern(this string str, string globPattern) + { + return new Regex( + "^" + Regex.Escape(globPattern).Replace(@"\*", ".*") + "$", + RegexOptions.IgnoreCase | RegexOptions.Singleline + ).IsMatch(str); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/NuGetScopes.cs b/src/NuGetGallery/Authentication/NuGetScopes.cs new file mode 100644 index 0000000000..da8cdcaaee --- /dev/null +++ b/src/NuGetGallery/Authentication/NuGetScopes.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.Authentication +{ + public static class NuGetScopes + { + public const string All = "all"; + public const string PackagePushVersion = "package:pushversion"; + public const string PackagePush = "package:push"; + public const string PackageUnlist = "package:unlist"; + public const string PackageVerify = "package:verify"; + + public static string Describe(string scope) + { + switch (scope.ToLowerInvariant()) + { + case All: + return Strings.ScopeDescription_All; + case PackagePush: + return Strings.ScopeDescription_PushPackage; + case PackagePushVersion: + return Strings.ScopeDescription_PushPackageVersion; + case PackageUnlist: + return Strings.ScopeDescription_UnlistPackage; + case PackageVerify: + return Strings.ScopeDescription_VerifyPackage; + } + + return Strings.ScopeDescription_Unknown; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/PasswordAuthenticationResult.cs b/src/NuGetGallery/Authentication/PasswordAuthenticationResult.cs new file mode 100644 index 0000000000..3bd7927e73 --- /dev/null +++ b/src/NuGetGallery/Authentication/PasswordAuthenticationResult.cs @@ -0,0 +1,37 @@ +// 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.Authentication +{ + public class PasswordAuthenticationResult + { + public enum AuthenticationResult + { + AccountLocked, // The account is locked + BadCredentials, // Bad user name or password provided + Success // All good + } + + /// + /// The authentication status + /// + public AuthenticationResult Result { get; } + + /// + /// If the account is locked, this is the period of time until unlock. + /// + public int LockTimeRemainingMinutes { get; } + + /// + /// Is authentication was successful, this is the user details. + /// + public AuthenticatedUser AuthenticatedUser { get; } + + public PasswordAuthenticationResult(AuthenticationResult result, AuthenticatedUser authenticatedUser = null, int lockTimeRemainingMinutes = 0) + { + Result = result; + LockTimeRemainingMinutes = lockTimeRemainingMinutes; + AuthenticatedUser = authenticatedUser; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticationHandler.cs b/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticationHandler.cs index 07d044415a..15a13a506b 100644 --- a/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticationHandler.cs +++ b/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticationHandler.cs @@ -1,16 +1,17 @@ // 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.Globalization; using System.Linq; using System.Security.Claims; -using System.Text; using System.Threading.Tasks; using Microsoft.Owin; using Microsoft.Owin.Logging; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Infrastructure; +using Newtonsoft.Json; +using NuGetGallery.Infrastructure.Authentication; namespace NuGetGallery.Authentication.Providers.ApiKey { @@ -20,14 +21,32 @@ public class ApiKeyAuthenticationHandler : AuthenticationHandler AuthenticateCoreAsync() + protected override async Task AuthenticateCoreAsync() { var apiKey = Request.Headers[TheOptions.ApiKeyHeaderName]; - if (!String.IsNullOrEmpty(apiKey)) + if (!string.IsNullOrEmpty(apiKey)) { // Get the user - var authUser = Auth.Authenticate(CredentialBuilder.CreateV1ApiKey(apiKey)); + var authUser = await Auth.Authenticate(apiKey); if (authUser != null) { // Set the current user Context.Set(Constants.CurrentUserOwinEnvironmentKey, authUser); - return Task.FromResult( - new AuthenticationTicket( + // Fetch scopes and store them in a claim + var scopes = JsonConvert.SerializeObject( + authUser.CredentialUsed.Scopes, Formatting.None); + + // Create authentication ticket + return new AuthenticationTicket( AuthenticationService.CreateIdentity( authUser.User, AuthenticationTypes.ApiKey, - new Claim(NuGetClaims.ApiKey, apiKey)), - new AuthenticationProperties())); + new Claim(NuGetClaims.ApiKey, apiKey), + new Claim(NuGetClaims.Scope, scopes)), + new AuthenticationProperties()); } else { + // No user was matched Logger.WriteWarning("No match for API Key!"); } } @@ -98,7 +123,8 @@ protected override Task AuthenticateCoreAsync() { Logger.WriteVerbose("No API Key Header found in request."); } - return Task.FromResult(null); + + return null; } } } diff --git a/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticationMiddleware.cs b/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticationMiddleware.cs index 6232212256..c1c25f9e9a 100644 --- a/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticationMiddleware.cs +++ b/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticationMiddleware.cs @@ -1,13 +1,11 @@ // 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.Web; + using System.Web.Mvc; using Microsoft.Owin; using Microsoft.Owin.Logging; using Microsoft.Owin.Security.Infrastructure; +using NuGetGallery.Infrastructure.Authentication; using Owin; namespace NuGetGallery.Authentication.Providers.ApiKey @@ -26,7 +24,8 @@ protected override AuthenticationHandler CreateHand { return new ApiKeyAuthenticationHandler( _logger, - DependencyResolver.Current.GetService()); + DependencyResolver.Current.GetService(), + DependencyResolver.Current.GetService()); } } } \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticator.cs b/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticator.cs index 0a1e4ae04b..7bdcf79e86 100644 --- a/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticator.cs +++ b/src/NuGetGallery/Authentication/Providers/ApiKey/ApiKeyAuthenticator.cs @@ -1,9 +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; -using System.Collections.Generic; -using System.Linq; -using System.Web; + using NuGetGallery.Configuration; using Owin; @@ -11,12 +8,15 @@ namespace NuGetGallery.Authentication.Providers.ApiKey { public class ApiKeyAuthenticator : Authenticator { - protected override void AttachToOwinApp(ConfigurationService config, IAppBuilder app) + protected override void AttachToOwinApp(IGalleryConfigurationService config, IAppBuilder app) { - app.UseApiKeyAuthentication(new ApiKeyAuthenticationOptions() + app.Map("/api", api => { - ApiKeyHeaderName = Config.HeaderName, - ApiKeyClaim = Config.Claim + api.UseApiKeyAuthentication(new ApiKeyAuthenticationOptions + { + ApiKeyHeaderName = Config.HeaderName, + ApiKeyClaim = Config.Claim + }); }); } } diff --git a/src/NuGetGallery/Authentication/Providers/Authenticator.cs b/src/NuGetGallery/Authentication/Providers/Authenticator.cs index 84e3bcae7e..294d591247 100644 --- a/src/NuGetGallery/Authentication/Providers/Authenticator.cs +++ b/src/NuGetGallery/Authentication/Providers/Authenticator.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Web.Mvc; using NuGetGallery.Configuration; using Owin; @@ -13,8 +14,8 @@ namespace NuGetGallery.Authentication.Providers { public abstract class Authenticator { + public const string AuthPrefix = "Auth."; private static readonly Regex NameShortener = new Regex(@"^(?[A-Za-z0-9_]*)Authenticator$"); - private static readonly string AuthPrefix = "Auth."; public AuthenticatorConfiguration BaseConfig { get; private set; } @@ -28,9 +29,9 @@ protected Authenticator() BaseConfig = CreateConfigObject(); } - public void Startup(ConfigurationService config, IAppBuilder app) + public async Task Startup(IGalleryConfigurationService config, IAppBuilder app) { - Configure(config); + await Configure(config); if (BaseConfig.Enabled) { @@ -38,12 +39,12 @@ public void Startup(ConfigurationService config, IAppBuilder app) } } - protected virtual void AttachToOwinApp(ConfigurationService config, IAppBuilder app) { } + protected virtual void AttachToOwinApp(IGalleryConfigurationService config, IAppBuilder app) { } // Configuration Logic - public virtual void Configure(ConfigurationService config) + protected virtual async Task Configure(IGalleryConfigurationService config) { - BaseConfig = config.ResolveConfigObject(BaseConfig, AuthPrefix + Name + "."); + BaseConfig = await config.ResolveConfigObject(BaseConfig, AuthPrefix + Name + "."); } public static string GetName(Type authenticator) diff --git a/src/NuGetGallery/Authentication/Providers/AuthenticatorUI.cs b/src/NuGetGallery/Authentication/Providers/AuthenticatorUI.cs index 7e0dd9157c..a2aa044eb4 100644 --- a/src/NuGetGallery/Authentication/Providers/AuthenticatorUI.cs +++ b/src/NuGetGallery/Authentication/Providers/AuthenticatorUI.cs @@ -1,10 +1,5 @@ // 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.Text; -using Microsoft.Owin; namespace NuGetGallery.Authentication.Providers { diff --git a/src/NuGetGallery/Authentication/Providers/AzureActiveDirectory/AzureActiveDirectoryAuthenticator.cs b/src/NuGetGallery/Authentication/Providers/AzureActiveDirectory/AzureActiveDirectoryAuthenticator.cs index 172fe4be92..ec555db294 100644 --- a/src/NuGetGallery/Authentication/Providers/AzureActiveDirectory/AzureActiveDirectoryAuthenticator.cs +++ b/src/NuGetGallery/Authentication/Providers/AzureActiveDirectory/AzureActiveDirectoryAuthenticator.cs @@ -15,7 +15,7 @@ public class AzureActiveDirectoryAuthenticator : Authenticator + /// Evaluates if a scope claim allows at least one of the requested actions for a subject. + /// + /// Json serialized array of + /// The subject. + /// A list of requested actions + public static bool ScopeClaimsAllowsActionForSubject( + string scopeClaim, + string subject, + string[] requestedActions) + { + if (IsEmptyScopeClaim(scopeClaim)) + { + // Legacy API key, allow access... + return true; + } + + // Deserialize scope claim + var scopesFromClaim = JsonConvert.DeserializeObject>(scopeClaim); + foreach (var scopeFromClaim in scopesFromClaim) + { + var subjectMatches = string.IsNullOrEmpty(subject) || (!string.IsNullOrEmpty(subject) && subject.MatchesPackagePattern(scopeFromClaim.Subject)); + + var actionMatches = requestedActions.Any( + allowed => string.IsNullOrEmpty(allowed) + || string.IsNullOrEmpty(scopeFromClaim.AllowedAction) + || string.Equals(scopeFromClaim.AllowedAction, allowed, StringComparison.OrdinalIgnoreCase) + || string.Equals(scopeFromClaim.AllowedAction, NuGetScopes.All, StringComparison.OrdinalIgnoreCase)); + + if (subjectMatches && actionMatches) + { + return true; + } + } + + return false; + } + + public static bool IsEmptyScopeClaim(string scopeClaim) + { + return string.IsNullOrEmpty(scopeClaim) || scopeClaim == "[]"; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Configuration/AppConfiguration.cs b/src/NuGetGallery/Configuration/AppConfiguration.cs index 76edc53841..c39f39c416 100644 --- a/src/NuGetGallery/Configuration/AppConfiguration.cs +++ b/src/NuGetGallery/Configuration/AppConfiguration.cs @@ -92,6 +92,12 @@ public class AppConfiguration : IAppConfiguration [TypeConverter(typeof(MailAddressConverter))] public MailAddress GalleryOwner { get; set; } + /// + /// Gets the gallery e-mail from name and email address + /// + [TypeConverter(typeof(MailAddressConverter))] + public MailAddress GalleryNoReplyAddress { get; set; } + /// /// Gets the storage mechanism used by this instance of the gallery /// @@ -168,6 +174,28 @@ public class AppConfiguration : IAppConfiguration /// public string EnforcedAuthProviderForAdmin { get; set; } + /// + /// A regex to validate password format. The default regex requires the password to be atlease 8 characters, + /// include at least one uppercase letter, one lowercase letter and a digit. + /// + [Required] + [DefaultValue("^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).{8,64}$")] + public string UserPasswordRegex { get; set; } + + [Required] + [DefaultValue("Your password must be at least 8 characters, should include at least one uppercase letter, one lowercase letter and a digit.")] + public string UserPasswordHint { get; set; } + + /// + /// Defines the time after which V1 API keys expire. + /// + public int ExpirationInDaysForApiKeyV1 { get; set; } + + /// + /// Defines the number of days before the API key expires when the server should emit a warning to the client. + /// + public int WarnAboutExpirationInDaysForApiKeyV1 { get; set; } + /// /// Gets a string containing the PagerDuty account name. /// @@ -182,5 +210,10 @@ public class AppConfiguration : IAppConfiguration /// Gets a string containing the PagerDuty Service key. /// public string PagerDutyServiceKey { get; set; } + + /// + /// Gets/sets a bool that indicates if the OData requests will be filtered. + /// + public bool IsODataFilterEnabled { get; set; } } } diff --git a/src/NuGetGallery/Configuration/ConfigurationService.cs b/src/NuGetGallery/Configuration/ConfigurationService.cs index 241aac91e4..eed59228af 100644 --- a/src/NuGetGallery/Configuration/ConfigurationService.cs +++ b/src/NuGetGallery/Configuration/ConfigurationService.cs @@ -8,73 +8,85 @@ using System.Configuration; using System.Globalization; using System.Linq; +using System.Threading.Tasks; using System.Web; using System.Web.Configuration; using Microsoft.WindowsAzure.ServiceRuntime; -using PoliteCaptcha; +using NuGet.Services.KeyVault; +using NuGetGallery.Configuration.SecretReader; namespace NuGetGallery.Configuration { - public class ConfigurationService : IConfigurationSource + public class ConfigurationService : PoliteCaptcha.IConfigurationSource, IGalleryConfigurationService { - private const string _settingPrefix = "Gallery."; - private const string _featurePrefix = "Feature."; + protected const string SettingPrefix = "Gallery."; + protected const string FeaturePrefix = "Feature."; private bool _notInCloud; - private IAppConfiguration _current; private readonly Lazy _httpSiteRootThunk; private readonly Lazy _httpsSiteRootThunk; - private FeatureConfiguration _features; + private ISecretReaderFactory _secretReaderFactory; + private Lazy _secretInjector; + private Lazy _lazyAppConfiguration; + private Lazy _lazyFeatureConfiguration; - public ConfigurationService() + public ConfigurationService(ISecretReaderFactory secretReaderFactory) { + if (secretReaderFactory == null) + { + throw new ArgumentNullException(nameof(secretReaderFactory)); + } + + _secretReaderFactory = secretReaderFactory; + _secretInjector = new Lazy(InitSecretInjector, isThreadSafe: false); + _httpSiteRootThunk = new Lazy(GetHttpSiteRoot); _httpsSiteRootThunk = new Lazy(GetHttpsSiteRoot); + + _lazyAppConfiguration = new Lazy(() => ResolveSettings().Result); + _lazyFeatureConfiguration = new Lazy(() => ResolveFeatures().Result); } - public virtual IAppConfiguration Current + public static IEnumerable GetConfigProperties(T instance) { - get { return _current ?? (_current = ResolveSettings()); } - set { _current = value; } + return TypeDescriptor.GetProperties(instance).Cast().Where(p => !p.IsReadOnly); } - public virtual FeatureConfiguration Features + /// + /// PoliteCaptcha.IConfigurationSource implementation + /// + public string GetConfigurationValue(string key) { - get { return _features ?? (_features = ResolveFeatures()); } - set { _features = value; } + // Fudge the name because Azure cscfg system doesn't allow : in setting names + // Used by PoliteCaptcha + return ReadSetting(key.Replace("::", ".")).Result; } + public IAppConfiguration Current => _lazyAppConfiguration.Value; + + public FeatureConfiguration Features => _lazyFeatureConfiguration.Value; + /// /// Gets the site root using the specified protocol /// /// If true, the root will be returned in HTTPS form, otherwise, HTTP. /// - public virtual string GetSiteRoot(bool useHttps) + public string GetSiteRoot(bool useHttps) { return useHttps ? _httpsSiteRootThunk.Value : _httpSiteRootThunk.Value; } - public virtual FeatureConfiguration ResolveFeatures() - { - return ResolveConfigObject(new FeatureConfiguration(), _featurePrefix); - } - - public virtual IAppConfiguration ResolveSettings() - { - return ResolveConfigObject(new AppConfiguration(), _settingPrefix); - } - - public virtual T ResolveConfigObject(T instance, string prefix) + public async Task ResolveConfigObject(T instance, string prefix) { // Iterate over the properties foreach (var property in GetConfigProperties(instance)) { // Try to get a config setting value - string baseName = String.IsNullOrEmpty(property.DisplayName) ? property.Name : property.DisplayName; + string baseName = string.IsNullOrEmpty(property.DisplayName) ? property.Name : property.DisplayName; string settingName = prefix + baseName; - string value = ReadSetting(settingName); + string value = await ReadSetting(settingName); - if (String.IsNullOrEmpty(value)) + if (string.IsNullOrEmpty(value)) { var defaultValue = property.Attributes.OfType().FirstOrDefault(); if (defaultValue != null && defaultValue.Value != null) @@ -91,7 +103,7 @@ public virtual T ResolveConfigObject(T instance, string prefix) } } - if (!String.IsNullOrEmpty(value)) + if (!string.IsNullOrEmpty(value)) { if (property.PropertyType.IsAssignableFrom(typeof(string))) { @@ -105,43 +117,58 @@ public virtual T ResolveConfigObject(T instance, string prefix) } else if (property.Attributes.OfType().Any()) { - throw new ConfigurationErrorsException(String.Format(CultureInfo.InvariantCulture, "Missing required configuration setting: '{0}'", settingName)); + throw new ConfigurationErrorsException(string.Format(CultureInfo.InvariantCulture, "Missing required configuration setting: '{0}'", settingName)); } } return instance; } - - internal static IEnumerable GetConfigProperties(T instance) - { - return TypeDescriptor.GetProperties(instance).Cast().Where(p => !p.IsReadOnly); - } - - public virtual string ReadSetting(string settingName) + + public async Task ReadSetting(string settingName) { string value; - var cstr = GetConnectionString(settingName); - if (cstr != null) + + value = GetCloudSetting(settingName); + + if (value == "null") { - value = cstr.ConnectionString; + value = null; } - else + else if (string.IsNullOrEmpty(value)) { - value = GetAppSetting(settingName); + var cstr = GetConnectionString(settingName); + value = cstr != null ? cstr.ConnectionString : GetAppSetting(settingName); } - string cloudValue = GetCloudSetting(settingName); - if (string.IsNullOrEmpty(cloudValue)) + if (!string.IsNullOrEmpty(value)) { - return value; - } - else if (cloudValue == "null") - { - return null; + value = await _secretInjector.Value.InjectAsync(value); } - return cloudValue; + + return value; + } + + protected virtual HttpRequestBase GetCurrentRequest() + { + return new HttpRequestWrapper(HttpContext.Current.Request); + } + + + private ISecretInjector InitSecretInjector() + { + return _secretReaderFactory.CreateSecretInjector(_secretReaderFactory.CreateSecretReader(new ConfigurationService(new EmptySecretReaderFactory()))); + } + + private async Task ResolveFeatures() + { + return await ResolveConfigObject(new FeatureConfiguration(), FeaturePrefix); } - public virtual string GetCloudSetting(string settingName) + private async Task ResolveSettings() + { + return await ResolveConfigObject(new AppConfiguration(), SettingPrefix); + } + + protected virtual string GetCloudSetting(string settingName) { // Short-circuit if we've already determined we're not in the cloud if (_notInCloud) @@ -174,21 +201,16 @@ public virtual string GetCloudSetting(string settingName) return value; } - public virtual string GetAppSetting(string settingName) + protected virtual string GetAppSetting(string settingName) { return WebConfigurationManager.AppSettings[settingName]; } - public virtual ConnectionStringSettings GetConnectionString(string settingName) + protected virtual ConnectionStringSettings GetConnectionString(string settingName) { return WebConfigurationManager.ConnectionStrings[settingName]; } - - protected virtual HttpRequestBase GetCurrentRequest() - { - return new HttpRequestWrapper(HttpContext.Current.Request); - } - + private string GetHttpSiteRoot() { var request = GetCurrentRequest(); @@ -228,11 +250,5 @@ private string GetHttpsSiteRoot() return "https://" + siteRoot.Substring(7); } - - string IConfigurationSource.GetConfigurationValue(string key) - { - // Fudge the name because Azure cscfg system doesn't allow : in setting names - return ReadSetting(key.Replace("::", ".")); - } } } \ No newline at end of file diff --git a/src/NuGetGallery/Configuration/FeatureConfiguration.cs b/src/NuGetGallery/Configuration/FeatureConfiguration.cs index 77c9b29228..2d66628a6e 100644 --- a/src/NuGetGallery/Configuration/FeatureConfiguration.cs +++ b/src/NuGetGallery/Configuration/FeatureConfiguration.cs @@ -1,10 +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; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -using System.Text; namespace NuGetGallery.Configuration { @@ -17,6 +13,12 @@ public class FeatureConfiguration [Description("Displays reports on license data")] public virtual bool FriendlyLicenses { get; set; } + /// Gets a boolean indicating if package download counts should be recorded in the local database. + /// + [DefaultValue(false)] // Default: Disabled + [Description("Indicates if package download counts should be recorded in the local database")] + public virtual bool TrackPackageDownloadCountInLocalDatabase { get; set; } + /// /// Gets a boolean indicating if social networks share buttons show be displayed. /// diff --git a/src/NuGetGallery/Configuration/IAppConfiguration.cs b/src/NuGetGallery/Configuration/IAppConfiguration.cs index d1d40d5b2a..006c575a09 100644 --- a/src/NuGetGallery/Configuration/IAppConfiguration.cs +++ b/src/NuGetGallery/Configuration/IAppConfiguration.cs @@ -88,6 +88,11 @@ public interface IAppConfiguration /// MailAddress GalleryOwner { get; set; } + /// + /// Gets the gallery e-mail from name and email address + /// + MailAddress GalleryNoReplyAddress { get; set; } + /// /// Gets the storage mechanism used by this instance of the gallery /// @@ -155,6 +160,26 @@ public interface IAppConfiguration /// string EnforcedAuthProviderForAdmin { get; set; } + /// + /// The required format for a user password. + /// + string UserPasswordRegex { get; set; } + + /// + /// A message to show the user, to explain password requirements. + /// + string UserPasswordHint { get; set; } + + /// + /// Defines the time after which V1 API keys expire. + /// + int ExpirationInDaysForApiKeyV1 { get; set; } + + /// + /// Defines the number of days before the API key expires when the server should emit a warning to the client. + /// + int WarnAboutExpirationInDaysForApiKeyV1 { get; set; } + /// /// Gets a string containing the PagerDuty account name. /// @@ -171,5 +196,10 @@ public interface IAppConfiguration /// // ReSharper disable once InconsistentNaming string PagerDutyServiceKey { get; set; } + + /// + /// Gets/sets a bool that indicates if the OData requests will be filtered. + /// + bool IsODataFilterEnabled { get; set; } } } diff --git a/src/NuGetGallery/Configuration/IGalleryConfigurationService.cs b/src/NuGetGallery/Configuration/IGalleryConfigurationService.cs new file mode 100644 index 0000000000..6f05ab7a38 --- /dev/null +++ b/src/NuGetGallery/Configuration/IGalleryConfigurationService.cs @@ -0,0 +1,30 @@ +// 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.Threading.Tasks; + +namespace NuGetGallery.Configuration +{ + public interface IGalleryConfigurationService + { + IAppConfiguration Current { get; } + + FeatureConfiguration Features { get; } + + /// + /// Gets the site root using the specified protocol + /// + /// If true, the root will be returned in HTTPS form, otherwise, HTTP. + string GetSiteRoot(bool useHttps); + + /// + /// Populate the properties of from configuration. + /// + /// The type to populate. + /// The instance. + /// The prefix of the properties in the config. + Task ResolveConfigObject(T instance, string prefix); + + Task ReadSetting(string settingName); + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Configuration/SecretReader/CachingSecretReader.cs b/src/NuGetGallery/Configuration/SecretReader/CachingSecretReader.cs new file mode 100644 index 0000000000..45cd9f17a6 --- /dev/null +++ b/src/NuGetGallery/Configuration/SecretReader/CachingSecretReader.cs @@ -0,0 +1,47 @@ +// 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.Threading.Tasks; +using NuGet.Services.KeyVault; +using NuGetGallery.Diagnostics; + +namespace NuGetGallery.Configuration.SecretReader +{ + public class CachingSecretReader : ISecretReader + { + private ISecretReader _internalReader; + private Dictionary _cache; + private IDiagnosticsSource _trace; + + public CachingSecretReader(ISecretReader secretReader, IDiagnosticsService diagnosticsService) + { + if (secretReader == null) + { + throw new ArgumentNullException(nameof(secretReader)); + } + + if (diagnosticsService == null) + { + throw new ArgumentNullException(nameof(diagnosticsService)); + } + + _internalReader = secretReader; + _cache = new Dictionary(); + _trace = diagnosticsService.GetSource("CachingSecretReader"); + } + + public async Task GetSecretAsync(string secretName) + { + if (!_cache.ContainsKey(secretName)) + { + _trace.Information("Cache miss for setting " + secretName); + var secretValue = await _internalReader.GetSecretAsync(secretName); + _cache[secretName] = secretValue; + } + + return _cache[secretName]; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Configuration/SecretReader/EmptySecretReaderFactory.cs b/src/NuGetGallery/Configuration/SecretReader/EmptySecretReaderFactory.cs new file mode 100644 index 0000000000..cec9557e5d --- /dev/null +++ b/src/NuGetGallery/Configuration/SecretReader/EmptySecretReaderFactory.cs @@ -0,0 +1,20 @@ +// 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 NuGet.Services.KeyVault; + +namespace NuGetGallery.Configuration.SecretReader +{ + public class EmptySecretReaderFactory : ISecretReaderFactory + { + public ISecretInjector CreateSecretInjector(ISecretReader secretReader) + { + return new SecretInjector(secretReader); + } + + public ISecretReader CreateSecretReader(IGalleryConfigurationService configurationService) + { + return new EmptySecretReader(); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Configuration/SecretReader/ISecretReaderFactory.cs b/src/NuGetGallery/Configuration/SecretReader/ISecretReaderFactory.cs new file mode 100644 index 0000000000..0f9e32e088 --- /dev/null +++ b/src/NuGetGallery/Configuration/SecretReader/ISecretReaderFactory.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 NuGet.Services.KeyVault; + +namespace NuGetGallery.Configuration.SecretReader +{ + public interface ISecretReaderFactory + { + ISecretInjector CreateSecretInjector(ISecretReader secretReader); + + ISecretReader CreateSecretReader(IGalleryConfigurationService configurationService); + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Configuration/SecretReader/SecretReaderFactory.cs b/src/NuGetGallery/Configuration/SecretReader/SecretReaderFactory.cs new file mode 100644 index 0000000000..171beb0c61 --- /dev/null +++ b/src/NuGetGallery/Configuration/SecretReader/SecretReaderFactory.cs @@ -0,0 +1,70 @@ +// 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.Globalization; +using NuGet.Services.KeyVault; +using NuGetGallery.Diagnostics; + +namespace NuGetGallery.Configuration.SecretReader +{ + public class SecretReaderFactory : ISecretReaderFactory + { + internal const string KeyVaultConfigurationPrefix = "KeyVault."; + internal const string VaultNameConfigurationKey = "VaultName"; + internal const string ClientIdConfigurationKey = "ClientId"; + internal const string CertificateThumbprintConfigurationKey = "CertificateThumbprint"; + private IDiagnosticsService _diagnosticsService; + + public SecretReaderFactory(IDiagnosticsService diagnosticsService) + { + if (diagnosticsService == null) + { + throw new ArgumentNullException(nameof(diagnosticsService)); + } + + _diagnosticsService = diagnosticsService; + } + + public ISecretInjector CreateSecretInjector(ISecretReader secretReader) + { + if (secretReader == null) + { + throw new ArgumentNullException(nameof(secretReader)); + } + + return new SecretInjector(secretReader); + } + + public ISecretReader CreateSecretReader(IGalleryConfigurationService configurationService) + { + if (configurationService == null) + { + throw new ArgumentNullException(nameof(configurationService)); + } + + ISecretReader secretReader; + + var vaultName = configurationService.ReadSetting( + string.Format(CultureInfo.InvariantCulture, "{0}{1}", KeyVaultConfigurationPrefix, VaultNameConfigurationKey)).Result; + + if (!string.IsNullOrEmpty(vaultName)) + { + var clientId = configurationService.ReadSetting( + string.Format(CultureInfo.InvariantCulture, "{0}{1}", KeyVaultConfigurationPrefix, ClientIdConfigurationKey)).Result; + var certificateThumbprint = configurationService.ReadSetting( + string.Format(CultureInfo.InvariantCulture, "{0}{1}", KeyVaultConfigurationPrefix, CertificateThumbprintConfigurationKey)).Result; + + var keyVaultConfiguration = new KeyVaultConfiguration(vaultName, clientId, certificateThumbprint, validateCertificate: true); + + secretReader = new KeyVaultReader(keyVaultConfiguration); + } + else + { + secretReader = new EmptySecretReader(); + } + + return new CachingSecretReader(secretReader, _diagnosticsService); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Constants.cs b/src/NuGetGallery/Constants.cs index 36e774ad79..3cd3044230 100644 --- a/src/NuGetGallery/Constants.cs +++ b/src/NuGetGallery/Constants.cs @@ -11,7 +11,15 @@ public static class Constants public const string AlphabeticSortOrder = "package-title"; public const int DefaultPackageListPageSize = 20; public const string DefaultPackageListSortOrder = "package-download-count"; - public const int DefaultPasswordResetTokenExpirationHours = 24; + public const int PasswordResetTokenExpirationHours = 1; + + /// + /// Parameters for calculating account lockout period after + /// wrong password entry. + /// + public const double AccountLockoutMultiplierInMinutes = 10; + public const double AllowedLoginAttempts = 10; + public const int MaxEmailSubjectLength = 255; internal static readonly NuGetVersion MaxSupportedMinClientVersion = new NuGetVersion("3.4.0.0"); public const string PackageContentType = "binary/octet-stream"; @@ -44,6 +52,9 @@ public static class Constants public const string UrlValidationErrorMessage = "This doesn't appear to be a valid HTTP/HTTPS URL"; internal const string ApiKeyHeaderName = "X-NuGet-ApiKey"; + internal const string ClientVersionHeaderName = "X-NuGet-Client-Version"; + internal const string WarningHeaderName = "X-NuGet-Warning"; + public static readonly string ReturnUrlParameterName = "ReturnUrl"; public static readonly string CurrentUserOwinEnvironmentKey = "nuget.user"; @@ -55,5 +66,13 @@ public static class ContentNames public static readonly string PrivacyPolicy = "Privacy-Policy"; public static readonly string Team = "Team"; } + + public static class StatisticsDimensions + { + public const string Version = "Version"; + public const string ClientName = "ClientName"; + public const string ClientVersion = "ClientVersion"; + public const string Operation = "Operation"; + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Content/Images/icons/apiKey.png b/src/NuGetGallery/Content/Images/icons/apiKey.png new file mode 100644 index 0000000000..1c08c60d14 Binary files /dev/null and b/src/NuGetGallery/Content/Images/icons/apiKey.png differ diff --git a/src/NuGetGallery/Content/Images/icons/apiKeyExpired.png b/src/NuGetGallery/Content/Images/icons/apiKeyExpired.png new file mode 100644 index 0000000000..155b11895d Binary files /dev/null and b/src/NuGetGallery/Content/Images/icons/apiKeyExpired.png differ diff --git a/src/NuGetGallery/Content/Images/icons/apiKeyLegacy.png b/src/NuGetGallery/Content/Images/icons/apiKeyLegacy.png new file mode 100644 index 0000000000..bf68d08af3 Binary files /dev/null and b/src/NuGetGallery/Content/Images/icons/apiKeyLegacy.png differ diff --git a/src/NuGetGallery/Content/Images/icons/apiKeyNew.png b/src/NuGetGallery/Content/Images/icons/apiKeyNew.png new file mode 100644 index 0000000000..b7b2185013 Binary files /dev/null and b/src/NuGetGallery/Content/Images/icons/apiKeyNew.png differ diff --git a/src/NuGetGallery/Content/Images/icons/copy.png b/src/NuGetGallery/Content/Images/icons/copy.png new file mode 100644 index 0000000000..be6288e96f Binary files /dev/null and b/src/NuGetGallery/Content/Images/icons/copy.png differ diff --git a/src/NuGetGallery/Content/Images/icons/delete.png b/src/NuGetGallery/Content/Images/icons/delete.png new file mode 100644 index 0000000000..934aa9a6e5 Binary files /dev/null and b/src/NuGetGallery/Content/Images/icons/delete.png differ diff --git a/src/NuGetGallery/Content/Images/icons/edit.png b/src/NuGetGallery/Content/Images/icons/edit.png new file mode 100644 index 0000000000..7714b95624 Binary files /dev/null and b/src/NuGetGallery/Content/Images/icons/edit.png differ diff --git a/src/NuGetGallery/Content/Images/icons/expire.png b/src/NuGetGallery/Content/Images/icons/expire.png new file mode 100644 index 0000000000..46676dd7c6 Binary files /dev/null and b/src/NuGetGallery/Content/Images/icons/expire.png differ diff --git a/src/NuGetGallery/Content/Images/icons/regenerate.png b/src/NuGetGallery/Content/Images/icons/regenerate.png new file mode 100644 index 0000000000..cb4133ee7a Binary files /dev/null and b/src/NuGetGallery/Content/Images/icons/regenerate.png differ diff --git a/src/NuGetGallery/Content/PageStylings.css b/src/NuGetGallery/Content/PageStylings.css index 2945f580c3..afb8792139 100644 --- a/src/NuGetGallery/Content/PageStylings.css +++ b/src/NuGetGallery/Content/PageStylings.css @@ -43,38 +43,139 @@ /* Account/API Key */ -#apiKey h1 { - font-size: 2em; +#apiKeyExpired { + color: red; + font-weight: 600; } -#apiKey pre { +.apiKey { background-color: #202020; border: 2px solid #c0c0c0; color: #c0c0c0; - display: block; + display: inline-block; font: 1.2em 'andale mono', 'lucida console', monospace; line-height: 1.5em; - margin: 15px 0 0; overflow: auto; padding: 15px; } -#generateKey { - position: relative; +.packagecheckbox { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - #generateKey h2 { - font-size: 1.7em; - } +/* Api keys container */ +#apikeyscontainer { + background-color: white; + padding-top: 5px; + padding-left: 10px; + padding-right: 10px; + padding-bottom: 10px; +} - #generateKey #key { - font-size: 1.7em; - margin-bottom: 0; + #apikeyscontainer hr { + margin-bottom: 0px; } - #generateKey .form input[type="submit"] { - width: auto; - } +/* new api key form*/ +#newapikey-1-content { + padding: 5px; +} + +#addapikeypopup { + margin-left: -220px; +} + +#addapikeypopup, .editapikeypopup { + width: 360px; + border-radius: 6px; + margin-top: 10px; +} + +#addkeyselectpackages, #addkeypackageGlobPattern { + margin-top: 5px; + margin-bottom: 15px; +} + +#addkeyselectscopes { + margin-top: 5px; + margin-bottom: 25px; +} + +.nopackageslist { + padding-left: 10px; + padding-top: 5px; + display: block; +} + +/* Edit api key */ +.editapikeypopup { + margin-left: -275px; +} + +.popup .editapikeypopup::after { + left: 150px; +} + +.popup .editapikeypopup::before { + left: 149px; +} + +.details-table tr.editpackagetr + td { + border-top: none; + padding-left: 10px; +} + +.details-table tr.editpackagetr td { + border-top: none; +} + +.editpackagetd table td { + padding-left: 0px; + padding-top: 0px; +} + +#editkeypackageGlobPattern { + margin-top: 5px; + margin-bottom: 8px; +} + + + +#newapikey-1-actions { + margin-top: 0px; +} + +#selectablePackagesList { + max-height: 300px; + min-height: 100px; + width: 500px +} + +.scrollable-div.editpackagelist { + max-height: 300px; + min-height: 100px; + width: 450px; +} + +/* Details table */ +.details-table .shortlist { + text-overflow: ellipsis; + overflow: hidden; + display: inline-block; + vertical-align: bottom; + max-width: 570px; +} + +.details-table .packageslist+b { + display: inline-block; +} + +.details-table .shortlist+a { + display: inline-block; +} + /* Package Page (Display Package) */ @@ -93,16 +194,16 @@ padding-bottom: 10px; } - .package-page-heading h1, - .package-page-heading h2 { - margin: 0; - padding: 0; - display: inline; - } +.package-page-heading h1, +.package-page-heading h2 { + margin: 0; + padding: 0; + display: inline; +} - .package-page-heading h2 { - padding-left: 7px; - } +.package-page-heading h2 { + padding-left: 7px; +} /* NuGet Badge (Display Package) */ @@ -166,11 +267,11 @@ padding: 0; } - #dependencySets > li > h4 { - border-bottom: solid 1px #333; - display: inline; - margin: 2px 0; - } +#dependencySets > li > h4 { + border-bottom: solid 1px #333; + display: inline; + margin: 2px 0; +} ul.dependencySet { list-style: none; @@ -410,7 +511,7 @@ form .async-upload-progress-advance { } /* Account Page */ -#account-accordian { +#account-accordion { clear: both; } diff --git a/src/NuGetGallery/Content/Site.css b/src/NuGetGallery/Content/Site.css index 08abe795d3..5cf03d2010 100644 --- a/src/NuGetGallery/Content/Site.css +++ b/src/NuGetGallery/Content/Site.css @@ -60,6 +60,7 @@ h4 { h5 { font-size: 1.1em; + font-weight: 500; } h6 { @@ -144,6 +145,7 @@ a { color: #0071bc; outline: none; text-decoration: none; + cursor: pointer; } a:hover { @@ -204,6 +206,10 @@ nav, section { display: block; } +th { + text-align: left; +} + /* General */ .message { @@ -225,6 +231,14 @@ nav, section { color: #ff9600; } +.warning.fancy { + color: black; + border-color: #f7d3a5; + font-size: 1em; + font-weight: 500; + padding: 5px; +} + /* Logo */ #logo { @@ -619,73 +633,74 @@ ul.pager { font-size: 1.1em; } - /* header */ - .sexy-table thead tr { - border-bottom: solid 1px #333; - } +/* header */ - .sexy-table th { - font-size: 1.25em; - font-weight: normal; - padding: 5px 15px 0px 0px; - text-align: left; - } +.sexy-table thead tr { + border-bottom: solid 1px #333; +} - /* actions */ +.sexy-table th { + font-size: 1.25em; + font-weight: normal; + padding: 5px 15px 0px 0px; + text-align: left; +} - .sexy-table th.actions { - text-indent: -9999px; - } +/* actions */ - .sexy-table td.actions { - width: 32px; - } +.sexy-table th.actions { + text-indent: -9999px; +} - .sexy-table td.actions a.table-action-link { - text-decoration: none; - } +.sexy-table td.actions { + width: 32px; +} - /* body */ +.sexy-table td.actions a.table-action-link { + text-decoration: none; +} - .sexy-table tbody tr { - margin-bottom: 10px; - } +/* body */ - .sexy-table tbody tr:hover { - background-color: #f4f5f6; - } +.sexy-table tbody tr { + margin-bottom: 10px; +} - .sexy-table tbody tr.recommended { - font-weight: 800; - } +.sexy-table tbody tr:hover { + background-color: #f4f5f6; +} - .sexy-table tbody td { - padding: 5px 25px 5px 0; - } +.sexy-table tbody tr.recommended { + font-weight: 800; +} - .sexy-table tbody td.actions { - padding: 2px 5px; - } +.sexy-table tbody td { + padding: 5px 25px 5px 0; +} - .sexy-table tbody td.package-version-downloads { - text-align: right - } +.sexy-table tbody td.actions { + padding: 2px 5px; +} - /* footer */ +.sexy-table tbody td.package-version-downloads { + text-align: right +} - .sexy-table tfoot { - border-bottom: solid 1px #333; - font-weight: 600; - } +/* footer */ - .sexy-table tfoot td { - padding: 3px 0; - } +.sexy-table tfoot { + border-bottom: solid 1px #333; + font-weight: 600; +} - .sexy-table tfoot tr { - border-top: 2px solid #333; - } +.sexy-table tfoot td { + padding: 3px 0; +} + +.sexy-table tfoot tr { + border-top: 2px solid #333; +} /* Pivot Table */ @@ -871,9 +886,9 @@ fieldset.form { padding: 0; } - fieldset.form legend { - display: none; - } +fieldset.form legend { + display: none; +} .form-field p { margin-left: 10px; @@ -883,91 +898,116 @@ fieldset.form { color: #52a4ca; } +.form-field h4 { + color: #52a4ca; + font-size: 1.3em; + margin-bottom: 5px; +} + .form-field { margin-bottom: 10px; position: relative; } - .form-field label { - color: #52a4ca; - display: block; - font-size: 1.25em; - margin-bottom: 5px; - } +.form-field label { + color: #52a4ca; + display: block; + font-size: 1.25em; + margin-bottom: 5px; +} - .form-field label.checkbox { - display: inline; - } +.form-field label.checkoboxsmall { + color: #333; + font-size: 100%; + display: block; + margin-bottom: 5px; +} - .form-field textarea, - .form-field input[type="email"], - .form-field input[type="text"], - .form-field input[type="file"], - .form-field input[type="url"], - .form-field input[type="password"] { - background: #fff url("../content/images/inputBackground.png") repeat-x; - border: solid 1px #ccc; - color: #7f8c7d; - font-size: 1.25em; - padding: 5px 0 5px 10px; - vertical-align: middle; - width: 400px; - /* This won't work in IE7, but it will only produce a minor layout quirk */ - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - } +.form-field label.checkoboxsmall.inline { + display: inline-block; +} - .form-field.form-field-full input { - width: 100%; - } +.form-field label.checkbox { + display: inline; +} - .form-field input[data-val-required], - .form-field textarea[data-val-required] { - border-left: solid 5px #52a4ca; - } +.form-field textarea, +.form-field input[type="email"], +.form-field input[type="text"], +.form-field input[type="file"], +.form-field input[type="url"], +.form-field input[type="password"] { + background: #fff url("../content/images/inputBackground.png") repeat-x; + border: solid 1px #ccc; + color: #7f8c7d; + font-size: 1.25em; + padding: 5px 0 5px 10px; + vertical-align: middle; + width: 400px; + /* This won't work in IE7, but it will only produce a minor layout quirk */ + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + height: 2.25em; +} - .form-field input[data-edited=true], - .form-field textarea[data-edited=true] { - border-left: solid 5px #2ef12e; - padding-left: 6px; - } +.form-field.form-field-full input { + width: 100%; +} - .form-field input[type="checkbox"] { - border-left: none; - } +.form-field input[data-val-required], +.form-field textarea[data-val-required] { + border-left: solid 5px #52a4ca; +} - .form-field input[type="url"] { - width: 100%; - } +.form-field input[data-edited=true], +.form-field textarea[data-edited=true] { + border-left: solid 5px #2ef12e; + padding-left: 6px; +} - .form-field select { - color: #7f8c7d; - font-size: 1.25em; - padding: 2px; - margin: 0px 0px 10px 0; - } +.form-field input[type="checkbox"] { + border-left: none; +} - .form-field select[data-edited=true] { - border-left: solid 4px #2ef12e; - padding-left: 0px; - } +.form-field input[type="url"] { + width: 100%; +} - .form-field textarea[disabled], - .form-field input[type="email"][disabled], - .form-field input[type="text"][disabled], - .form-field input[type="file"][disabled], - .form-field input[type="password"][disabled], - .form input[type="submit"][disabled], - .form input[type="submit"][disabled]:hover { - background: silver; - cursor: not-allowed; - border: none; - opacity: 0.65; - filter: alpha(opacity=65); - color: #333; - background-color: #e6e6e6; - } +.form-field select { + background: #fff url("../content/images/inputBackground.png") repeat-x; + border: solid 1px #ccc; + color: #7f8c7d; + font-size: 1.25em; + padding: 5px 5px 5px 10px; + vertical-align: middle; + /* This won't work in IE7, but it will only produce a minor layout quirk */ + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + height: 2.25em; +} + +.form-field select[data-edited=true] { + border-left: solid 4px #2ef12e; + padding-left: 0px; +} + +.form-field textarea[disabled], +.form-field input[type="email"][disabled], +.form-field input[type="text"][disabled], +.form-field input[type="file"][disabled], +.form-field input[type="password"][disabled], +.form input[type="submit"][disabled], +.form input[type="submit"][disabled]:hover { + background: silver; + cursor: not-allowed; + border: none; + opacity: 0.65; + filter: alpha(opacity=65); + color: #333; + background-color: #e6e6e6; +} .form-field select[data-edited=true] { border-left: solid 4px #2ef12e; @@ -1060,6 +1100,23 @@ a.btn { display: inline-block; } +.btn.btn-veryflat { + display: inline-block; + background: none !important; + border: none; + padding: 2px !important; + font: inherit; + cursor: pointer; + color: #0071bc; + outline: none; + text-decoration: none; + box-shadow: none; +} + + .btn.btn-veryflat:hover { + text-decoration: underline; + } + a:hover.btn { color: white; text-decoration: none; @@ -1083,14 +1140,17 @@ button, input[type="submit"], .btn { width: auto; } - input[type="submit"]:hover, .btn:hover { - background-color: #307A25; - background-image: -ms-linear-gradient(top, #307A25 0%, #2C9E1B 100%); - background-image: -o-linear-gradient(top, #307A25 0%, #2C9E1B 100%); - background-image: -webkit-linear-gradient(top, #307A25 0%, #2C9E1B 100%); - background-image: linear-gradient(top, #307A25 0%, #2C9E1B 100%); - border-color: #307A25; - } +input[type="submit"]:hover, .btn:hover { + background-color: #307A25; + background-image: -ms-linear-gradient(top, #307A25 0%, #2C9E1B 100%); + background-image: -o-linear-gradient(top, #307A25 0%, #2C9E1B 100%); + background-image: -webkit-linear-gradient(top, #307A25 0%, #2C9E1B 100%); + background-image: linear-gradient(top, #307A25 0%, #2C9E1B 100%); + border-color: #307A25; +} + + + .form a.cancel { font-size: 1.25em; @@ -1244,12 +1304,12 @@ ul.actionlist { text-decoration: none; } -ul.accordian { +ul.accordion { margin: 0; padding: 5px; } - ul.accordian li.accordian-item { + ul.accordion li.accordion-item { background-color: #eff7fa; margin: 0.5em 0; padding: 0.5em; @@ -1259,43 +1319,80 @@ ul.accordian { min-height: 32px; } - ul.accordian li.accordian-item .accordian-item-actions { + ul.accordion li.accordion-item .accordion-item-actions { float: right; margin-top: 3px; } - ul.accordian li.accordian-item .accordian-item-title { + ul.accordion li.accordion-item .accordion-item-title { font-size: 16pt; margin-bottom: 1em; } - ul.accordian li.accordian-item .accordian-item-subtitle { + ul.accordion li.accordion-item .accordion-item-subtitle { font-size: 12pt; margin-left: 1em; } - ul.accordian li.accordian-item .accordian-item-subtitle .owner-image { + ul.accordion li.accordion-item .accordion-item-subtitle .owner-image { margin-bottom: -8px; } - ul.accordian li.accordian-item .accordian-item-content { + ul.accordion li.accordion-item .accordion-item-content { padding-top: 1em; } - ul.accordian li.accordian-item .accordian-expand-link { + ul.accordion li.accordion-item .accordion-expand-link { display: inline-block; margin-top: 0.25em; + margin-left: 3px; } - ul.accordian li.accordian-item.accordian-item-disabled { + ul.accordion li.accordion-item.accordion-item-disabled { background-color: white; border: 1px dashed silver; box-shadow: none; } - ul.accordian li.accordian-item.accordian-item-disabled .accordian-item-title, li.accordian-item-disabled .accordian-item-subtitle { - font-style: italic; - } + ul.accordion li.accordion-item.accordion-item-disabled .accordion-item-title, li.accordion-item-disabled .accordion-item-subtitle { + font-style: italic; + } + + ul.accordion.enhanced { + margin: 0; + padding: 0px; + } + + ul.accordion.enhanced li.accordion-item { + border: none; + box-shadow: none; + margin: 0px; + padding: 0; + } + + ul.accordion.enhanced li.accordion-item .accordion-item-header { + background-color: #3a6e8b; + color: white; + padding: 0.5em; + list-style-type: none; + min-height: 32px; + } + + ul.accordion.enhanced li.accordion-item .accordion-item-content { + background-color: white; + } + + ul.accordion.enhanced .accordion-expand-button{ + font-weight: 400; + font-size: 1.1em; + padding: 5px 15px 5px 15px; + } + + ul.accordion.enhanced button{ + font-weight: 400; + font-size: 1.1em; + padding: 5px 15px 5px 15px; + } /* Script-related Classes */ .s-hidden { @@ -1394,3 +1491,211 @@ img.contributors-contributor-avatar { color: white; border: solid 1px #7F0000; } + +/* Details table*/ +.details-table +{ + table-layout: fixed; + border-spacing: 0px; +} + +.details-table tbody { + background-color: white; +} + +.details-table td{ + border-top:1px solid #ccc; + border-collapse: collapse; + padding-top: 10px; + padding-bottom: 10px; + padding-left:10px; + vertical-align: top; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.details-table tr:last-child td { + border-bottom:1px solid #ccc; +} + +.details-table b { + font-weight: 600; + color: #333; +} + +.details-table .longlist{ + list-style-position: inside; + list-style-type: none; + padding-left: 10px; +} + +.details-table .longlist li{ + display: none; +} + +.details-table .longlist ul{ + list-style-position: inside; + list-style-type: none; +} + +.details-table .hidden{ + display: none; +} + +.details-table td.actions { + text-align: right; + padding-right: 10px; +} + +.details-table span { + color: #555 +} + +.details-table span.expired { + font-weight: 600; +} + +.details-table tr:first-child td{ + border-top-width: 0px; +} + +/* Table inside table scenario */ +.details-table table td{ + border-top:0px solid #ccc; + border-collapse: collapse; + padding-top: 10px; + padding-bottom: 10px; + padding-left:10px; + vertical-align: top; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.details-table tr:last-child td { + border-bottom:0px solid #ccc; +} + + +/* divs*/ + +.scrollable-div { + overflow-x: hidden; + overflow-y: scroll; + background-color: white; + width:100%; + border: 1px solid #ccc; +} + +.disabled-div { + pointer-events: none; + opacity: 0.4; +} + +/* popup */ + +.popup { + position: relative; + display: inline-block; + padding: 5px; +} + +/* The actual popup (appears on top) */ +.popup .popupbox { + text-align: left; + position: absolute; + z-index: 1; + top: 125%; + left: 50%; + border: 1px solid #ccc; + opacity: 0.97; +} + +.popup .innertext { + padding: 18px 0px 10px 10px; + white-space: normal; +} + +/* Popup arrow */ +.popup .popupbox::after, +.popup .popupbox::before { + content: ""; + position: absolute; + bottom: 100%; + margin-left: -5px; + border-style: solid; +} + +/* this border color controlls the color of the triangle (what looks like the fill of the triangle) */ +.popup .popupbox::after { + border-color: transparent transparent white transparent; + border-width: 7px; + left: 100px; +} + +/* this border color controlls the outside, thin border */ +.popup .popupbox::before { + border-color: transparent transparent #ccc transparent; + border-width: 8px; + left: 99px; +} + +.popup table td, +.popup table th { + padding: 5px 10px 1px 0px; +} + +.popup table td + td, +.popup table th + th { + padding-left: 10px; +} + +.popup a.boxclose{ + margin: -5px 5px; + float:right; + cursor:pointer; + color: #ccc; + font-size: 15px; + display: inline-block; + line-height: 0px; + padding: 8px 0px 0px 0px; +} + +/* small popup*/ +.popup .innertext.small { + padding: 5px 5px 5px 5px; + font-size: 0.9em; +} + +.popup .popupbox.small::before { + left: 19px; + border-width: 6px; +} + +.popup .popupbox.small::after { + left: 20px; + border-width: 5px; +} + +.popup .popupbox.small { + border-radius: 3px; +} + +/* warning sign */ + +.warningsign i.icon-stack-base{ + color: #ffcc00; + font-size: 1.3em; +} + +.warningsign i.icon-exclamation{ + color: black; + font-size: 0.9em; +} + +.slimbutton { + font-weight: 400; + font-size: 1.1em; + padding: 5px 15px 5px 15px; +} diff --git a/src/NuGetGallery/Controllers/ApiController.cs b/src/NuGetGallery/Controllers/ApiController.cs index e99e3df8f7..1862446d1c 100644 --- a/src/NuGetGallery/Controllers/ApiController.cs +++ b/src/NuGetGallery/Controllers/ApiController.cs @@ -9,6 +9,7 @@ using System.IO.Compression; using System.Linq; using System.Net; +using System.Security.Claims; using System.Threading.Tasks; using System.Web.Mvc; using System.Web.UI; @@ -16,7 +17,12 @@ using NuGet.Frameworks; using NuGet.Packaging; using NuGet.Versioning; +using NuGetGallery.Auditing; +using NuGetGallery.Auditing.AuditedEntities; +using NuGetGallery.Authentication; +using NuGetGallery.Configuration; using NuGetGallery.Filters; +using NuGetGallery.Infrastructure.Authentication; using NuGetGallery.Packaging; using PackageIdValidator = NuGetGallery.Packaging.PackageIdValidator; @@ -37,9 +43,15 @@ public partial class ApiController public IAutomaticallyCuratePackageCommand AutoCuratePackage { get; set; } public IStatusService StatusService { get; set; } public IMessageService MessageService { get; set; } + public IAuditingService AuditingService { get; set; } + public IGalleryConfigurationService ConfigurationService { get; set; } + public ITelemetryService TelemetryService { get; set; } + public AuthenticationService AuthenticationService { get; set; } + public ICredentialBuilder CredentialBuilder { get; set; } protected ApiController() { + AuditingService = NuGetGallery.Auditing.AuditingService.None; } public ApiController( @@ -53,7 +65,12 @@ public ApiController( ISearchService searchService, IAutomaticallyCuratePackageCommand autoCuratePackage, IStatusService statusService, - IMessageService messageService) + IMessageService messageService, + IAuditingService auditingService, + IGalleryConfigurationService configurationService, + ITelemetryService telemetryService, + AuthenticationService authenticationService, + ICredentialBuilder credentialBuilder) { EntitiesContext = entitiesContext; PackageService = packageService; @@ -61,12 +78,17 @@ public ApiController( UserService = userService; NugetExeDownloaderService = nugetExeDownloaderService; ContentService = contentService; - StatisticsService = null; IndexingService = indexingService; SearchService = searchService; AutoCuratePackage = autoCuratePackage; StatusService = statusService; MessageService = messageService; + AuditingService = auditingService; + ConfigurationService = configurationService; + TelemetryService = telemetryService; + AuthenticationService = authenticationService; + CredentialBuilder = credentialBuilder; + StatisticsService = null; } public ApiController( @@ -81,8 +103,13 @@ public ApiController( IAutomaticallyCuratePackageCommand autoCuratePackage, IStatusService statusService, IStatisticsService statisticsService, - IMessageService messageService) - : this(entitiesContext, packageService, packageFileService, userService, nugetExeDownloaderService, contentService, indexingService, searchService, autoCuratePackage, statusService, messageService) + IMessageService messageService, + IAuditingService auditingService, + IGalleryConfigurationService configurationService, + ITelemetryService telemetryService, + AuthenticationService authenticationService, + ICredentialBuilder credentialBuilder) + : this(entitiesContext, packageService, packageFileService, userService, nugetExeDownloaderService, contentService, indexingService, searchService, autoCuratePackage, statusService, messageService, auditingService, configurationService, telemetryService, authenticationService, credentialBuilder) { StatisticsService = statisticsService; } @@ -107,6 +134,7 @@ public virtual async Task GetPackage(string id, string version) { return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, "The package version is not a valid semantic version"); } + // Normalize the version version = NuGetVersionNormalizer.Normalize(version); } @@ -138,7 +166,11 @@ public virtual async Task GetPackage(string id, string version) // Database was unavailable and we don't have a version, return a 503 return new HttpStatusCodeWithBodyResult(HttpStatusCode.ServiceUnavailable, Strings.DatabaseUnavailable_TrySpecificVersion); } + } + if (ConfigurationService.Features.TrackPackageDownloadCountInLocalDatabase) + { + await PackageService.IncrementDownloadCountAsync(id, version); } return await PackageFileService.CreateDownloadPackageActionResultAsync( @@ -166,35 +198,101 @@ public async virtual Task Status() return await StatusService.GetStatus(); } + private Credential GetCurrentCredential(User user) + { + var identity = User.Identity as ClaimsIdentity; + var apiKey = identity.GetClaimOrDefault(NuGetClaims.ApiKey); + + return user.Credentials.FirstOrDefault(c => c.Value == apiKey); + } + + [HttpPost] + [RequireSsl] + [ApiAuthorize] + [ApiScopeRequired(NuGetScopes.PackagePush, NuGetScopes.PackagePushVersion)] + [ActionName("CreatePackageVerificationKey")] + public async virtual Task CreatePackageVerificationKeyAsync(string id, string version) + { + // For backwards compatibility, we must preserve existing behavior where the client always pushes + // symbols and the VerifyPackageKey callback returns the appropriate response. For this reason, we + // always create a temp key scoped to the unverified package ID here and defer package and owner + // validation until the VerifyPackageKey call. + var credential = CredentialBuilder.CreatePackageVerificationApiKey(id); + + var user = GetCurrentUser(); + await AuthenticationService.AddCredential(user, credential); + + TelemetryService.TrackCreatePackageVerificationKeyEvent(id, version, user, User.Identity); + + return Json(new + { + Key = credential.Value, + Expires = credential.Expires.Value.ToString("O") + }); + } + [HttpGet] [RequireSsl] [ApiAuthorize] + [ApiScopeRequired(NuGetScopes.PackageVerify, NuGetScopes.PackagePush, NuGetScopes.PackagePushVersion)] [ActionName("VerifyPackageKey")] - public virtual ActionResult VerifyPackageKey(string id, string version) + public async virtual Task VerifyPackageKeyAsync(string id, string version) + { + var user = GetCurrentUser(); + var credential = GetCurrentCredential(user); + + var result = VerifyPackageKeyInternal(user, credential, id, version); + + // Expire and delete verification key after first use to avoid growing the database tables. + if (CredentialTypes.IsPackageVerificationApiKey(credential.Type)) + { + await AuthenticationService.RemoveCredential(user, credential); + } + + TelemetryService.TrackVerifyPackageKeyEvent(id, version, user, User.Identity, result?.StatusCode ?? 200); + + return (ActionResult)result ?? new EmptyResult(); + } + + private HttpStatusCodeWithBodyResult VerifyPackageKeyInternal(User user, Credential credential, string id, string version) { - if (!String.IsNullOrEmpty(id)) + // Verify that the user has permission to push for the specific Id \ version combination. + var package = PackageService.FindPackageByIdAndVersion(id, version); + if (package == null) { - // If the partialId is present, then verify that the user has permission to push for the specific Id \ version combination. - var package = PackageService.FindPackageByIdAndVersion(id, version); - if (package == null) + return new HttpStatusCodeWithBodyResult( + HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version)); + } + + if (!package.IsOwner(user)) + { + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); + } + + if (CredentialTypes.IsPackageVerificationApiKey(credential.Type)) + { + // Secure path: verify that verification key matches package scope. + if (!ApiKeyScopeAllows(id, NuGetScopes.PackageVerify)) { - return new HttpStatusCodeWithBodyResult( - HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version)); + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); } - - var user = GetCurrentUser(); - if (!package.IsOwner(user)) + } + else + { + // Insecure path: verify that API key is legacy or matches package scope. + if (!ApiKeyScopeAllows(id, NuGetScopes.PackagePush, NuGetScopes.PackagePushVersion)) { return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); } } - return new EmptyResult(); + return null; } [HttpPut] [RequireSsl] [ApiAuthorize] + [ApiScopeRequired(NuGetScopes.PackagePush, NuGetScopes.PackagePushVersion)] [ActionName("PushPackageApi")] public virtual Task CreatePackagePut() { @@ -204,6 +302,7 @@ public virtual Task CreatePackagePut() [HttpPost] [RequireSsl] [ApiAuthorize] + [ApiScopeRequired(NuGetScopes.PackagePush, NuGetScopes.PackagePushVersion)] [ActionName("PushPackageApi")] public virtual Task CreatePackagePost() { @@ -237,17 +336,32 @@ private async Task CreatePackageInternal() using (var packageToPush = new PackageArchiveReader(packageStream, leaveStreamOpen: false)) { - NuspecReader nuspec = null; try { - nuspec = packageToPush.GetNuspecReader(); + PackageService.EnsureValid(packageToPush); } catch (Exception ex) { + ex.Log(); + + var message = Strings.FailedToReadUploadFile; + if (ex is InvalidPackageException || ex is InvalidDataException || ex is EntityException) + { + message = ex.Message; + } + + return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, message); + } + + NuspecReader nuspec; + var errors = ManifestValidator.Validate(packageToPush.GetNuspec(), out nuspec).ToArray(); + if (errors.Length > 0) + { + var errorsString = string.Join("', '", errors.Select(error => error.ErrorMessage)); return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, string.Format( CultureInfo.CurrentCulture, - Strings.UploadPackage_InvalidNuspec, - ex.Message)); + errors.Length > 1 ? Strings.UploadPackage_InvalidNuspecMultiple : Strings.UploadPackage_InvalidNuspec, + errorsString)); } if (nuspec.GetMinClientVersion() > Constants.MaxSupportedMinClientVersion) @@ -260,19 +374,50 @@ private async Task CreatePackageInternal() // Ensure that the user can push packages for this partialId. var packageRegistration = PackageService.FindPackageRegistrationById(nuspec.GetId()); - if (packageRegistration != null) + if (packageRegistration == null) { + // Check if API key allows pushing a new package id + if (!ApiKeyScopeAllows( + subject: nuspec.GetId(), + requestedActions: NuGetScopes.PackagePush)) + { + // User cannot push a new package ID as the API key scope does not allow it + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Unauthorized, Strings.ApiKeyNotAuthorized); + } + } + else + { + // Is the user allowed to push this Id? if (!packageRegistration.IsOwner(user)) { - return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, - Strings.ApiKeyNotAuthorized); + // Audit that a non-owner tried to push the package + await AuditingService.SaveAuditRecordAsync( + new FailedAuthenticatedOperationAuditRecord( + user.Username, + AuditedAuthenticatedOperationAction.PackagePushAttemptByNonOwner, + attemptedPackage: new AuditedPackageIdentifier( + nuspec.GetId(), nuspec.GetVersion().ToNormalizedStringSafe()))); + + // User cannot push a package to an ID owned by another user. + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Conflict, + string.Format(CultureInfo.CurrentCulture, Strings.PackageIdNotAvailable, + nuspec.GetId())); + } + + // Check if API key allows pushing the current package id + if (!ApiKeyScopeAllows( + packageRegistration.Id, + NuGetScopes.PackagePushVersion, NuGetScopes.PackagePush)) + { + // User cannot push a package as the API key scope does not allow it + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Unauthorized, Strings.ApiKeyNotAuthorized); } // Check if a particular Id-Version combination already exists. We eventually need to remove this check. string normalizedVersion = nuspec.GetVersion().ToNormalizedString(); bool packageExists = packageRegistration.Packages.Any( - p => String.Equals( + p => string.Equals( p.NormalizedVersion, normalizedVersion, StringComparison.OrdinalIgnoreCase)); @@ -281,7 +426,7 @@ private async Task CreatePackageInternal() { return new HttpStatusCodeWithBodyResult( HttpStatusCode.Conflict, - String.Format(CultureInfo.CurrentCulture, Strings.PackageExistsAndCannotBeModified, + string.Format(CultureInfo.CurrentCulture, Strings.PackageExistsAndCannotBeModified, nuspec.GetId(), nuspec.GetVersion().ToNormalizedStringSafe())); } } @@ -290,28 +435,58 @@ private async Task CreatePackageInternal() { HashAlgorithm = Constants.Sha512HashAlgorithmId, Hash = CryptographyService.GenerateHash(packageStream.AsSeekableStream()), - Size = packageStream.Length, + Size = packageStream.Length }; - var package = - await - PackageService.CreatePackageAsync(packageToPush, packageStreamMetadata, user, - commitChanges: false); + var package = await PackageService.CreatePackageAsync( + packageToPush, + packageStreamMetadata, + user, + commitChanges: false); + await AutoCuratePackage.ExecuteAsync(package, packageToPush, commitChanges: false); - await EntitiesContext.SaveChangesAsync(); using (Stream uploadStream = packageStream) { uploadStream.Position = 0; - await PackageFileService.SavePackageFileAsync(package, uploadStream.AsSeekableStream()); - IndexingService.UpdatePackage(package); + + try + { + await PackageFileService.SavePackageFileAsync(package, uploadStream.AsSeekableStream()); + } + catch (InvalidOperationException ex) + { + ex.Log(); + + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Conflict, Strings.UploadPackage_IdVersionConflict); + } } + try + { + await EntitiesContext.SaveChangesAsync(); + } + catch + { + // If saving to the DB fails for any reason, we need to delete the package we just saved. + await PackageFileService.DeletePackageFileAsync(nuspec.GetId(), nuspec.GetVersion().ToNormalizedString()); + throw; + } + + IndexingService.UpdatePackage(package); + + // Write an audit record + await AuditingService.SaveAuditRecordAsync( + new PackageAuditRecord(package, AuditedPackageAction.Create, PackageCreatedVia.Api)); + + // Notify user of push MessageService.SendPackageAddedNotice(package, Url.Action("DisplayPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme), Url.Action("ReportMyPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme), Url.Action("Account", "Users", routeValues: null, protocol: Request.Url.Scheme)); + TelemetryService.TrackPackagePushEvent(package, user, User.Identity); + return new HttpStatusCodeResult(HttpStatusCode.Created); } } @@ -334,6 +509,13 @@ private async Task CreatePackageInternal() } } + private bool ApiKeyScopeAllows(string subject, params string[] requestedActions) + { + return User.Identity.HasScopeThatAllowsActionForSubject( + subject: subject, + requestedActions: requestedActions); + } + private static ActionResult BadRequestForExceptionMessage(Exception ex) { return new HttpStatusCodeWithBodyResult( @@ -344,6 +526,7 @@ private static ActionResult BadRequestForExceptionMessage(Exception ex) [HttpDelete] [RequireSsl] [ApiAuthorize] + [ApiScopeRequired(NuGetScopes.PackageUnlist)] [ActionName("DeletePackageApi")] public virtual async Task DeletePackage(string id, string version) { @@ -360,6 +543,14 @@ public virtual async Task DeletePackage(string id, string version) return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); } + // Check if API key allows listing/unlisting the current package id + if (!ApiKeyScopeAllows( + subject: id, + requestedActions: NuGetScopes.PackageUnlist)) + { + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); + } + await PackageService.MarkPackageUnlistedAsync(package); IndexingService.UpdatePackage(package); return new EmptyResult(); @@ -368,6 +559,7 @@ public virtual async Task DeletePackage(string id, string version) [HttpPost] [RequireSsl] [ApiAuthorize] + [ApiScopeRequired(NuGetScopes.PackageUnlist)] [ActionName("PublishPackageApi")] public virtual async Task PublishPackage(string id, string version) { @@ -384,6 +576,14 @@ public virtual async Task PublishPackage(string id, string version return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, String.Format(CultureInfo.CurrentCulture, Strings.ApiKeyNotAuthorized, "publish")); } + // Check if API key allows listing/unlisting the current package id + if (!ApiKeyScopeAllows( + subject: id, + requestedActions: NuGetScopes.PackageUnlist)) + { + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); + } + await PackageService.MarkPackageListedAsync(package); IndexingService.UpdatePackage(package); return new EmptyResult(); @@ -434,7 +634,7 @@ protected internal virtual Stream ReadPackageFromRequest() [ActionName("PackageIDs")] public virtual async Task GetPackageIds(string partialId, bool? includePrerelease) { - var query = GetService(); + var query = GetService(); return new JsonResult { Data = (await query.Execute(partialId, includePrerelease)).ToArray(), @@ -446,7 +646,7 @@ public virtual async Task GetPackageIds(string partialId, bool? in [ActionName("PackageVersions")] public virtual async Task GetPackageVersions(string id, bool? includePrerelease) { - var query = GetService(); + var query = GetService(); return new JsonResult { Data = (await query.Execute(id, includePrerelease)).ToArray(), diff --git a/src/NuGetGallery/Controllers/AppController.cs b/src/NuGetGallery/Controllers/AppController.cs index 275af7ad5c..aa49125e5d 100644 --- a/src/NuGetGallery/Controllers/AppController.cs +++ b/src/NuGetGallery/Controllers/AppController.cs @@ -14,19 +14,20 @@ public abstract partial class AppController { private IOwinContext _overrideContext; - public IOwinContext OwinContext - { - get { return _overrideContext ?? HttpContext.GetOwinContext(); } - set { _overrideContext = value; } - } + public IOwinContext OwinContext => _overrideContext ?? HttpContext.GetOwinContext(); - public NuGetContext NuGetContext { get; private set; } + public NuGetContext NuGetContext { get; } public new ClaimsPrincipal User { get { return base.User as ClaimsPrincipal; } } + public void SetOwinContextOverride(IOwinContext owinContext) + { + _overrideContext = owinContext; + } + protected AppController() { NuGetContext = new NuGetContext(this); diff --git a/src/NuGetGallery/Controllers/AuthenticationController.cs b/src/NuGetGallery/Controllers/AuthenticationController.cs index d969c0bfdf..b761ea52d6 100644 --- a/src/NuGetGallery/Controllers/AuthenticationController.cs +++ b/src/NuGetGallery/Controllers/AuthenticationController.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Mail; using System.Security.Claims; @@ -10,34 +11,57 @@ using System.Web.Mvc; using NuGetGallery.Authentication; using NuGetGallery.Filters; +using NuGetGallery.Infrastructure.Authentication; namespace NuGetGallery { public partial class AuthenticationController : AppController { - // For sub-classes to initialize services themselves - protected AuthenticationController() - { - } + private readonly AuthenticationService _authService; + + private readonly IUserService _userService; - public AuthenticationService AuthService { get; protected set; } - public IUserService UserService { get; protected set; } - public IMessageService MessageService { get; protected set; } + private readonly IMessageService _messageService; + + private readonly ICredentialBuilder _credentialBuilder; public AuthenticationController( AuthenticationService authService, IUserService userService, - IMessageService messageService) + IMessageService messageService, + ICredentialBuilder credentialBuilder) { - AuthService = authService; - UserService = userService; - MessageService = messageService; + if (authService == null) + { + throw new ArgumentNullException(nameof(authService)); + } + + if (userService == null) + { + throw new ArgumentNullException(nameof(userService)); + } + + if (messageService == null) + { + throw new ArgumentNullException(nameof(messageService)); + } + + if (credentialBuilder == null) + { + throw new ArgumentNullException(nameof(credentialBuilder)); + } + + _authService = authService; + _userService = userService; + _messageService = messageService; + _credentialBuilder = credentialBuilder; } /// /// Sign In\Register view /// + [HttpGet] [RequireSsl] public virtual ActionResult LogOn(string returnUrl) { @@ -72,16 +96,33 @@ public virtual async Task SignIn(LogOnViewModel model, string retu return LogOnView(model); } - var user = await AuthService.Authenticate(model.SignIn.UserNameOrEmail, model.SignIn.Password); + var authenticationResult = await _authService.Authenticate(model.SignIn.UserNameOrEmail, model.SignIn.Password); + - if (user == null) + if (authenticationResult.Result != PasswordAuthenticationResult.AuthenticationResult.Success) { - ModelState.AddModelError( - "SignIn", - Strings.UsernameAndPasswordNotFound); + string modelErrorMessage = string.Empty; + if (authenticationResult.Result == PasswordAuthenticationResult.AuthenticationResult.BadCredentials) + { + modelErrorMessage = Strings.UsernameAndPasswordNotFound; + } + else if (authenticationResult.Result == PasswordAuthenticationResult.AuthenticationResult.AccountLocked) + { + string timeRemaining = + authenticationResult.LockTimeRemainingMinutes == 1 + ? Strings.AMinute + : string.Format(CultureInfo.CurrentCulture, Strings.Minutes, + authenticationResult.LockTimeRemainingMinutes); + + modelErrorMessage = string.Format(CultureInfo.CurrentCulture, Strings.UserAccountLocked, timeRemaining); + } + + ModelState.AddModelError("SignIn", modelErrorMessage); return LogOnView(model); } + + var user = authenticationResult.AuthenticatedUser; if (linkingAccount) { @@ -103,7 +144,7 @@ public virtual async Task SignIn(LogOnViewModel model, string retu } // Create session - AuthService.CreateSession(OwinContext, user.User); + await _authService.CreateSessionAsync(OwinContext, user); return SafeRedirect(returnUrl); } @@ -120,7 +161,7 @@ internal bool ShouldChallengeEnforcedProvider(string enforcedProviders, Authenti && !providers.Any(p => string.Equals(CredentialTypes.ExternalPrefix + p, authenticatedUser.CredentialUsed.Type, StringComparison.OrdinalIgnoreCase))) { // Challenge authentication using the first required authentication provider - challenge = AuthService.Challenge( + challenge = _authService.Challenge( providers.First(), Url.Action("LinkExternalAccount", "Authentication", new { ReturnUrl = returnUrl })); @@ -142,7 +183,7 @@ public virtual ActionResult RegisterLegacy(string returnUrl) [HttpPost] [RequireSsl] [ValidateAntiForgeryToken] - public async virtual Task Register(LogOnViewModel model, string returnUrl, bool linkingAccount) + public virtual async Task Register(LogOnViewModel model, string returnUrl, bool linkingAccount) { // I think it should be obvious why we don't want the current URL to be the return URL here ;) ViewData[Constants.ReturnUrlViewDataKey] = returnUrl; @@ -168,23 +209,23 @@ public async virtual Task Register(LogOnViewModel model, string re { if (linkingAccount) { - var result = await AuthService.ReadExternalLoginCredential(OwinContext); + var result = await _authService.ReadExternalLoginCredential(OwinContext); if (result.ExternalIdentity == null) { return ExternalLinkExpired(); } - user = await AuthService.Register( + user = await _authService.Register( model.Register.Username, model.Register.EmailAddress, result.Credential); } else { - user = await AuthService.Register( + user = await _authService.Register( model.Register.Username, model.Register.EmailAddress, - CredentialBuilder.CreatePbkdf2Password(model.Register.Password)); + _credentialBuilder.CreatePasswordCredential(model.Register.Password)); } } catch (EntityException ex) @@ -194,9 +235,9 @@ public async virtual Task Register(LogOnViewModel model, string re } // Send a new account email - if (NuGetContext.Config.Current.ConfirmEmailAddresses && !String.IsNullOrEmpty(user.User.UnconfirmedEmailAddress)) + if (NuGetContext.Config.Current.ConfirmEmailAddresses && !string.IsNullOrEmpty(user.User.UnconfirmedEmailAddress)) { - MessageService.SendNewAccountEmail( + _messageService.SendNewAccountEmail( new MailAddress(user.User.UnconfirmedEmailAddress, user.User.Username), Url.ConfirmationUrl( "Confirm", @@ -215,10 +256,11 @@ public async virtual Task Register(LogOnViewModel model, string re } // Create session - AuthService.CreateSession(OwinContext, user.User); + await _authService.CreateSessionAsync(OwinContext, user); return RedirectFromRegister(returnUrl); } + [HttpGet] public virtual ActionResult LogOff(string returnUrl) { OwinContext.Authentication.SignOut(); @@ -232,17 +274,33 @@ public virtual ActionResult LogOff(string returnUrl) return SafeRedirect(returnUrl); } - public virtual ActionResult Authenticate(string returnUrl, string provider) + [ActionName("Authenticate")] + [HttpGet] + public virtual ActionResult AuthenticateGet(string returnUrl, string provider) + { + return ChallengeAuthentication(returnUrl, provider); + } + + [ActionName("Authenticate")] + [HttpPost] + [ValidateAntiForgeryToken] + public virtual ActionResult AuthenticatePost(string returnUrl, string provider) + { + return ChallengeAuthentication(returnUrl, provider); + } + + [NonAction] + public ActionResult ChallengeAuthentication(string returnUrl, string provider) { - return AuthService.Challenge( + return _authService.Challenge( provider, Url.Action("LinkExternalAccount", "Authentication", new { ReturnUrl = returnUrl })); } - public async virtual Task LinkExternalAccount(string returnUrl) + public virtual async Task LinkExternalAccount(string returnUrl) { // Extract the external login info - var result = await AuthService.AuthenticateExternalLogin(OwinContext); + var result = await _authService.AuthenticateExternalLogin(OwinContext); if (result.ExternalIdentity == null) { // User got here without an external login cookie (or an expired one) @@ -262,7 +320,7 @@ public async virtual Task LinkExternalAccount(string returnUrl) } // Create session - AuthService.CreateSession(OwinContext, result.Authentication.User); + await _authService.CreateSessionAsync(OwinContext, result.Authentication); return SafeRedirect(returnUrl); } else @@ -278,7 +336,7 @@ public async virtual Task LinkExternalAccount(string returnUrl) User existingUser = null; if (!string.IsNullOrEmpty(email)) { - existingUser = UserService.FindByEmailAddress(email); + existingUser = _userService.FindByEmailAddress(email); } var external = new AssociateExternalAccountViewModel() @@ -320,7 +378,7 @@ private ActionResult RedirectFromRegister(string returnUrl) private async Task AssociateCredential(AuthenticatedUser user) { - var result = await AuthService.ReadExternalLoginCredential(OwinContext); + var result = await _authService.ReadExternalLoginCredential(OwinContext); if (result.ExternalIdentity == null) { // User got here without an external login cookie (or an expired one) @@ -328,17 +386,17 @@ private async Task AssociateCredential(AuthenticatedUser user return null; } - await AuthService.AddCredential(user.User, result.Credential); + await _authService.AddCredential(user.User, result.Credential); // Notify the user of the change - MessageService.SendCredentialAddedNotice(user.User, result.Credential); + _messageService.SendCredentialAddedNotice(user.User, result.Credential); return new AuthenticatedUser(user.User, result.Credential); } private List GetProviders() { - return (from p in AuthService.Authenticators.Values + return (from p in _authService.Authenticators.Values where p.BaseConfig.Enabled let ui = p.GetUI() where ui != null && ui.ShowOnLoginPage diff --git a/src/NuGetGallery/Controllers/ErrorsController.cs b/src/NuGetGallery/Controllers/ErrorsController.cs index 7141747e28..d06436e105 100644 --- a/src/NuGetGallery/Controllers/ErrorsController.cs +++ b/src/NuGetGallery/Controllers/ErrorsController.cs @@ -7,11 +7,13 @@ namespace NuGetGallery { public partial class ErrorsController : AppController { + [AcceptVerbs(HttpVerbs.Get | HttpVerbs.Head)] public virtual ActionResult NotFound() { return View(); } + [HttpGet] public virtual ActionResult InternalError() { return View(); diff --git a/src/NuGetGallery/Controllers/JsonApiController.cs b/src/NuGetGallery/Controllers/JsonApiController.cs index 79d7b39918..2767cdcee3 100644 --- a/src/NuGetGallery/Controllers/JsonApiController.cs +++ b/src/NuGetGallery/Controllers/JsonApiController.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; +using System.Web; using System.Web.Mvc; namespace NuGetGallery @@ -59,8 +60,10 @@ public virtual ActionResult GetPackageOwners(string id, string version) } [HttpPost] - public async Task AddPackageOwner(string id, string username) + public async Task AddPackageOwner(string id, string username, string message) { + message = HttpUtility.HtmlEncode(message); + var package = _packageService.FindPackageRegistrationById(id); if (package == null) { @@ -89,7 +92,7 @@ public async Task AddPackageOwner(string id, string username) user.Username, ownerRequest.ConfirmationCode, new { id = package.Id }); - _messageService.SendPackageOwnerRequest(currentUser, user, package, confirmationUrl); + _messageService.SendPackageOwnerRequest(currentUser, user, package, confirmationUrl, message); return Json(new { success = true, name = user.Username, pending = true }); } diff --git a/src/NuGetGallery/Controllers/NuGetContext.cs b/src/NuGetGallery/Controllers/NuGetContext.cs index 3f1fa922b5..aa6f5c3923 100644 --- a/src/NuGetGallery/Controllers/NuGetContext.cs +++ b/src/NuGetGallery/Controllers/NuGetContext.cs @@ -13,12 +13,12 @@ public class NuGetContext public NuGetContext(AppController ctrl) { - Config = DependencyResolver.Current.GetService(); + Config = DependencyResolver.Current.GetService(); _currentUser = new Lazy(() => ctrl.OwinContext.GetCurrentUser()); } - public ConfigurationService Config { get; internal set; } + public IGalleryConfigurationService Config { get; internal set; } public User CurrentUser { get { return _currentUser.Value; } } } } \ No newline at end of file diff --git a/src/NuGetGallery/Controllers/ODataV1FeedController.cs b/src/NuGetGallery/Controllers/ODataV1FeedController.cs index a5d2365f3f..ecfd41cfc7 100644 --- a/src/NuGetGallery/Controllers/ODataV1FeedController.cs +++ b/src/NuGetGallery/Controllers/ODataV1FeedController.cs @@ -8,8 +8,10 @@ using System.Web.Http; using System.Web.Http.OData; using System.Web.Http.OData.Query; +using System.Web.Http.Results; using NuGetGallery.Configuration; using NuGetGallery.OData; +using NuGetGallery.OData.QueryFilter; using NuGetGallery.WebApi; using WebApi.OutputCache.V2; @@ -22,12 +24,12 @@ public class ODataV1FeedController private const int MaxPageSize = SearchAdaptor.MaxPageSize; private readonly IEntityRepository _packagesRepository; - private readonly ConfigurationService _configurationService; + private readonly IGalleryConfigurationService _configurationService; private readonly ISearchService _searchService; public ODataV1FeedController( IEntityRepository packagesRepository, - ConfigurationService configurationService, + IGalleryConfigurationService configurationService, ISearchService searchService) : base(configurationService) { @@ -42,6 +44,12 @@ public ODataV1FeedController( [CacheOutput(NoCache = true)] public IHttpActionResult Get(ODataQueryOptions options) { + if (!ODataQueryVerifier.AreODataOptionsAllowed(options, ODataQueryVerifier.V1Packages, + _configurationService.Current.IsODataFilterEnabled, nameof(Get))) + { + return BadRequest(ODataQueryVerifier.GetValidationFailedMessage(options)); + } + var queryable = _packagesRepository.GetAll() .Where(p => !p.IsPrerelease && !p.Deleted) .WithoutVersionSort() @@ -170,7 +178,7 @@ public async Task Search( } } - // Peform actual search + // Perform actual search var packages = _packagesRepository.GetAll() .Include(p => p.PackageRegistration) .Include(p => p.PackageRegistration.Owners) @@ -198,6 +206,12 @@ public async Task Search( SearchAdaptor.GetNextLink(Request.RequestUri, resultCount, new { searchTerm, targetFramework }, o, s)); } + if (!ODataQueryVerifier.AreODataOptionsAllowed(options, ODataQueryVerifier.V1Search, + _configurationService.Current.IsODataFilterEnabled, nameof(Search))) + { + return BadRequest(ODataQueryVerifier.GetValidationFailedMessage(options)); + } + // If not, just let OData handle things var queryable = query.ToV1FeedPackageQuery(GetSiteRoot()); return QueryResult(options, queryable, MaxPageSize); diff --git a/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs b/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs index c26bc5ff52..27bb2ded96 100644 --- a/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs +++ b/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs @@ -23,13 +23,13 @@ public class ODataV2CuratedFeedController private const int MaxPageSize = 40; private readonly IEntitiesContext _entities; - private readonly ConfigurationService _configurationService; + private readonly IGalleryConfigurationService _configurationService; private readonly ISearchService _searchService; private readonly ICuratedFeedService _curatedFeedService; public ODataV2CuratedFeedController( IEntitiesContext entities, - ConfigurationService configurationService, + IGalleryConfigurationService configurationService, ISearchService searchService, ICuratedFeedService curatedFeedService) : base(configurationService) @@ -198,7 +198,7 @@ public async Task Search( } } - // Peform actual search + // Perform actual search var curatedFeed = _curatedFeedService.GetFeedByName(curatedFeedName, includePackages: false); var packages = _curatedFeedService.GetPackages(curatedFeedName) .OrderBy(p => p.PackageRegistration.Id).ThenBy(p => p.Version); diff --git a/src/NuGetGallery/Controllers/ODataV2FeedController.cs b/src/NuGetGallery/Controllers/ODataV2FeedController.cs index cf6c8e61e6..df2a09d84c 100644 --- a/src/NuGetGallery/Controllers/ODataV2FeedController.cs +++ b/src/NuGetGallery/Controllers/ODataV2FeedController.cs @@ -9,11 +9,13 @@ using System.Web.Http; using System.Web.Http.OData; using System.Web.Http.OData.Query; +using System.Web.Http.Results; using NuGet.Frameworks; using NuGet.Versioning; using NuGetGallery.Configuration; using NuGetGallery.Infrastructure.Lucene; using NuGetGallery.OData; +using NuGetGallery.OData.QueryFilter; using NuGetGallery.OData.QueryInterceptors; using NuGetGallery.WebApi; using QueryInterceptor; @@ -28,12 +30,12 @@ public class ODataV2FeedController private const int MaxPageSize = SearchAdaptor.MaxPageSize; private readonly IEntityRepository _packagesRepository; - private readonly ConfigurationService _configurationService; + private readonly IGalleryConfigurationService _configurationService; private readonly ISearchService _searchService; public ODataV2FeedController( IEntityRepository packagesRepository, - ConfigurationService configurationService, + IGalleryConfigurationService configurationService, ISearchService searchService) : base(configurationService) { @@ -59,7 +61,7 @@ public async Task Get(ODataQueryOptions option try { HijackableQueryParameters hijackableQueryParameters = null; - if (SearchHijacker.IsHijackable(options, out hijackableQueryParameters) && _searchService is ExternalSearchService) + if (_searchService is ExternalSearchService && SearchHijacker.IsHijackable(options, out hijackableQueryParameters)) { var searchAdaptorResult = await SearchAdaptor.FindByIdAndVersionCore( _searchService, GetTraditionalHttpContext().Request, packages, @@ -89,6 +91,13 @@ public async Task Get(ODataQueryOptions option QuietLog.LogHandledException(ex); } + //Reject only when try to reach database. + if (!ODataQueryVerifier.AreODataOptionsAllowed(options, ODataQueryVerifier.V2Packages, + _configurationService.Current.IsODataFilterEnabled, nameof(Get))) + { + return BadRequest(ODataQueryVerifier.GetValidationFailedMessage(options)); + } + var queryable = packages.ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); return QueryResult(options, queryable, MaxPageSize); } @@ -222,7 +231,7 @@ public async Task Search( } } - // Peform actual search + // Perform actual search var packages = _packagesRepository.GetAll() .Include(p => p.PackageRegistration) .Include(p => p.PackageRegistration.Owners) @@ -257,6 +266,12 @@ public async Task Search( return null; }); } + //Reject only when try to reach database. + if (!ODataQueryVerifier.AreODataOptionsAllowed(options, ODataQueryVerifier.V2Search, + _configurationService.Current.IsODataFilterEnabled, nameof(Search))) + { + return BadRequest(ODataQueryVerifier.GetValidationFailedMessage(options)); + } // If not, just let OData handle things var queryable = query.ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); @@ -293,10 +308,16 @@ public IHttpActionResult GetUpdates( return Ok(Enumerable.Empty().AsQueryable()); } + if (!ODataQueryVerifier.AreODataOptionsAllowed(options, ODataQueryVerifier.V2GetUpdates, + _configurationService.Current.IsODataFilterEnabled, nameof(GetUpdates))) + { + return BadRequest(ODataQueryVerifier.GetValidationFailedMessage(options)); + } + // Workaround https://github.com/NuGet/NuGetGallery/issues/674 for NuGet 2.1 client. // Can probably eventually be retired (when nobody uses 2.1 anymore...) // Note - it was URI un-escaping converting + to ' ', undoing that is actually a pretty conservative substitution because - // space characters are never acepted as valid by VersionUtility.ParseFrameworkName. + // space characters are never accepted as valid by VersionUtility.ParseFrameworkName. if (!string.IsNullOrEmpty(targetFrameworks)) { targetFrameworks = targetFrameworks.Replace(' ', '+'); diff --git a/src/NuGetGallery/Controllers/PackagesController.cs b/src/NuGetGallery/Controllers/PackagesController.cs index 73934bc540..9d96e9b9db 100644 --- a/src/NuGetGallery/Controllers/PackagesController.cs +++ b/src/NuGetGallery/Controllers/PackagesController.cs @@ -20,6 +20,7 @@ using NuGet.Versioning; using NuGetGallery.Areas.Admin; using NuGetGallery.AsyncFileUpload; +using NuGetGallery.Auditing; using NuGetGallery.Configuration; using NuGetGallery.Filters; using NuGetGallery.Helpers; @@ -50,6 +51,8 @@ public partial class PackagesController private readonly EditPackageService _editPackageService; private readonly IPackageDeleteService _packageDeleteService; private readonly ISupportRequestService _supportRequestService; + private readonly IAuditingService _auditingService; + private readonly ITelemetryService _telemetryService; public PackagesController( IPackageService packageService, @@ -64,7 +67,9 @@ public PackagesController( ICacheService cacheService, EditPackageService editPackageService, IPackageDeleteService packageDeleteService, - ISupportRequestService supportRequestService) + ISupportRequestService supportRequestService, + IAuditingService auditingService, + ITelemetryService telemetryService) { _packageService = packageService; _uploadFileService = uploadFileService; @@ -79,8 +84,11 @@ public PackagesController( _editPackageService = editPackageService; _packageDeleteService = packageDeleteService; _supportRequestService = supportRequestService; + _auditingService = auditingService; + _telemetryService = telemetryService; } + [HttpGet] [Authorize] [OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")] public virtual ActionResult UploadPackageProgress() @@ -141,6 +149,8 @@ public virtual async Task UndoPendingEdits(string id, string versi } else if (numOK > 0) { + await _auditingService.SaveAuditRecordAsync(new PackageAuditRecord(package, AuditedPackageAction.UndoEdit)); + TempData["Message"] = "Your pending edits for this package were successfully canceled."; } else @@ -223,28 +233,17 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadF _packageService.EnsureValid(packageArchiveReader); } - catch (InvalidPackageException ipex) - { - ipex.Log(); - ModelState.AddModelError(String.Empty, ipex.Message); - return View(); - } - catch (InvalidDataException idex) - { - idex.Log(); - ModelState.AddModelError(String.Empty, idex.Message); - return View(); - } - catch (EntityException enex) - { - enex.Log(); - ModelState.AddModelError(String.Empty, enex.Message); - return View(); - } catch (Exception ex) { ex.Log(); - ModelState.AddModelError(String.Empty, Strings.FailedToReadUploadFile); + + var message = Strings.FailedToReadUploadFile; + if (ex is InvalidPackageException || ex is InvalidDataException || ex is EntityException) + { + message = ex.Message; + } + + ModelState.AddModelError(String.Empty, message); return View(); } finally @@ -343,8 +342,11 @@ public virtual async Task DisplayPackage(string id, string version var isIndexed = HttpContext.Cache.Get(isIndexedCacheKey) as bool?; if (!isIndexed.HasValue) { + var normalizedRegistrationId = package.PackageRegistration.Id + .Normalize(NormalizationForm.FormC); + var searchFilter = SearchAdaptor.GetSearchFilter( - "id:\"" + package.PackageRegistration.Id + "\" AND version:\"" + package.Version + "\"", + "id:\"" + normalizedRegistrationId + "\" AND version:\"" + package.Version + "\"", 1, null, SearchFilter.ODataSearchContext); searchFilter.IncludePrerelease = true; @@ -375,8 +377,11 @@ public virtual async Task DisplayPackage(string id, string version return View(model); } - public virtual async Task ListPackages(string q, int page = 1) + public virtual async Task ListPackages(PackageListSearchViewModel searchAndListModel) { + var page = searchAndListModel.Page; + var q = searchAndListModel.Q; + if (page < 1) { page = 1; @@ -456,6 +461,7 @@ public virtual async Task ListPackages(string q, int page = 1) ReportPackageReason.Other }; + [HttpGet] public virtual ActionResult ReportAbuse(string id, string version) { var package = _packageService.FindPackageByIdAndVersion(id, version); @@ -495,12 +501,12 @@ public virtual ActionResult ReportAbuse(string id, string version) private static readonly ReportPackageReason[] ReportMyPackageReasons = { ReportPackageReason.ContainsPrivateAndConfidentialData, - ReportPackageReason.PublishedWithWrongVersion, ReportPackageReason.ReleasedInPublicByAccident, ReportPackageReason.ContainsMaliciousCode, ReportPackageReason.Other }; + [HttpGet] [Authorize] [RequiresAccountConfirmation("contact support about your package")] public virtual ActionResult ReportMyPackage(string id, string version) @@ -636,6 +642,7 @@ public virtual async Task ReportMyPackage(string id, string versio return Redirect(Url.Package(id, version)); } + [HttpGet] [Authorize] [RequiresAccountConfirmation("contact package owners")] public virtual ActionResult ContactOwners(string id) @@ -703,11 +710,13 @@ public virtual ActionResult ContactOwners(string id, ContactOwnersViewModel cont } // This is the page that explains why there's no download link. + [HttpGet] public virtual ActionResult Download() { return View(); } + [HttpGet] [Authorize] public virtual ActionResult ManagePackageOwners(string id) { @@ -725,7 +734,8 @@ public virtual ActionResult ManagePackageOwners(string id) return View(model); } - + + [HttpGet] [Authorize] [RequiresAccountConfirmation("delete a package")] public virtual ActionResult Delete(string id, string version) @@ -744,6 +754,40 @@ public virtual ActionResult Delete(string id, string version) return View(model); } + [Authorize(Roles = "Admins")] + [RequiresAccountConfirmation("reflow a package")] + public virtual async Task Reflow(string id, string version) + { + var package = _packageService.FindPackageByIdAndVersion(id, version); + + if (package == null) + { + return HttpNotFound(); + } + + var reflowPackageService = new ReflowPackageService( + _entitiesContext, + (PackageService) _packageService, + _packageFileService); + + try + { + await reflowPackageService.ReflowAsync(id, version); + + TempData["Message"] = + "The package is being reflowed. It may take a while for this change to propagate through our system."; + } + catch (Exception ex) + { + TempData["Message"] = + $"An error occurred while reflowing the package. {ex.Message}"; + + QuietLog.LogHandledException(ex); + } + + return SafeRedirect(Url.Package(id, version)); + } + [Authorize(Roles = "Admins")] [HttpPost] [RequiresAccountConfirmation("delete a package")] @@ -809,6 +853,7 @@ public virtual async Task UpdateListed(string id, string version, return await Edit(id, version, listed, Url.Package); } + [HttpGet] [Authorize] [RequiresAccountConfirmation("edit a package")] public virtual ActionResult Edit(string id, string version) @@ -871,6 +916,10 @@ public virtual async Task Edit(string id, string version, EditPack { _editPackageService.StartEditPackageRequest(package, formData.Edit, user); await _entitiesContext.SaveChangesAsync(); + + var packageWithEditsApplied = formData.Edit.ApplyTo(package); + + await _auditingService.SaveAuditRecordAsync(new PackageAuditRecord(packageWithEditsApplied, AuditedPackageAction.Edit)); } catch (EntityException ex) { @@ -1123,14 +1172,37 @@ public virtual async Task VerifyPackage(VerifyPackageRequest formD // save package to blob storage uploadFile.Position = 0; - await _packageFileService.SavePackageFileAsync(package, uploadFile.AsSeekableStream()); + try + { + await _packageFileService.SavePackageFileAsync(package, uploadFile.AsSeekableStream()); + } + catch (InvalidOperationException ex) + { + ex.Log(); + TempData["Message"] = Strings.UploadPackage_IdVersionConflict; + return new RedirectResult(Url.VerifyPackage()); + } - // commit all changes to database as an atomic transaction - await _entitiesContext.SaveChangesAsync(); + try + { + // commit all changes to database as an atomic transaction + await _entitiesContext.SaveChangesAsync(); + } + catch + { + // If saving to the DB fails for any reason we need to delete the package we just saved. + await _packageFileService.DeletePackageFileAsync(packageMetadata.Id, packageMetadata.Version.ToNormalizedString()); + throw; + } // tell Lucene to update index for the new package _indexingService.UpdateIndex(); + // write an audit record + await _auditingService.SaveAuditRecordAsync( + new PackageAuditRecord(package, AuditedPackageAction.Create, PackageCreatedVia.Web)); + + // notify user _messageService.SendPackageAddedNotice(package, Url.Action("DisplayPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme), Url.Action("ReportMyPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme), @@ -1140,6 +1212,8 @@ public virtual async Task VerifyPackage(VerifyPackageRequest formD // delete the uploaded binary in the Uploads container await _uploadFileService.DeleteUploadFileAsync(currentUser.Key); + _telemetryService.TrackPackagePushEvent(package, currentUser, User.Identity); + TempData["Message"] = String.Format( CultureInfo.CurrentCulture, Strings.SuccessfullyUploadedPackage, package.PackageRegistration.Id, package.Version); diff --git a/src/NuGetGallery/Controllers/PagesController.cs b/src/NuGetGallery/Controllers/PagesController.cs index 729b1a6046..6d1c4b01c9 100644 --- a/src/NuGetGallery/Controllers/PagesController.cs +++ b/src/NuGetGallery/Controllers/PagesController.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; +using System.Web; using System.Web.Mvc; using NuGetGallery.Areas.Admin; @@ -30,6 +31,7 @@ public PagesController(IContentService contentService, } // This will let you add 'static' cshtml pages to the site under View/Pages or Branding/Views/Pages + [HttpGet] public virtual ActionResult Page(string pageName) { // Prevent traversal attacks and serving non-pages by disallowing ., /, %, and more! @@ -41,23 +43,27 @@ public virtual ActionResult Page(string pageName) return View(pageName); } + [HttpGet] public virtual ActionResult About() { return View(); } + [HttpGet] public virtual ActionResult Contact() { return View(); } + [HttpGet] public virtual ActionResult Downloads() { return Redirect("https://dist.nuget.org/index.html"); } - [Authorize] [HttpPost] + [Authorize] + [ValidateAntiForgeryToken] public virtual async Task Contact(ContactSupportViewModel contactForm) { if (!ModelState.IsValid) @@ -91,13 +97,18 @@ public virtual async Task Home() { if (_contentService != null) { - ViewBag.Content = await _contentService.GetContentItemAsync( - Constants.ContentNames.Home, - TimeSpan.FromMinutes(1)); + var homeContent = await _contentService.GetContentItemAsync( + Constants.ContentNames.Home, + TimeSpan.FromMinutes(1)); + + homeContent = new HtmlString(homeContent.ToString().Replace("~/", Url.Content("~/"))); + + ViewBag.Content = homeContent; } return View(); } + [HttpGet] public virtual ActionResult EmptyHome() { return new HttpStatusCodeResult(HttpStatusCode.OK, "Empty Home"); diff --git a/src/NuGetGallery/Controllers/StatisticsController.cs b/src/NuGetGallery/Controllers/StatisticsController.cs index cf74c85cc8..3fe1a1c5f6 100644 --- a/src/NuGetGallery/Controllers/StatisticsController.cs +++ b/src/NuGetGallery/Controllers/StatisticsController.cs @@ -16,6 +16,19 @@ public partial class StatisticsController private readonly IStatisticsService _statisticsService = null; private readonly IAggregateStatsService _aggregateStatsService = null; + private static readonly string[] PackageDownloadsByVersionDimensions = new[] { + Constants.StatisticsDimensions.Version, + Constants.StatisticsDimensions.ClientName, + Constants.StatisticsDimensions.ClientVersion, + Constants.StatisticsDimensions.Operation + }; + + private static readonly string[] PackageDownloadsDetailDimensions = new [] { + Constants.StatisticsDimensions.ClientName, + Constants.StatisticsDimensions.ClientVersion, + Constants.StatisticsDimensions.Operation + }; + public StatisticsController(IAggregateStatsService aggregateStatsService) { _statisticsService = null; @@ -34,13 +47,12 @@ public StatisticsController(IStatisticsService statisticsService, IAggregateStat _aggregateStatsService = aggregateStatsService; } - [HttpGet] + [AcceptVerbs(HttpVerbs.Get | HttpVerbs.Head)] [OutputCache(VaryByHeader = "Accept-Language", Duration = 120, Location = OutputCacheLocation.Server)] public virtual async Task Totals() { var stats = await _aggregateStatsService.GetAggregateStats(); - return Json( new { @@ -52,8 +64,6 @@ public virtual async Task Totals() JsonRequestBehavior.AllowGet); } - - // // GET: /stats @@ -148,9 +158,17 @@ public virtual async Task PackageDownloadsByVersion(string id, str return new HttpStatusCodeResult(HttpStatusCode.NotFound); } - StatisticsPackagesReport report = await _statisticsService.GetPackageDownloadsByVersion(id); + StatisticsPackagesReport report = null; + try + { + report = await _statisticsService.GetPackageDownloadsByVersion(id); - ProcessReport(report, groupby, new string[] { "Version", "ClientName", "ClientVersion", "Operation" }, id); + ProcessReport(report, groupby, PackageDownloadsByVersionDimensions, id); + } + catch (StatisticsReportNotFoundException) + { + // no report found + } if (report != null) { @@ -176,9 +194,17 @@ public virtual async Task PackageDownloadsDetail(string id, string return new HttpStatusCodeResult(HttpStatusCode.NotFound); } - StatisticsPackagesReport report = await _statisticsService.GetPackageVersionDownloadsByClient(id, version); + StatisticsPackagesReport report = null; + try + { + report = await _statisticsService.GetPackageVersionDownloadsByClient(id, version); - ProcessReport(report, groupby, new[] { "ClientName", "ClientVersion", "Operation" }, null); + ProcessReport(report, groupby, PackageDownloadsDetailDimensions, null); + } + catch (StatisticsReportNotFoundException) + { + // no report found + } if (report != null) { @@ -204,7 +230,7 @@ private void ProcessReport(StatisticsPackagesReport report, string[] groupby, st var pivot = new string[4]; if (groupby != null) { - // process and validate the groupby query. unrecognized fields are ignored. others fields regarded for existance + // process and validate the groupby query. unrecognized fields are ignored. others fields regarded for existence var dim = 0; foreach (var dimension in dimensions) @@ -222,7 +248,10 @@ private void ProcessReport(StatisticsPackagesReport report, string[] groupby, st // the pivot array is used as the Columns in the report so we resize because this was the final set of columns Array.Resize(ref pivot, dim); } + } + if (groupby != null) + { Tuple result = StatisticsPivot.GroupBy(report.Facts, pivot); if (id != null) @@ -252,7 +281,15 @@ private void ProcessReport(StatisticsPackagesReport report, string[] groupby, st foreach (string dimension in dimensions) { - report.Dimensions.Add(new StatisticsDimension { Value = dimension, DisplayName = GetDimensionDisplayName(dimension), IsChecked = false }); + if (!report.Dimensions.Any(d => d.Value == dimension)) + { + report.Dimensions.Add(new StatisticsDimension + { + Value = dimension, + DisplayName = GetDimensionDisplayName(dimension), + IsChecked = false + }); + } } report.Table = null; diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index 7bfd513e63..217084a8e6 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -2,24 +2,28 @@ // 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.Net; using System.Net.Mail; using System.Threading.Tasks; using System.Web.Mvc; using NuGetGallery.Authentication; using NuGetGallery.Configuration; +using NuGetGallery.Infrastructure.Authentication; namespace NuGetGallery { public partial class UsersController : AppController { - public ICuratedFeedService CuratedFeedService { get; protected set; } - public IUserService UserService { get; protected set; } - public IMessageService MessageService { get; protected set; } - public IPackageService PackageService { get; protected set; } - public IAppConfiguration Config { get; protected set; } - public AuthenticationService AuthService { get; protected set; } + private readonly ICuratedFeedService _curatedFeedService; + private readonly IUserService _userService; + private readonly IMessageService _messageService; + private readonly IPackageService _packageService; + private readonly IAppConfiguration _config; + private readonly AuthenticationService _authService; + private readonly ICredentialBuilder _credentialBuilder; public UsersController( ICuratedFeedService feedsQuery, @@ -27,14 +31,51 @@ public UsersController( IPackageService packageService, IMessageService messageService, IAppConfiguration config, - AuthenticationService authService) + AuthenticationService authService, + ICredentialBuilder credentialBuilder) { - CuratedFeedService = feedsQuery; - UserService = userService; - PackageService = packageService; - MessageService = messageService; - Config = config; - AuthService = authService; + if (feedsQuery == null) + { + throw new ArgumentNullException(nameof(feedsQuery)); + } + + if (userService == null) + { + throw new ArgumentNullException(nameof(userService)); + } + + if (packageService == null) + { + throw new ArgumentNullException(nameof(packageService)); + } + + if (messageService == null) + { + throw new ArgumentNullException(nameof(messageService)); + } + + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + if (authService == null) + { + throw new ArgumentNullException(nameof(authService)); + } + + if (credentialBuilder == null) + { + throw new ArgumentNullException(nameof(credentialBuilder)); + } + + _curatedFeedService = feedsQuery; + _userService = userService; + _packageService = packageService; + _messageService = messageService; + _config = config; + _authService = authService; + _credentialBuilder = credentialBuilder; } [HttpGet] @@ -53,6 +94,7 @@ public virtual ActionResult ConfirmationRequired() [Authorize] [HttpPost] [ActionName("ConfirmationRequired")] + [ValidateAntiForgeryToken] public virtual ActionResult ConfirmationRequiredPost() { User user = GetCurrentUser(); @@ -64,7 +106,7 @@ public virtual ActionResult ConfirmationRequiredPost() ConfirmationViewModel model; if (!alreadyConfirmed) { - MessageService.SendNewAccountEmail(new MailAddress(user.UnconfirmedEmailAddress, user.Username), confirmationUrl); + _messageService.SendNewAccountEmail(new MailAddress(user.UnconfirmedEmailAddress, user.Username), confirmationUrl); model = new ConfirmationViewModel { @@ -80,6 +122,7 @@ public virtual ActionResult ConfirmationRequiredPost() return View(model); } + [HttpGet] [Authorize] public virtual ActionResult Account() { @@ -97,7 +140,7 @@ public virtual async Task ChangeEmailSubscription(bool? emailAllow return HttpNotFound(); } - await UserService.ChangeEmailSubscriptionAsync(user, + await _userService.ChangeEmailSubscriptionAsync(user, emailAllowed.HasValue && emailAllowed.Value, notifyPackagePushed.HasValue && notifyPackagePushed.Value); @@ -105,6 +148,7 @@ await UserService.ChangeEmailSubscriptionAsync(user, return RedirectToAction("Account"); } + [HttpGet] [Authorize] public virtual ActionResult Thanks() { @@ -114,11 +158,12 @@ public virtual ActionResult Thanks() return View(); } + [HttpGet] [Authorize] public virtual ActionResult Packages() { var user = GetCurrentUser(); - var packages = PackageService.FindPackagesByOwner(user, includeUnlisted: true) + var packages = _packageService.FindPackagesByOwner(user, includeUnlisted: true) .Select(p => new PackageViewModel(p) { DownloadCount = p.PackageRegistration.DownloadCount, @@ -132,6 +177,7 @@ public virtual ActionResult Packages() return View(model); } + [HttpGet] public virtual ActionResult ForgotPassword() { // We don't want Login to have us as a return URL @@ -151,7 +197,7 @@ public virtual async Task ForgotPassword(ForgotPasswordViewModel m if (ModelState.IsValid) { - var user = await AuthService.GeneratePasswordResetToken(model.Email, Constants.DefaultPasswordResetTokenExpirationHours * 60); + var user = await _authService.GeneratePasswordResetToken(model.Email, Constants.PasswordResetTokenExpirationHours * 60); if (user != null) { return SendPasswordResetEmail(user, forgotPassword: true); @@ -163,6 +209,7 @@ public virtual async Task ForgotPassword(ForgotPasswordViewModel m return View(model); } + [HttpGet] public virtual ActionResult PasswordSent() { // We don't want Login to have us as a return URL @@ -170,10 +217,11 @@ public virtual ActionResult PasswordSent() ViewData[Constants.ReturnUrlViewDataKey] = null; ViewBag.Email = TempData["Email"]; - ViewBag.Expiration = Constants.DefaultPasswordResetTokenExpirationHours; + ViewBag.Expiration = Constants.PasswordResetTokenExpirationHours; return View(); } + [HttpGet] public virtual ActionResult ResetPassword(bool forgot) { // We don't want Login to have us as a return URL @@ -193,12 +241,17 @@ public virtual async Task ResetPassword(string username, string to // By having this value present in the dictionary BUT null, we don't put "returnUrl" on the Login link at all ViewData[Constants.ReturnUrlViewDataKey] = null; + if (!ModelState.IsValid) + { + return ResetPassword(forgot); + } + ViewBag.ForgotPassword = forgot; Credential credential = null; try { - credential = await AuthService.ResetPasswordWithToken(username, token, model.NewPassword); + credential = await _authService.ResetPasswordWithToken(username, token, model.NewPassword); } catch (InvalidOperationException ex) { @@ -217,7 +270,7 @@ public virtual async Task ResetPassword(string username, string to if (credential != null && !forgot) { // Setting a password, so notify the user - MessageService.SendCredentialAddedNotice(credential.User, credential); + _messageService.SendCredentialAddedNotice(credential.User, credential); } return RedirectToAction( @@ -258,7 +311,7 @@ public virtual async Task Confirm(string username, string token) try { - if (!(await UserService.ConfirmEmailAddress(user, token))) + if (!(await _userService.ConfirmEmailAddress(user, token))) { model.SuccessfulConfirmation = false; } @@ -273,7 +326,7 @@ public virtual async Task Confirm(string username, string token) // Change notice not required for new accounts. if (model.SuccessfulConfirmation && !model.ConfirmingNewAccount) { - MessageService.SendEmailChangeNoticeToPreviousEmailAddress(user, existingEmail); + _messageService.SendEmailChangeNoticeToPreviousEmailAddress(user, existingEmail); string returnUrl = HttpContext.GetConfirmationReturnUrl(); if (!String.IsNullOrEmpty(returnUrl)) @@ -287,15 +340,16 @@ public virtual async Task Confirm(string username, string token) return View(model); } + [HttpGet] public virtual ActionResult Profiles(string username, int page = 1, bool showAllPackages = false) { - var user = UserService.FindByUsername(username); + var user = _userService.FindByUsername(username); if (user == null) { return HttpNotFound(); } - var packages = PackageService.FindPackagesByOwner(user, includeUnlisted: false) + var packages = _packageService.FindPackagesByOwner(user, includeUnlisted: false) .OrderByDescending(p => p.PackageRegistration.DownloadCount) .Select(p => new PackageViewModel(p) { @@ -325,16 +379,18 @@ public virtual async Task ChangeEmail(AccountViewModel model) return AccountView(model); } - var authUser = await AuthService.Authenticate(User.Identity.Name, model.ChangeEmail.Password); - if (authUser == null) + Credential _; + + if (!_authService.ValidatePasswordCredential(user.Credentials, model.ChangeEmail.Password, out _)) { ModelState.AddModelError("ChangeEmail.Password", Strings.CurrentPasswordIncorrect); return AccountView(model); } } + // No password? We can't do any additional verification... - if (String.Equals(model.ChangeEmail.NewEmail, user.LastSavedEmailAddress, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(model.ChangeEmail.NewEmail, user.LastSavedEmailAddress, StringComparison.OrdinalIgnoreCase)) { // email address unchanged - accept return RedirectToAction(actionName: "Account", controllerName: "Users"); @@ -342,7 +398,7 @@ public virtual async Task ChangeEmail(AccountViewModel model) try { - await UserService.ChangeEmailAddress(user, model.ChangeEmail.NewEmail); + await _userService.ChangeEmailAddress(user, model.ChangeEmail.NewEmail); } catch (EntityException e) { @@ -354,7 +410,7 @@ public virtual async Task ChangeEmail(AccountViewModel model) { var confirmationUrl = Url.ConfirmationUrl( "Confirm", "Users", user.Username, user.EmailConfirmationToken); - MessageService.SendEmailChangeConfirmationNotice(new MailAddress(user.UnconfirmedEmailAddress, user.Username), confirmationUrl); + _messageService.SendEmailChangeConfirmationNotice(new MailAddress(user.UnconfirmedEmailAddress, user.Username), confirmationUrl); TempData["Message"] = Strings.EmailUpdated_ConfirmationRequired; } @@ -377,7 +433,7 @@ public virtual async Task CancelChangeEmail(AccountViewModel model return RedirectToAction(actionName: "Account", controllerName: "Users"); } - await UserService.CancelChangeEmailAddress(user); + await _userService.CancelChangeEmailAddress(user); TempData["Message"] = Strings.CancelEmailAddress; @@ -398,7 +454,8 @@ public virtual async Task ChangePassword(AccountViewModel model) if (oldPassword == null) { // User is requesting a password set email - await AuthService.GeneratePasswordResetToken(user, Constants.DefaultPasswordResetTokenExpirationHours * 60); + await _authService.GeneratePasswordResetToken(user, Constants.PasswordResetTokenExpirationHours * 60); + return SendPasswordResetEmail(user, forgotPassword: false); } else @@ -408,14 +465,13 @@ public virtual async Task ChangePassword(AccountViewModel model) return AccountView(model); } - if (!(await AuthService.ChangePassword(user, model.ChangePassword.OldPassword, model.ChangePassword.NewPassword))) + if (!await _authService.ChangePassword(user, model.ChangePassword.OldPassword, model.ChangePassword.NewPassword)) { ModelState.AddModelError("ChangePassword.OldPassword", Strings.CurrentPasswordIncorrect); return AccountView(model); } TempData["Message"] = Strings.PasswordChanged; - return RedirectToAction("Account"); } } @@ -429,21 +485,69 @@ public virtual Task RemovePassword() var passwordCred = user.Credentials.SingleOrDefault( c => c.Type.StartsWith(CredentialTypes.Password.Prefix, StringComparison.OrdinalIgnoreCase)); - return RemoveCredential(user, passwordCred, Strings.PasswordRemoved); + return RemoveCredentialInternal(user, passwordCred, Strings.PasswordRemoved); } [HttpPost] [Authorize] [ValidateAntiForgeryToken] - public virtual Task RemoveCredential(string credentialType) + public virtual async Task RemoveCredential(string credentialType, int? credentialKey) { var user = GetCurrentUser(); var cred = user.Credentials.SingleOrDefault( - c => String.Equals(c.Type, credentialType, StringComparison.OrdinalIgnoreCase)); + c => string.Equals(c.Type, credentialType, StringComparison.OrdinalIgnoreCase) + && CredentialKeyMatches(credentialKey, c)); - return RemoveCredential(user, cred, Strings.CredentialRemoved); + if (CredentialTypes.IsApiKey(credentialType)) + { + return await RemoveApiKeyCredential(user, cred); + } + + return await RemoveCredentialInternal(user, cred, Strings.CredentialRemoved); } + [HttpPost] + [Authorize] + [ValidateAntiForgeryToken] + public virtual async Task RegenerateCredential(string credentialType, int? credentialKey) + { + if (credentialType != CredentialTypes.ApiKey.V2) + { + Response.StatusCode = (int)HttpStatusCode.BadRequest; + return Json(Strings.Unsupported); + } + + var user = GetCurrentUser(); + var cred = user.Credentials.SingleOrDefault( + c => string.Equals(c.Type, credentialType, StringComparison.OrdinalIgnoreCase) + && CredentialKeyMatches(credentialKey, c)); + + if (cred == null) + { + Response.StatusCode = (int)HttpStatusCode.NotFound; + return Json(Strings.CredentialNotFound); + } + + var newCredential = await GenerateApiKeyInternal( + cred.Description, + BuildScopes(cred.Scopes), + cred.ExpirationTicks.HasValue + ? new TimeSpan(cred.ExpirationTicks.Value) : new TimeSpan?()); + + await _authService.RemoveCredential(user, cred); + + var credentialViewModel = _authService.DescribeCredential(newCredential); + credentialViewModel.Value = newCredential.Value; + + return Json(credentialViewModel); + } + + private static bool CredentialKeyMatches(int? credentialKey, Credential c) + { + return (credentialKey == null || credentialKey == 0 || c.Key == credentialKey); + } + + [HttpGet] public virtual ActionResult PasswordChanged() { return View(); @@ -452,48 +556,175 @@ public virtual ActionResult PasswordChanged() [Authorize] [HttpPost] [ValidateAntiForgeryToken] - public virtual async Task GenerateApiKey() + public virtual async Task GenerateApiKey(string description, string[] scopes = null, string[] subjects = null, int? expirationInDays = null) + { + if (string.IsNullOrWhiteSpace(description)) + { + Response.StatusCode = (int)HttpStatusCode.BadRequest; + return Json(Strings.ApiKeyDescriptionRequired); + } + + // Set expiration + var expiration = TimeSpan.Zero; + if (_config.ExpirationInDaysForApiKeyV1 > 0) + { + expiration = TimeSpan.FromDays(_config.ExpirationInDaysForApiKeyV1); + + if (expirationInDays.HasValue && expirationInDays.Value > 0) + { + expiration = TimeSpan.FromDays(Math.Min(expirationInDays.Value, _config.ExpirationInDaysForApiKeyV1)); + } + } + + var newCredential = await GenerateApiKeyInternal(description, BuildScopes(scopes, subjects), expiration); + var credentialViewModel = _authService.DescribeCredential(newCredential); + credentialViewModel.Value = newCredential.Value; + + _messageService.SendCredentialAddedNotice(GetCurrentUser(), newCredential); + + return Json(credentialViewModel); + } + + [Authorize] + [HttpPost] + [ValidateAntiForgeryToken] + public virtual async Task EditCredential(string credentialType, int? credentialKey, string[] subjects) { - // Get the user + if (credentialType != CredentialTypes.ApiKey.V2) + { + Response.StatusCode = (int)HttpStatusCode.BadRequest; + return Json(Strings.Unsupported); + } + var user = GetCurrentUser(); + var cred = user.Credentials.SingleOrDefault( + c => string.Equals(c.Type, credentialType, StringComparison.OrdinalIgnoreCase) + && CredentialKeyMatches(credentialKey, c)); - // Generate an API Key - var apiKey = Guid.NewGuid(); + if (cred == null) + { + Response.StatusCode = (int)HttpStatusCode.NotFound; + return Json(Strings.CredentialNotFound); + } - // Add/Replace the API Key credential, and save to the database - TempData["Message"] = Strings.ApiKeyReset; - await AuthService.ReplaceCredential(user, CredentialBuilder.CreateV1ApiKey(apiKey)); - return RedirectToAction("Account"); + var scopes = cred.Scopes.Select(x => x.AllowedAction).Distinct().ToArray(); + var newScopes = BuildScopes(scopes, subjects); + + await _authService.EditCredentialScopes(user, cred, newScopes); + + var credentialViewModel = _authService.DescribeCredential(cred); + + return Json(credentialViewModel); + } + + private async Task GenerateApiKeyInternal(string description, ICollection scopes, TimeSpan? expiration) + { + var user = GetCurrentUser(); + + // Create a new API Key credential, and save to the database + var newCredential = _credentialBuilder.CreateApiKey(expiration); + newCredential.Description = description; + newCredential.Scopes = scopes; + + await _authService.AddCredential(user, newCredential); + + return newCredential; + } + + private static IList BuildScopes(string[] scopes, string[] subjects) + { + var result = new List(); + + var subjectsList = subjects?.Where(s => !string.IsNullOrWhiteSpace(s)).ToList() ?? new List(); + + // No package filtering information was provided. So allow any pattern. + if (!subjectsList.Any()) + { + subjectsList.Add(NuGetPackagePattern.AllInclusivePattern); + } + + if (scopes != null) + { + foreach (var scope in scopes) + { + result.AddRange(subjectsList.Select(subject => new Scope(subject, scope))); + } + } + else + { + result.AddRange(subjectsList.Select(subject => new Scope(subject, NuGetScopes.All))); + } + + return result; + } + + private static IList BuildScopes(IEnumerable scopes) + { + return scopes.Select(scope => new Scope {AllowedAction = scope.AllowedAction, Subject = scope.Subject}).ToList(); } - private async Task RemoveCredential(User user, Credential cred, string message) + + private async Task RemoveApiKeyCredential(User user, Credential cred) { - // Count login credentials - if (CountLoginCredentials(user) <= 1) + if (cred == null) + { + Response.StatusCode = (int)HttpStatusCode.NotFound; + return Json(Strings.CredentialNotFound); + } + + await _authService.RemoveCredential(user, cred); + + // Notify the user of the change + _messageService.SendCredentialRemovedNotice(user, cred); + + return Json(Strings.CredentialRemoved); + } + + private async Task RemoveCredentialInternal(User user, Credential cred, string message) + { + if (cred == null) + { + TempData["Message"] = Strings.CredentialNotFound; + + return RedirectToAction("Account"); + } + + // Count credentials and make sure the user can always login + if (!CredentialTypes.IsApiKey(cred.Type) && CountLoginCredentials(user) <= 1) { TempData["Message"] = Strings.CannotRemoveOnlyLoginCredential; } - else if (cred != null) + else { - await AuthService.RemoveCredential(user, cred); + await _authService.RemoveCredential(user, cred); // Notify the user of the change - MessageService.SendCredentialRemovedNotice(user, cred); + _messageService.SendCredentialRemovedNotice(user, cred); TempData["Message"] = message; } + return RedirectToAction("Account"); } private ActionResult AccountView(AccountViewModel model) { - // Load Credential info + // Load user info var user = GetCurrentUser(); - var curatedFeeds = CuratedFeedService.GetFeedsForManager(user.Key); - var creds = user.Credentials.Select(c => AuthService.DescribeCredential(c)).ToList(); + var curatedFeeds = _curatedFeedService.GetFeedsForManager(user.Key); + var creds = user.Credentials.Where(c => CredentialTypes.IsViewSupportedCredential(c)) + .Select(c => _authService.DescribeCredential(c)).ToList(); + var packageNames = _packageService.FindPackageRegistrationsByOwner(user).Select(p => p.Id).ToList(); + + packageNames.Sort(); + model.Credentials = creds; model.CuratedFeeds = curatedFeeds.Select(f => f.Name); + model.Packages = packageNames; + + model.ExpirationInDaysForApiKeyV1 = _config.ExpirationInDaysForApiKeyV1; + return View("Account", model); } @@ -512,7 +743,7 @@ private ActionResult SendPasswordResetEmail(User user, bool forgotPassword) user.Username, user.PasswordResetToken, new { forgot = forgotPassword }); - MessageService.SendPasswordResetInstructions(user, resetPasswordUrl, forgotPassword); + _messageService.SendPasswordResetInstructions(user, resetPasswordUrl, forgotPassword); return RedirectToAction(actionName: "PasswordSent", controllerName: "Users"); } diff --git a/src/NuGetGallery/Diagnostics/ElmahHandleErrorAttribute.cs b/src/NuGetGallery/Diagnostics/ElmahHandleErrorAttribute.cs new file mode 100644 index 0000000000..ee50f25e41 --- /dev/null +++ b/src/NuGetGallery/Diagnostics/ElmahHandleErrorAttribute.cs @@ -0,0 +1,64 @@ +// 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.Web; +using System.Web.Mvc; +using Elmah; + +namespace NuGetGallery.Diagnostics +{ + /// + /// Source: http://stackoverflow.com/a/779961/52749 + /// + public class ElmahHandleErrorAttribute : HandleErrorAttribute + { + public override void OnException(ExceptionContext context) + { + base.OnException(context); + if (!context.ExceptionHandled // if unhandled, will be logged anyhow + || TryRaiseErrorSignal(context) // prefer signaling, if possible + || IsFiltered(context)) // filtered? + return; + + LogException(context); + } + + private static bool TryRaiseErrorSignal(ExceptionContext context) + { + var httpContext = GetHttpContextImpl(context.HttpContext); + if (httpContext == null) + return false; + var signal = ErrorSignal.FromContext(httpContext); + if (signal == null) + return false; + signal.Raise(context.Exception, httpContext); + return true; + } + + private static bool IsFiltered(ExceptionContext context) + { + var config = context.HttpContext.GetSection("elmah/errorFilter") + as ErrorFilterConfiguration; + + if (config == null) + return false; + + var testContext = new ErrorFilterModule.AssertionHelperContext( + context.Exception, + GetHttpContextImpl(context.HttpContext)); + return config.Assertion.Test(testContext); + } + + private static void LogException(ExceptionContext context) + { + var httpContext = GetHttpContextImpl(context.HttpContext); + var error = new Error(context.Exception, httpContext); + ErrorLog.GetDefault(httpContext).Log(error); + } + + private static HttpContext GetHttpContextImpl(HttpContextBase context) + { + return context.ApplicationInstance.Context; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Diagnostics/PerfCounters.cs b/src/NuGetGallery/Diagnostics/PerfCounters.cs index 90821a89d3..c3eef9c634 100644 --- a/src/NuGetGallery/Diagnostics/PerfCounters.cs +++ b/src/NuGetGallery/Diagnostics/PerfCounters.cs @@ -73,7 +73,7 @@ public PerfStats GetStats() double max = Double.MinValue; // Start at _position-1 and work backwards. - // In order to avoid negative indicies, we do this by ADDING the distance between the end of ring and i, + // In order to avoid negative indices, we do this by ADDING the distance between the end of ring and i, // then we use mod to get a real offset for (int i = 0; i < _count; i++) { diff --git a/src/NuGetGallery/Diagnostics/SendErrorsToTelemetryAttribute.cs b/src/NuGetGallery/Diagnostics/SendErrorsToTelemetryAttribute.cs index a7bf59c167..f39af59451 100644 --- a/src/NuGetGallery/Diagnostics/SendErrorsToTelemetryAttribute.cs +++ b/src/NuGetGallery/Diagnostics/SendErrorsToTelemetryAttribute.cs @@ -3,7 +3,6 @@ using System; using System.Web.Mvc; -using Elmah.Contrib.Mvc; using Microsoft.ApplicationInsights; namespace NuGetGallery.Diagnostics diff --git a/src/NuGetGallery/ExtensionMethods.cs b/src/NuGetGallery/ExtensionMethods.cs index 01f676341b..9ad8c0261f 100644 --- a/src/NuGetGallery/ExtensionMethods.cs +++ b/src/NuGetGallery/ExtensionMethods.cs @@ -19,6 +19,7 @@ using Microsoft.Owin; using NuGet.Frameworks; using NuGet.Packaging; +using NuGetGallery.Authentication; namespace NuGetGallery { @@ -97,7 +98,7 @@ public static IEnumerable AsPackageDependencyEnumerable(this else { foreach (var dependency in dependencyGroup.Packages.Select( - d => new {d.Id, d.VersionRange, dependencyGroup.TargetFramework})) + d => new { d.Id, d.VersionRange, dependencyGroup.TargetFramework })) { yield return new PackageDependency { @@ -110,6 +111,19 @@ public static IEnumerable AsPackageDependencyEnumerable(this } } + public static IEnumerable AsPackageTypeEnumerable(this IEnumerable packageTypes) + { + foreach (var packageType in packageTypes) + { + yield return new PackageType + { + Name = packageType.Name, + Version = packageType.Version.ToString() + }; + } + + } + public static string Flatten(this IEnumerable list) { if (list == null) @@ -126,6 +140,11 @@ public static string Flatten(this IEnumerable dependency AsPackageDependencyEnumerable(dependencyGroups).ToList()); } + public static string Flatten(this IEnumerable packageTypes) + { + return String.Join("|", packageTypes.Select(d => String.Format(CultureInfo.InvariantCulture, "{0}:{1}", d.Name, d.Version))); + } + public static string Flatten(this ICollection dependencies) { return @@ -253,6 +272,11 @@ public static IQueryable SortBy(this IQueryable source, string sortExpr public static MailAddress ToMailAddress(this User user) { + if (!user.Confirmed) + { + return new MailAddress(user.UnconfirmedEmailAddress, user.Username); + } + return new MailAddress(user.EmailAddress, user.Username); } @@ -349,11 +373,57 @@ public static string GetClaimOrDefault(this ClaimsIdentity self, string claimTyp public static string GetClaimOrDefault(this IEnumerable self, string claimType) { return self - .Where(c => String.Equals(c.Type, claimType, StringComparison.OrdinalIgnoreCase)) + .Where(c => string.Equals(c.Type, claimType, StringComparison.OrdinalIgnoreCase)) .Select(c => c.Value) .FirstOrDefault(); } + public static bool HasScopeThatAllowsActionForSubject( + this IIdentity self, + string subject, + string[] requestedActions) + { + var identity = self as ClaimsIdentity; + + if (identity == null) + { + return false; + } + + var scopeClaim = identity.GetClaimOrDefault(NuGetClaims.Scope); + + return ScopeEvaluator.ScopeClaimsAllowsActionForSubject(scopeClaim, subject, requestedActions); + } + + public static string GetAuthenticationType(this IIdentity self) + { + var identity = self as ClaimsIdentity; + + return identity?.GetClaimOrDefault(ClaimTypes.AuthenticationMethod); + } + + private static string GetScopeClaim(this IIdentity self) + { + var identity = self as ClaimsIdentity; + + return identity?.GetClaimOrDefault(NuGetClaims.Scope); + } + + public static bool IsScopedAuthentication(this IIdentity self) + { + var scopeClaim = self.GetScopeClaim(); + + return !ScopeEvaluator.IsEmptyScopeClaim(scopeClaim); + } + + public static bool HasVerifyScope(this IIdentity self) + { + var scopeClaim = self.GetScopeClaim(); + + return ScopeEvaluator.ScopeClaimsAllowsActionForSubject(scopeClaim, subject: null, + requestedActions: new [] { NuGetScopes.PackageVerify }); + } + // This is a method because the first call will perform a database call /// /// Get the current user, from the database, or if someone in this request has already diff --git a/src/NuGetGallery/Extensions/DateTimeExtensions.cs b/src/NuGetGallery/Extensions/DateTimeExtensions.cs index b14e2ca0b7..21d4c96936 100644 --- a/src/NuGetGallery/Extensions/DateTimeExtensions.cs +++ b/src/NuGetGallery/Extensions/DateTimeExtensions.cs @@ -20,7 +20,12 @@ public static bool IsInThePast(this DateTime date) public static string ToJavaScriptUTC(this DateTime self) { - return self.ToUniversalTime().ToString("O", CultureInfo.CurrentCulture); + return ToJavaScript(self.ToUniversalTime()); + } + + public static string ToJavaScript(this DateTime self) + { + return self.ToString("O", CultureInfo.CurrentCulture); } public static string ToNuGetShortDateTimeString(this DateTime self) diff --git a/src/NuGetGallery/Filters/ApiAuthorizeAttribute.cs b/src/NuGetGallery/Filters/ApiAuthorizeAttribute.cs index 0c5db4f446..3c8ec4f039 100644 --- a/src/NuGetGallery/Filters/ApiAuthorizeAttribute.cs +++ b/src/NuGetGallery/Filters/ApiAuthorizeAttribute.cs @@ -1,14 +1,58 @@ // 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.Globalization; +using System.Linq; +using System.Security.Claims; using System.Web; using System.Web.Mvc; using NuGetGallery.Authentication; +using AuthenticationTypes = NuGetGallery.Authentication.AuthenticationTypes; +using AuthorizationContext = System.Web.Mvc.AuthorizationContext; namespace NuGetGallery.Filters { public sealed class ApiAuthorizeAttribute : AuthorizeAttribute { + public override void OnAuthorization(AuthorizationContext filterContext) + { + // Add a warning header if the API key is about to expire (or has expired) + var identity = filterContext.HttpContext.User.Identity as ClaimsIdentity; + var controller = filterContext.Controller as AppController; + if (identity != null && identity.IsAuthenticated && identity.AuthenticationType == AuthenticationTypes.ApiKey && controller != null) + { + var apiKey = identity.GetClaimOrDefault(NuGetClaims.ApiKey); + + var user = controller.GetCurrentUser(); + + var apiKeyCredential = user.Credentials.FirstOrDefault(c => c.Value == apiKey); + if (apiKeyCredential != null && apiKeyCredential.Expires.HasValue) + { + var accountUrl = controller.NuGetContext.Config.GetSiteRoot( + controller.NuGetContext.Config.Current.RequireSSL).TrimEnd('/') + "/account"; + + var expirationPeriod = apiKeyCredential.Expires.Value - DateTime.UtcNow; + if (apiKeyCredential.HasExpired) + { + // expired warning + filterContext.HttpContext.Response.Headers.Add( + Constants.WarningHeaderName, + string.Format(CultureInfo.InvariantCulture, Strings.WarningApiKeyExpired, accountUrl)); + } + else if (expirationPeriod.TotalDays <= controller.NuGetContext.Config.Current.WarnAboutExpirationInDaysForApiKeyV1) + { + // about to expire warning + filterContext.HttpContext.Response.Headers.Add( + Constants.WarningHeaderName, + string.Format(CultureInfo.InvariantCulture, Strings.WarningApiKeyAboutToExpire, expirationPeriod.TotalDays, accountUrl)); + } + } + } + + base.OnAuthorization(filterContext); + } + protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { var owinContext = filterContext.HttpContext.GetOwinContext(); diff --git a/src/NuGetGallery/Filters/ApiScopeRequiredAttribute.cs b/src/NuGetGallery/Filters/ApiScopeRequiredAttribute.cs new file mode 100644 index 0000000000..1e819471cf --- /dev/null +++ b/src/NuGetGallery/Filters/ApiScopeRequiredAttribute.cs @@ -0,0 +1,46 @@ +// 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; +using System.Net; +using System.Security.Claims; +using System.Web; +using System.Web.Mvc; +using AuthenticationTypes = NuGetGallery.Authentication.AuthenticationTypes; +using AuthorizationContext = System.Web.Mvc.AuthorizationContext; + +namespace NuGetGallery.Filters +{ + public sealed class ApiScopeRequiredAttribute + : AuthorizeAttribute + { + public List Scopes { get; set; } + + public ApiScopeRequiredAttribute(params string[] scopes) + { + Scopes = scopes.ToList(); + } + + protected override bool AuthorizeCore(HttpContextBase httpContext) + { + var identity = httpContext.User.Identity as ClaimsIdentity; + if (identity != null && identity.IsAuthenticated) + { + return identity.HasScopeThatAllowsActionForSubject( + subject: null, + requestedActions: Scopes.ToArray()); + } + + return base.AuthorizeCore(httpContext); + } + + protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) + { + var owinContext = filterContext.HttpContext.GetOwinContext(); + owinContext.Authentication.Challenge(AuthenticationTypes.ApiKey); + owinContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + filterContext.Result = new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/GlobalSuppressions.cs b/src/NuGetGallery/GlobalSuppressions.cs new file mode 100644 index 0000000000..a0c0dd5dff Binary files /dev/null and b/src/NuGetGallery/GlobalSuppressions.cs differ diff --git a/src/NuGetGallery/Helpers/AccordianHelper.cs b/src/NuGetGallery/Helpers/AccordeonHelper.cs similarity index 65% rename from src/NuGetGallery/Helpers/AccordianHelper.cs rename to src/NuGetGallery/Helpers/AccordeonHelper.cs index 445fce16ab..fab4656cc4 100644 --- a/src/NuGetGallery/Helpers/AccordianHelper.cs +++ b/src/NuGetGallery/Helpers/AccordeonHelper.cs @@ -1,33 +1,36 @@ // 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.Web; using System.Web.Mvc; namespace NuGetGallery.Helpers { - public class AccordianHelper + public class AccordionHelper { - public string Name { get; private set; } - public string FormModelStatePrefix { get; private set; } - public WebViewPage Page { get; private set; } + private readonly bool _expanded; + + public string Name { get; } + public string FormModelStatePrefix { get; } + public WebViewPage Page { get; } public string ItemId { get { return Name + "-item"; } } public string ContentDropDownId { get { return Name + "-content"; } } + public string CollapseButtonId { get { return Name + "-collapse"; } } public string ContentHiddenClass { get { return Expanded ? null : "s-hidden"; } } public bool Expanded { get { - return FormModelStatePrefix != null && - !Page.ViewData.ModelState.IsValidField(FormModelStatePrefix); + return _expanded + || (FormModelStatePrefix != null && !Page.ViewData.ModelState.IsValidField(FormModelStatePrefix)); } } - public AccordianHelper(string name, string formModelStatePrefix, WebViewPage page) + public AccordionHelper(string name, string formModelStatePrefix, bool expanded, WebViewPage page) { + _expanded = expanded; + Name = name; FormModelStatePrefix = formModelStatePrefix; Page = page; @@ -36,10 +39,12 @@ public AccordianHelper(string name, string formModelStatePrefix, WebViewPage pag public HtmlString ExpandButton(string closedTitle, string expandedTitle) { return new HtmlString( - "" + (Expanded ? expandedTitle : closedTitle) + ""); @@ -48,10 +53,12 @@ public HtmlString ExpandButton(string closedTitle, string expandedTitle) public HtmlString ExpandLink(string closedTitle, string expandedTitle) { return new HtmlString( - "" + (Expanded ? expandedTitle : closedTitle) + ""); diff --git a/src/NuGetGallery/Helpers/HttpContextBaseExtensions.cs b/src/NuGetGallery/Helpers/HttpContextBaseExtensions.cs index f358edd72a..66b8441730 100644 --- a/src/NuGetGallery/Helpers/HttpContextBaseExtensions.cs +++ b/src/NuGetGallery/Helpers/HttpContextBaseExtensions.cs @@ -18,7 +18,9 @@ public static void SetConfirmationReturnUrl(this HttpContextBase httpContext, st }; string json = JsonConvert.SerializeObject(confirmationContext); string protectedJson = Convert.ToBase64String(MachineKey.Protect(Encoding.UTF8.GetBytes(json), "ConfirmationContext")); - httpContext.Response.Cookies.Add(new HttpCookie("ConfirmationContext", protectedJson)); + HttpCookie responseCookie = new HttpCookie("ConfirmationContext", protectedJson); + responseCookie.HttpOnly = true; + httpContext.Response.Cookies.Add(responseCookie); } public static string GetConfirmationReturnUrl(this HttpContextBase httpContext) diff --git a/src/NuGetGallery/Helpers/PackageHelper.cs b/src/NuGetGallery/Helpers/PackageHelper.cs index 482a91eedf..2c1b645707 100644 --- a/src/NuGetGallery/Helpers/PackageHelper.cs +++ b/src/NuGetGallery/Helpers/PackageHelper.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 { public static class PackageHelper @@ -13,5 +15,17 @@ public static string ParseTags(string tags) } return tags.Replace(',', ' ').Replace(';', ' ').Replace('\t', ' ').Replace(" ", " "); } + + public static bool ShouldRenderUrl(string url) + { + Uri uri = null; + if (!string.IsNullOrEmpty(url) && Uri.TryCreate(url, UriKind.Absolute, out uri)) + { + return uri.Scheme == Uri.UriSchemeHttps + || uri.Scheme == Uri.UriSchemeHttp; + } + + return false; + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Helpers/StringExtensions.cs b/src/NuGetGallery/Helpers/StringExtensions.cs index 1e5c357ce3..4750cf0a60 100644 --- a/src/NuGetGallery/Helpers/StringExtensions.cs +++ b/src/NuGetGallery/Helpers/StringExtensions.cs @@ -43,17 +43,20 @@ public static string Abbreviate(this string text, int length) return text.Substring(0, length - 3) + "..."; } - public static MvcHtmlString TruncateAtWordBoundary(this string input, int length = 300, string ommission = "...", string moreText = "") + public static string TruncateAtWordBoundary(this string input, int length, string omission, out bool wasTruncated) { + wasTruncated = false; if (string.IsNullOrEmpty(input) || input.Length < length) - return new MvcHtmlString(input); + { + return input; + } int nextSpace = input.LastIndexOf(" ", length, StringComparison.Ordinal); - return new MvcHtmlString(string.Format(CultureInfo.CurrentCulture, "{2}{1}{0}", - moreText, - ommission, - HttpUtility.HtmlEncode(input.Substring(0, (nextSpace > 0) ? nextSpace : length).Trim()))); + wasTruncated = true; + return string.Format(CultureInfo.CurrentCulture, "{0}{1}", + input.Substring(0, (nextSpace > 0) ? nextSpace : length).Trim(), + omission); } } } \ No newline at end of file diff --git a/src/NuGetGallery/Helpers/TelemetryResponseCodeFilter.cs b/src/NuGetGallery/Helpers/TelemetryResponseCodeFilter.cs new file mode 100644 index 0000000000..0c693c8734 --- /dev/null +++ b/src/NuGetGallery/Helpers/TelemetryResponseCodeFilter.cs @@ -0,0 +1,32 @@ +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; + +namespace NuGetGallery.Helpers +{ + public class TelemetryResponseCodeFilter : ITelemetryProcessor + { + public TelemetryResponseCodeFilter(ITelemetryProcessor next) + { + Next = next; + } + + private ITelemetryProcessor Next { get; set; } + + public void Process(ITelemetry item) + { + var request = item as RequestTelemetry; + int responseCode; + + if (request != null && int.TryParse(request.ResponseCode, out responseCode)) + { + if (responseCode == 400 || responseCode == 404) + { + request.Success = true; + } + } + + this.Next.Process(item); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Helpers/TreeView.cs b/src/NuGetGallery/Helpers/TreeView.cs index 86950be429..58b26576fc 100644 --- a/src/NuGetGallery/Helpers/TreeView.cs +++ b/src/NuGetGallery/Helpers/TreeView.cs @@ -22,7 +22,7 @@ public static TreeView TreeView(this HtmlHelper html, IEnumerable items } /// - /// Create an HTML tree from a resursive collection of items + /// Create an HTML tree from a recursive collection of items /// public class TreeView : IHtmlString { diff --git a/src/NuGetGallery/Infrastructure/Authentication/CredentialBuilder.cs b/src/NuGetGallery/Infrastructure/Authentication/CredentialBuilder.cs new file mode 100644 index 0000000000..2100553151 --- /dev/null +++ b/src/NuGetGallery/Infrastructure/Authentication/CredentialBuilder.cs @@ -0,0 +1,54 @@ +// 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 NuGetGallery.Authentication; +using NuGetGallery.Services.Authentication; + +namespace NuGetGallery.Infrastructure.Authentication +{ + public class CredentialBuilder : ICredentialBuilder + { + public const string LatestPasswordType = CredentialTypes.Password.V3; + + public Credential CreatePasswordCredential(string plaintextPassword) + { + return new Credential( + LatestPasswordType, + V3Hasher.GenerateHash(plaintextPassword)); + } + + public Credential CreateApiKey(TimeSpan? expiration) + { + return new Credential( + CredentialTypes.ApiKey.V2, + CreateKeyString(), + expiration: expiration); + } + + public Credential CreatePackageVerificationApiKey(string id) + { + var credential = new Credential( + CredentialTypes.ApiKey.VerifyV1, + CreateKeyString(), + expiration: TimeSpan.FromDays(1)); + + credential.Scopes.Add(new Scope(subject: id, allowedAction: NuGetScopes.PackageVerify)); + + return credential; + } + + public Credential CreateExternalCredential(string issuer, string value, string identity) + { + return new Credential(CredentialTypes.ExternalPrefix + issuer, value) + { + Identity = identity + }; + } + + private static string CreateKeyString() + { + return Guid.NewGuid().ToString().ToLowerInvariant(); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Infrastructure/Authentication/CredentialValidator.cs b/src/NuGetGallery/Infrastructure/Authentication/CredentialValidator.cs new file mode 100644 index 0000000000..3107c25327 --- /dev/null +++ b/src/NuGetGallery/Infrastructure/Authentication/CredentialValidator.cs @@ -0,0 +1,31 @@ +// 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.Web.Helpers; +using NuGetGallery.Services.Authentication; + +namespace NuGetGallery.Infrastructure.Authentication +{ + public class CredentialValidator : ICredentialValidator + { + public static readonly Dictionary> Validators = new Dictionary>(StringComparer.OrdinalIgnoreCase) { + { CredentialTypes.Password.V3, (password, cred) => V3Hasher.VerifyHash(hashedData: cred.Value, providedInput: password) }, + { CredentialTypes.Password.Pbkdf2, (password, cred) => Crypto.VerifyHashedPassword(hashedPassword: cred.Value, password: password) }, + { CredentialTypes.Password.Sha1, (password, cred) => LegacyHasher.VerifyHash(cred.Value, password, Constants.Sha1HashAlgorithmId) } + }; + + public bool ValidatePasswordCredential(Credential credential, string providedPassword) + { + Func validator; + + if (!Validators.TryGetValue(credential.Type, out validator)) + { + return false; + } + + return validator(providedPassword, credential); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Infrastructure/Authentication/CryptographyService.cs b/src/NuGetGallery/Infrastructure/Authentication/CryptographyService.cs new file mode 100644 index 0000000000..c439a8e7c9 --- /dev/null +++ b/src/NuGetGallery/Infrastructure/Authentication/CryptographyService.cs @@ -0,0 +1,42 @@ +// 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.IO; +using System.Security.Cryptography; +using System.Web; + +namespace NuGetGallery +{ + public static class CryptographyService + { + public static string GenerateHash( + Stream input, + string hashAlgorithmId = Constants.Sha512HashAlgorithmId) + { + input.Position = 0; + + byte[] hashBytes; + + using (var hashAlgorithm = HashAlgorithm.Create(hashAlgorithmId)) + { + hashBytes = hashAlgorithm.ComputeHash(input); + } + + var hash = Convert.ToBase64String(hashBytes); + return hash; + } + + public static string GenerateToken() + { + var data = new byte[0x10]; + + using (var crypto = new RNGCryptoServiceProvider()) + { + crypto.GetBytes(data); + + return HttpServerUtility.UrlTokenEncode(data); + } + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Infrastructure/Authentication/ICredentialBuilder.cs b/src/NuGetGallery/Infrastructure/Authentication/ICredentialBuilder.cs new file mode 100644 index 0000000000..ed2cbc6144 --- /dev/null +++ b/src/NuGetGallery/Infrastructure/Authentication/ICredentialBuilder.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; + +namespace NuGetGallery.Infrastructure.Authentication +{ + public interface ICredentialBuilder + { + Credential CreatePasswordCredential(string plaintextPassword); + + Credential CreateApiKey(TimeSpan? expiration); + + Credential CreatePackageVerificationApiKey(string id); + + Credential CreateExternalCredential(string issuer, string value, string identity); + } +} diff --git a/src/NuGetGallery.Operations/PackageRegistration.cs b/src/NuGetGallery/Infrastructure/Authentication/ICredentialValidator.cs similarity index 50% rename from src/NuGetGallery.Operations/PackageRegistration.cs rename to src/NuGetGallery/Infrastructure/Authentication/ICredentialValidator.cs index a9f2b3a929..ebe7d77818 100644 --- a/src/NuGetGallery.Operations/PackageRegistration.cs +++ b/src/NuGetGallery/Infrastructure/Authentication/ICredentialValidator.cs @@ -1,11 +1,10 @@ // 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.Operations +namespace NuGetGallery.Infrastructure.Authentication { - public class PackageRegistration + public interface ICredentialValidator { - public string Id { get; set; } - public int Key { get; set; } + bool ValidatePasswordCredential(Credential credential, string providedPassword); } } diff --git a/src/NuGetGallery/Services/CryptographyService.cs b/src/NuGetGallery/Infrastructure/Authentication/LegacyHasher.cs similarity index 51% rename from src/NuGetGallery/Services/CryptographyService.cs rename to src/NuGetGallery/Infrastructure/Authentication/LegacyHasher.cs index bfb9d13852..c141a5a79a 100644 --- a/src/NuGetGallery/Services/CryptographyService.cs +++ b/src/NuGetGallery/Infrastructure/Authentication/LegacyHasher.cs @@ -1,85 +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.IO; using System.Linq; using System.Security.Cryptography; using System.Text; -using System.Web; -using System.Web.Helpers; -namespace NuGetGallery +namespace NuGetGallery.Services.Authentication { - public class CryptographyService + public static class LegacyHasher { private const int SaltLengthInBytes = 16; - public static string GenerateHash( - Stream input, - string hashAlgorithmId = Constants.Sha512HashAlgorithmId) - { - input.Position = 0; - - byte[] hashBytes; - - using (var hashAlgorithm = HashAlgorithm.Create(hashAlgorithmId)) - { - hashBytes = hashAlgorithm.ComputeHash(input); - } - - var hash = Convert.ToBase64String(hashBytes); - return hash; - } - - public static string GenerateSaltedHash( - string input, - string hashAlgorithmId) - { - if (hashAlgorithmId.Equals(Constants.PBKDF2HashAlgorithmId, StringComparison.OrdinalIgnoreCase)) - { - return Crypto.HashPassword(input); - } - - return GenerateLegacySaltedHash(input, hashAlgorithmId); - } - - public static string GenerateToken() - { - var data = new byte[0x10]; - - using (var crypto = new RNGCryptoServiceProvider()) - { - crypto.GetBytes(data); - - return HttpServerUtility.UrlTokenEncode(data); - } - } - - public static bool ValidateHash( - string hash, - byte[] input, - string hashAlgorithmId = Constants.Sha512HashAlgorithmId) - { - using (var tempStream = new MemoryStream(input)) - { - return hash.Equals(GenerateHash(tempStream, hashAlgorithmId)); - } - } - - public static bool ValidateSaltedHash( - string hash, - string input, - string hashAlgorithmId) - { - if (hashAlgorithmId.Equals(Constants.PBKDF2HashAlgorithmId, StringComparison.OrdinalIgnoreCase)) - { - return Crypto.VerifyHashedPassword(hashedPassword: hash, password: input); - } - - return ValidateLegacySaltedHash(hash, input, hashAlgorithmId); - } - - private static string GenerateLegacySaltedHash(string input, string hashAlgorithmId) + public static string GenerateHash(string input, string hashAlgorithmId) { var saltBytes = new byte[SaltLengthInBytes]; @@ -108,7 +41,7 @@ private static string GenerateLegacySaltedHash(string input, string hashAlgorith return saltedHash; } - private static bool ValidateLegacySaltedHash(string hash, string input, string hashAlgorithmId) + public static bool VerifyHash(string hash, string input, string hashAlgorithmId) { var saltPlusHashBytes = Convert.FromBase64String(hash); diff --git a/src/NuGetGallery/Infrastructure/Authentication/V3Hasher.cs b/src/NuGetGallery/Infrastructure/Authentication/V3Hasher.cs new file mode 100644 index 0000000000..e759862a29 --- /dev/null +++ b/src/NuGetGallery/Infrastructure/Authentication/V3Hasher.cs @@ -0,0 +1,161 @@ +// 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.Runtime.CompilerServices; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; + +namespace NuGetGallery.Services.Authentication +{ + /// + /// This code is mostly copied from https://github.com/aspnet/Identity/blob/dev/src/Microsoft.AspNetCore.Identity/PasswordHasher.cs + /// The algorithm: PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. + /// + public static class V3Hasher + { + private static readonly RandomNumberGenerator DefaultRng = RandomNumberGenerator.Create(); // secure PRNG + + private const int IterationCount = 10000; + + /// + /// Returns a hashed representation of the supplied . + /// + /// The string to hash. + /// A hashed representation of the supplied . + public static string GenerateHash(string input) + { + return Convert.ToBase64String( + GenerateHashInternal(input, DefaultRng, + prf: KeyDerivationPrf.HMACSHA256, + iterCount: IterationCount, + saltSize: 128 / 8, + numBytesRequested: 256 / 8)); + } + + private static byte[] GenerateHashInternal(string input, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) + { + // Produce a version 3 (see comment above) text hash. + byte[] salt = new byte[saltSize]; + rng.GetBytes(salt); + byte[] subkey = KeyDerivation.Pbkdf2(input, salt, prf, iterCount, numBytesRequested); + + var outputBytes = new byte[13 + salt.Length + subkey.Length]; + outputBytes[0] = 0x01; // format marker + WriteNetworkByteOrder(outputBytes, 1, (uint)prf); + WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount); + WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize); + Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); + Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length); + return outputBytes; + } + + /// + /// Returns a indicating the result of a hash comparison. + /// + /// The hash value for a user's stored credential. + /// The input supplied for comparison. + /// A indicating the result of a hash comparison. + public static bool VerifyHash(string hashedData, string providedInput) + { + if (hashedData == null) + { + throw new ArgumentNullException(nameof(hashedData)); + } + if (providedInput == null) + { + throw new ArgumentNullException(nameof(providedInput)); + } + + byte[] decodedHashedCredential = Convert.FromBase64String(hashedData); + + // read the format marker from the hashed credential + if (decodedHashedCredential.Length == 0) + { + return false; + } + + // Verify format marker + if (decodedHashedCredential[0] != 0x01) + { + return false; + } + + return VerifyHashInternal(decodedHashedCredential, providedInput); + } + + private static bool VerifyHashInternal(byte[] hashedData, string providedInput) + { + try + { + // Read header information + KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedData, 1); + int iterCount = (int)ReadNetworkByteOrder(hashedData, 5); + int saltLength = (int)ReadNetworkByteOrder(hashedData, 9); + + // Read the salt: must be >= 128 bits + if (saltLength < 128 / 8) + { + return false; + } + byte[] salt = new byte[saltLength]; + Buffer.BlockCopy(hashedData, 13, salt, 0, salt.Length); + + // Read the subkey (the rest of the payload): must be >= 128 bits + int subkeyLength = hashedData.Length - 13 - salt.Length; + if (subkeyLength < 128 / 8) + { + return false; + } + byte[] expectedSubkey = new byte[subkeyLength]; + Buffer.BlockCopy(hashedData, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); + + // Hash the incoming credential and verify it + byte[] actualSubkey = KeyDerivation.Pbkdf2(providedInput, salt, prf, iterCount, subkeyLength); + return ByteArraysEqual(actualSubkey, expectedSubkey); + } + catch + { + // This should never occur except in the case of a malformed payload, where + // we might go off the end of the array. Regardless, a malformed payload + // implies verification failed. + return false; + } + } + + private static uint ReadNetworkByteOrder(byte[] buffer, int offset) + { + return ((uint)(buffer[offset + 0]) << 24) + | ((uint)(buffer[offset + 1]) << 16) + | ((uint)(buffer[offset + 2]) << 8) + | ((uint)(buffer[offset + 3])); + } + private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) + { + buffer[offset + 0] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)(value >> 0); + } + + // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static bool ByteArraysEqual(byte[] a, byte[] b) + { + if (a == null && b == null) + { + return true; + } + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + var areSame = true; + for (var i = 0; i < a.Length; i++) + { + areSame &= (a[i] == b[i]); + } + return areSame; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Infrastructure/CookieTempDataProvider.cs b/src/NuGetGallery/Infrastructure/CookieTempDataProvider.cs index bb2e588b70..da2238c123 100644 --- a/src/NuGetGallery/Infrastructure/CookieTempDataProvider.cs +++ b/src/NuGetGallery/Infrastructure/CookieTempDataProvider.cs @@ -76,6 +76,7 @@ protected virtual void SaveTempData(ControllerContext controllerContext, IDictio { var cookie = new HttpCookie(TempDataCookieKey); cookie.HttpOnly = true; + cookie.Secure = true; foreach (var item in values) { cookie[item.Key] = Convert.ToString(item.Value, CultureInfo.InvariantCulture); diff --git a/src/NuGetGallery/Infrastructure/CredentialBuilder.cs b/src/NuGetGallery/Infrastructure/CredentialBuilder.cs deleted file mode 100644 index a27a369c3c..0000000000 --- a/src/NuGetGallery/Infrastructure/CredentialBuilder.cs +++ /dev/null @@ -1,52 +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 System.Linq; -using System.Web; - -namespace NuGetGallery -{ - /// - /// Provides helper methods to generate credentials. - /// - public static class CredentialBuilder - { - public static Credential CreateV1ApiKey() - { - return CreateV1ApiKey(Guid.NewGuid()); - } - - public static Credential CreateV1ApiKey(Guid apiKey) - { - return CreateV1ApiKey(apiKey.ToString()); - } - - public static Credential CreatePbkdf2Password(string plaintextPassword) - { - return new Credential( - CredentialTypes.Password.Pbkdf2, - CryptographyService.GenerateSaltedHash(plaintextPassword, Constants.PBKDF2HashAlgorithmId)); - } - - public static Credential CreateSha1Password(string plaintextPassword) - { - return new Credential( - CredentialTypes.Password.Sha1, - CryptographyService.GenerateSaltedHash(plaintextPassword, Constants.Sha1HashAlgorithmId)); - } - - internal static Credential CreateV1ApiKey(string apiKey) - { - return new Credential(CredentialTypes.ApiKeyV1, apiKey.ToLowerInvariant()); - } - - internal static Credential CreateExternalCredential(string issuer, string value, string identity) - { - return new Credential(CredentialTypes.ExternalPrefix + issuer, value) - { - Identity = identity - }; - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Model/Database.cs b/src/NuGetGallery/Infrastructure/DateTimeProvider.cs similarity index 53% rename from src/NuGetGallery.Operations/Model/Database.cs rename to src/NuGetGallery/Infrastructure/DateTimeProvider.cs index 642fb6aeb4..ed4780a869 100644 --- a/src/NuGetGallery.Operations/Model/Database.cs +++ b/src/NuGetGallery/Infrastructure/DateTimeProvider.cs @@ -1,11 +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. -namespace NuGetGallery.Operations.Model +using System; + +namespace NuGetGallery { - public class Db + public class DateTimeProvider : IDateTimeProvider { - public string Name { get; set; } - public byte State { get; set; } + public DateTime UtcNow { get { return DateTime.UtcNow; } } } -} +} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Infrastructure/Verbosity.cs b/src/NuGetGallery/Infrastructure/IDateTimeProvider.cs similarity index 62% rename from src/NuGetGallery.Operations/Infrastructure/Verbosity.cs rename to src/NuGetGallery/Infrastructure/IDateTimeProvider.cs index 1b88d11a89..d3b47d0e1d 100644 --- a/src/NuGetGallery.Operations/Infrastructure/Verbosity.cs +++ b/src/NuGetGallery/Infrastructure/IDateTimeProvider.cs @@ -1,12 +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. -namespace NuGetGallery.Operations +using System; + +namespace NuGetGallery { - public enum Verbosity + public interface IDateTimeProvider { - Normal, - Quiet, - Detailed + DateTime UtcNow { get; } } } diff --git a/src/NuGetGallery/Infrastructure/PackageIndexEntity.cs b/src/NuGetGallery/Infrastructure/PackageIndexEntity.cs index 965c6c9513..38c54feda7 100644 --- a/src/NuGetGallery/Infrastructure/PackageIndexEntity.cs +++ b/src/NuGetGallery/Infrastructure/PackageIndexEntity.cs @@ -120,6 +120,7 @@ public Document ToDocument() document.Add(new Field("Copyright", Package.Copyright.ToStringSafe(), Field.Store.YES, Field.Index.NO)); document.Add(new Field("Created", Package.Created.ToString(CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NO)); document.Add(new Field("FlattenedDependencies", Package.FlattenedDependencies.ToStringSafe(), Field.Store.YES, Field.Index.NO)); + document.Add(new Field("FlattenedPackageTypes", Package.FlattenedPackageTypes.ToStringSafe(), Field.Store.YES, Field.Index.NO)); document.Add(new Field("Hash", Package.Hash.ToStringSafe(), Field.Store.YES, Field.Index.NO)); document.Add(new Field("HashAlgorithm", Package.HashAlgorithm.ToStringSafe(), Field.Store.YES, Field.Index.NO)); document.Add(new Field("Id-Original", Package.PackageRegistration.Id, Field.Store.YES, Field.Index.NO)); diff --git a/src/NuGetGallery/Infrastructure/PasswordValidationAttribute.cs b/src/NuGetGallery/Infrastructure/PasswordValidationAttribute.cs new file mode 100644 index 0000000000..23704056ee --- /dev/null +++ b/src/NuGetGallery/Infrastructure/PasswordValidationAttribute.cs @@ -0,0 +1,36 @@ +// 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.ComponentModel.DataAnnotations; +using System.Web.Mvc; +using NuGetGallery.Configuration; + +namespace NuGetGallery.Infrastructure +{ + [AttributeUsage(AttributeTargets.Property)] + public sealed class PasswordValidationAttribute : ValidationAttribute + { + private readonly RegularExpressionAttribute _regexAttribute; + + public PasswordValidationAttribute() + { + var configuration = DependencyResolver.Current.GetService().Current; + + _regexAttribute = new RegularExpressionAttribute(configuration.UserPasswordRegex) + { + ErrorMessage = configuration.UserPasswordHint + }; + } + + public override bool IsValid(object value) + { + return _regexAttribute.IsValid(value); + } + + public override string FormatErrorMessage(string name) + { + return _regexAttribute.FormatErrorMessage(name); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/201605310704169_RemoveOldCredentialColumnsFromUsersTable.Designer.cs b/src/NuGetGallery/Migrations/201605310704169_RemoveOldCredentialColumnsFromUsersTable.Designer.cs new file mode 100644 index 0000000000..d8664a81c5 --- /dev/null +++ b/src/NuGetGallery/Migrations/201605310704169_RemoveOldCredentialColumnsFromUsersTable.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 RemoveOldCredentialColumnsFromUsersTable : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(RemoveOldCredentialColumnsFromUsersTable)); + + string IMigrationMetadata.Id + { + get { return "201605310704169_RemoveOldCredentialColumnsFromUsersTable"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201605310704169_RemoveOldCredentialColumnsFromUsersTable.cs b/src/NuGetGallery/Migrations/201605310704169_RemoveOldCredentialColumnsFromUsersTable.cs new file mode 100644 index 0000000000..5ecdfa7b20 --- /dev/null +++ b/src/NuGetGallery/Migrations/201605310704169_RemoveOldCredentialColumnsFromUsersTable.cs @@ -0,0 +1,24 @@ +using System.Data.Entity.Migrations; + +namespace NuGetGallery.Migrations +{ + /// + /// These were supposed to have run with , which did not happen. + /// + public partial class RemoveOldCredentialColumnsFromUsersTable : DbMigration + { + public override void Up() + { + DropColumn("dbo.Users", "ApiKey"); + DropColumn("dbo.Users", "HashedPassword"); + DropColumn("dbo.Users", "PasswordHashAlgorithm"); + } + + public override void Down() + { + AddColumn("dbo.Users", "PasswordHashAlgorithm", c => c.String()); + AddColumn("dbo.Users", "HashedPassword", c => c.String(maxLength: 256)); + AddColumn("dbo.Users", "ApiKey", c => c.Guid(nullable: false)); + } + } +} diff --git a/src/NuGetGallery/Migrations/201605310704169_RemoveOldCredentialColumnsFromUsersTable.resx b/src/NuGetGallery/Migrations/201605310704169_RemoveOldCredentialColumnsFromUsersTable.resx new file mode 100644 index 0000000000..fa7539a096 --- /dev/null +++ b/src/NuGetGallery/Migrations/201605310704169_RemoveOldCredentialColumnsFromUsersTable.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/Migrations/201606012049351_AddIndexForCredentialsUserKey.Designer.cs b/src/NuGetGallery/Migrations/201606012049351_AddIndexForCredentialsUserKey.Designer.cs new file mode 100644 index 0000000000..2e0d21af0c --- /dev/null +++ b/src/NuGetGallery/Migrations/201606012049351_AddIndexForCredentialsUserKey.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 AddIndexForCredentialsUserKey : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddIndexForCredentialsUserKey)); + + string IMigrationMetadata.Id + { + get { return "201606012049351_AddIndexForCredentialsUserKey"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201606012049351_AddIndexForCredentialsUserKey.cs b/src/NuGetGallery/Migrations/201606012049351_AddIndexForCredentialsUserKey.cs new file mode 100644 index 0000000000..b0fd46db38 --- /dev/null +++ b/src/NuGetGallery/Migrations/201606012049351_AddIndexForCredentialsUserKey.cs @@ -0,0 +1,17 @@ +using System.Data.Entity.Migrations; + +namespace NuGetGallery.Migrations +{ + public partial class AddIndexForCredentialsUserKey : DbMigration + { + public override void Up() + { + Sql("IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'nci_wi_Credentials_UserKey' AND object_id = OBJECT_ID('Credentials')) CREATE NONCLUSTERED INDEX [nci_wi_Credentials_UserKey] ON [dbo].[Credentials] ([UserKey]) INCLUDE ([Identity], [Key], [Type], [Value])"); + } + + public override void Down() + { + Sql("DROP INDEX [nci_wi_Credentials_UserKey] ON [dbo].[Credentials]"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201606012049351_AddIndexForCredentialsUserKey.resx b/src/NuGetGallery/Migrations/201606012049351_AddIndexForCredentialsUserKey.resx new file mode 100644 index 0000000000..034eb51ae2 --- /dev/null +++ b/src/NuGetGallery/Migrations/201606012049351_AddIndexForCredentialsUserKey.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/Migrations/201606012058492_AddIndexForPackageLicenseReportsPackageKey.Designer.cs b/src/NuGetGallery/Migrations/201606012058492_AddIndexForPackageLicenseReportsPackageKey.Designer.cs new file mode 100644 index 0000000000..709af7314e --- /dev/null +++ b/src/NuGetGallery/Migrations/201606012058492_AddIndexForPackageLicenseReportsPackageKey.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 AddIndexForPackageLicenseReportsPackageKey : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddIndexForPackageLicenseReportsPackageKey)); + + string IMigrationMetadata.Id + { + get { return "201606012058492_AddIndexForPackageLicenseReportsPackageKey"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201606012058492_AddIndexForPackageLicenseReportsPackageKey.cs b/src/NuGetGallery/Migrations/201606012058492_AddIndexForPackageLicenseReportsPackageKey.cs new file mode 100644 index 0000000000..3b6f771e0f --- /dev/null +++ b/src/NuGetGallery/Migrations/201606012058492_AddIndexForPackageLicenseReportsPackageKey.cs @@ -0,0 +1,17 @@ +using System.Data.Entity.Migrations; + +namespace NuGetGallery.Migrations +{ + public partial class AddIndexForPackageLicenseReportsPackageKey : DbMigration + { + public override void Up() + { + Sql("IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'nci_wi_PackageLicenseReports_PackageKey' AND object_id = OBJECT_ID('PackageLicenseReports')) CREATE NONCLUSTERED INDEX [nci_wi_PackageLicenseReports_PackageKey] ON [dbo].[PackageLicenseReports] ([PackageKey]) INCLUDE ([Comment], [CreatedUtc], [Key], [ReportUrl], [Sequence])"); + } + + public override void Down() + { + Sql("DROP INDEX [nci_wi_PackageLicenseReports_PackageKey] ON [dbo].[PackageLicenseReports]"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201606012058492_AddIndexForPackageLicenseReportsPackageKey.resx b/src/NuGetGallery/Migrations/201606012058492_AddIndexForPackageLicenseReportsPackageKey.resx new file mode 100644 index 0000000000..ea3ce4ceac --- /dev/null +++ b/src/NuGetGallery/Migrations/201606012058492_AddIndexForPackageLicenseReportsPackageKey.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/Migrations/201606020741056_CredentialExpires.Designer.cs b/src/NuGetGallery/Migrations/201606020741056_CredentialExpires.Designer.cs new file mode 100644 index 0000000000..d170c60aaf --- /dev/null +++ b/src/NuGetGallery/Migrations/201606020741056_CredentialExpires.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 CredentialExpires : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CredentialExpires)); + + string IMigrationMetadata.Id + { + get { return "201606020741056_CredentialExpires"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201606020741056_CredentialExpires.cs b/src/NuGetGallery/Migrations/201606020741056_CredentialExpires.cs new file mode 100644 index 0000000000..dc579ef0ce --- /dev/null +++ b/src/NuGetGallery/Migrations/201606020741056_CredentialExpires.cs @@ -0,0 +1,23 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class CredentialExpires : DbMigration + { + public override void Up() + { + AddColumn("dbo.Credentials", "Created", c => c.DateTime(nullable: false, defaultValueSql: "GETUTCDATE()")); + AddColumn("dbo.Credentials", "Expires", c => c.DateTime()); + + // Set expiration date to 95 days + a random value between 0 and 20 (-> max. 110 days) + Sql("UPDATE [dbo].[Credentials] SET [Created] = GETUTCDATE(), [Expires] = DATEADD(Day, 95 + ABS(CHECKSUM(NewId())) % 20, GETUTCDATE()) WHERE [Type] = 'apikey.v1'"); + } + + public override void Down() + { + DropColumn("dbo.Credentials", "Expires"); + DropColumn("dbo.Credentials", "Created"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201606020741056_CredentialExpires.resx b/src/NuGetGallery/Migrations/201606020741056_CredentialExpires.resx new file mode 100644 index 0000000000..181527b5f8 --- /dev/null +++ b/src/NuGetGallery/Migrations/201606020741056_CredentialExpires.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/Migrations/201607190813558_CredentialDoesNotExpire.Designer.cs b/src/NuGetGallery/Migrations/201607190813558_CredentialDoesNotExpire.Designer.cs new file mode 100644 index 0000000000..142e869843 --- /dev/null +++ b/src/NuGetGallery/Migrations/201607190813558_CredentialDoesNotExpire.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 CredentialDoesNotExpire : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CredentialDoesNotExpire)); + + string IMigrationMetadata.Id + { + get { return "201607190813558_CredentialDoesNotExpire"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201607190813558_CredentialDoesNotExpire.cs b/src/NuGetGallery/Migrations/201607190813558_CredentialDoesNotExpire.cs new file mode 100644 index 0000000000..d0a0c6b6fe --- /dev/null +++ b/src/NuGetGallery/Migrations/201607190813558_CredentialDoesNotExpire.cs @@ -0,0 +1,18 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class CredentialDoesNotExpire : DbMigration + { + public override void Up() + { + // Remove expiration dates on API keys + Sql("UPDATE [dbo].[Credentials] SET [Expires] = NULL WHERE [Type] = 'apikey.v1'"); + } + + public override void Down() + { + } + } +} diff --git a/src/NuGetGallery/Migrations/201607190813558_CredentialDoesNotExpire.resx b/src/NuGetGallery/Migrations/201607190813558_CredentialDoesNotExpire.resx new file mode 100644 index 0000000000..181527b5f8 --- /dev/null +++ b/src/NuGetGallery/Migrations/201607190813558_CredentialDoesNotExpire.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 + + + H4sIAAAAAAAEAO1d3XLcuHK+T1XeYWquklN7NJJ3z9auSzqntLK9R3Vs2eWxt3KnooeQxCyHnCU5tpRUniwXeaS8QgD+4qcbPyTImVHGFy4NATSAxocG0Ohu/O9//8/53x7X8ewryfIoTS7mZyen8xlJVmkYJfcX821x9+ef5n/76z//0/nrcP04+63J9z3LR0sm+cX8oSg2LxeLfPVA1kF+so5WWZqnd8XJKl0vgjBdvDg9/XlxdrYglMSc0prNzj9ukyJak/IH/XmVJiuyKbZB/C4NSZzX32nKsqQ6uwnWJN8EK3Ixv9n+Sopfgzgm2dN8dhlHAW3DksR381mQJGkRFLSFLz/nZFlkaXK/3NAPQfzpaUNovrsgzknd8pdddttOnL5gnVh0BRtSq21epGtHgmff11xZyMV78Xbeco3y7TXlb/HEel3y7mJ+lZGQJIwV85lc3curOGNZReaedEW+m4kJaUa+a8Hw/cnP9F/13+l3s6ttXGwzcpGQbZGxoh+2X+Jo9Q/y9Cn9nSQXyTaO+abSxtI04QP99CFLNyQrnj6Su7oDNNN8thALLuSSbTm+UNW166T4/sWcdiSOgy8xabHAsWFZ0I79ShKSBQUJPwRFQTI6lNclFwq1eqkyirnMokI9EVa2oUARTKfhfPYueHxLkvvi4WL+4w/z2ZvokYTNh5ro5ySik5aWKbItca70tyDe6mp98Zcfx6i2Y+zgmvUVUSSzEW3qeUV/fKLipxcartL1ZlsS09f5+nETZSRX65SK3QRfo/uyRgBO89lHEpeJ+UO0qeQcNy9vqzxvsnT9MY2FWV4m3S7TbbZiI5vC6Z+C7J4UYpvOF53w0IqUqnJLYcIyH8WIjRh5vQ6i+DIMKXjy0WfGZ7rWJ3dRtibhtPVSOCTBenpRV/UyjtNvnTz4JaWTI0icad2kRXT39CFY/R7ckw/b/GE4ybJ5V9WQlGgq0a7hEgPj4NH4EOT5tzQLP5KcFDuqsZSXZZeZqDSKTVjCfy5W/QVuJxzzoXK3kauY3G3ksm3T3tE5STEGt6uk2OXoWiUkKGuBmAqtBLoWMRqa5tTJUlvKr3BDqqRB61E5d+oOWa9LfKHj+mSzPv2ShrodG/3Tg3xgwIH30/pyS9qHoTJ4uf3y72RVjN7HT2m/EwM6JxumgdOSB/ptl7OboWAGZbLCuVylR9X1IdJMlrCwrOslRxhBa/nBMh/lho3cuNFv93rOKe3xqecCBWJLXLt6AetqW3LuDWH7RFslTFfmCLM9hNm7IKESB0EaN3i3XUZu2wikq2d2KJOrwK0PKeZmdhnhZjbp2ma2mQbt52qKNTHXGVMXO04am0nDDd5gDWbN+I/kPsqL6kQ3mObllo18Ea3oMD7VjR26zbtOVvE29KIFIDqFiW+JI6wivmazcmDVTXlHocMjQdfiOvstWExpvyY3Jpt0RQZJKrDFluIKKHuUWTYy6zrUTLqzFz+NoT18lX5L4jQIr9Jtd84cemx7/y3BNhAQUpvs3YzAcykzQZPV65YCqgcSRrp8Vo33s9Vw3WMcNxcOE3WMDcFVunnKovuH8bU0u7g7fEXyVRZtqnVk5P7RyUuCnIy0i/EoP5X7VcbTIK7R9TmLR2/934P84TK+T7OoeFjrFp9TT5Xt4Ep+lSZTsPI6f0tnRj5YV9vQWRas0FBqb4O8+LwJJ5/vrN7XYQRWaygZrUiST4P+KCR1dR/JJs0Gj93bILnflqsujnIfM6lcaPnL2UnGtJZLb6KYLKP/IJywYxfbrmfqLGVXElMM80fyx5YZrtRDfblilnlBsho8uZbb9TrIxr86+hTcj7+QfYqKWAtcL1YS9VZyciOJmzRbBzFFbeitBVZirDTzHH3sBCE2xZR6S/e9HvRV+YeMZNVubSitNzETZwkJL7fFQ5qNz/O2wldkQxJ6cFhFE4z0uyi5iiN6SjHD+AcfMO5zY/2KjmgfdKDn8nZI8WP5bZtHOYk3Sdjhu013VReIA69pm5hRbSCfjrZSyOTaVEFAaBsr5LxttQhKo8F8itJVm7mn8pVtK7VdYBk0LeeTsQYLeXq28+9URtKDlR4bVaYnTXOlHFiL5Wxjaba9qMGQPuh1ZrY9WW43DGYkfJPR39/S7HftCLS5NGOg5MF6oGZ0bT1qRdIQlI1c+O+o8BhuGl1TqmSlq1axKnXULTroFgfrEye2Gmgnz7A1GplXyho+BMPtSvrkiuOu5BHLU2LZy8WY1cF0uSGr0Q/BFcjbtcJ/fYNmqdNuFZuv4JZ2yKSV9HRu81YofJy6U05dnRuDq7qN/LElnNquX4NslCQjXUVcpes1Z809wXyvge9w4utKGI58TUZsz4nk7nknP96R1ar58rbfgyTrKcOO0suDyawvWxrcl8hV0eI+7axULcokHQJcphRxRS0rc4TslAuuH599uj7nRbDeDF60P2UR8WEcUV4oZxnTPTyLi7CprimmMyua0sRnKqOOCY0RJr0Qn9BA6nj77v2I7nLBgWxvwUsQXzrqkjiip27TtC3zpq+ur0Vcty11sePOZfc7l8k2Ksdl/7jsH5f947I/pdGdJ5PoPTLy9mok6ma97M9m1uNezd3AA9kXYXYgvjZtDX1k38Ynm5robffGXZG57d/agscdnM0Obq8vJPsZ6CAQxS15euG0xtWSFEXJKUuUisWOGLVS6ZPHAnTW8LQ4u8qm0seU7XJKnx838cSXPY7+rjwub8i3ciAGE6pHkqLQDz0+WuIVheskm3XaAThMYe/dUcNfnXDnZ8JtV0AR8GA+TMjDmZ3jAYqjat0LpZy+M1J2qz7JZXzstSqrfXeLOFbqKMNsZFjtF/E+GXyWqSn98uRBdAW5F8WN6yk+uk8ChoMJqtY4cNRs1M3tKtMtl1eZz3IWbA4r+cYIUFFXorHJl3IYWqsY7dsImcs8T1dR2cK66XCwRrHfr5NwZhW5sUKMHAOSgofKlmhDpQlt0sX8TwpjTRW0NhVdBdVxWCR8enJyptCmkohkVURcun6zTUqUFKrYipJVtAlim2ZIhW2lHhuVth45pbHLLGwYbdMAIayp2pC2Pkkwm7h1vuAgpEeWGF4TG3AkcrB+oM9kFpy/T6ppMbtcsSbQvVqQr4JQFTp0YoR2bQFA54bqXsgD+TEF4sDO21TMBZfdHc6qIJvagZVCQusRppNRUPzOjloV01VLzaFzciRwrE3ocwxdw/h3Wew7i8Ybn3CG9ppISMOnmErIYNhUvdupBEYRRZGhDSnK4Y6PIugAPF0wUrfZ68AByOOxDoOGNRQvAnED9Oe054qmsql407qLGdqo+ncrXJhoXUed2ZQGNW6UoyztCF+mkEgIB2yq5q1IdiKUQP8n01DDrvu7BiDsoqW0ineEHBWJEJemhCPEj0PCJOIrYICByV9HwYN0veK8Wpg8FbAKvS8dsEeQU+uVe8SRuaUEFphWhgyZ3tqOTDjNtQN4SNNdsO81oAg29lXAU/nuOGMUjpVyONCE2j8hIqHBOTggavUBuFW3Vwi6qQR87l2U6qeGz2Gd7BGrN8MYoyZwCohae35nHKFxlA5HmiFdmBCRyEAdkkwT7BwtMWOQbMNBuZvbJ00jdoCpgxR0qnGiYcQ1looKrjiDTGdkaUKWGQWeirAeahSb+WWaWD36vduptKs5dJiTBw4i6KKSVm0YnDTg46rhDLEPe6B9CDS1fJsQqlquOGwkFFPQXd5kNdMQwhyGEpdXn5Rbrh4C0uHJqB3Oon53sNZdmwLo9gN7qHAXX2IzAA55ZNHq4nYkAa1/MK7/ZBsCXpBNE8IVZIZN/fJLi7vcUcDm5IZ12mBbrshC0fPDeYeqt07fe3sXm15MuJ3QDp5NOwQvkL3BruxE4IIp1KNgVCRjPgkHCWikM7vCNTKiNs2BfJN2iXLFnN6AMdy2HjBnqDxYnHGM2uUfCnaxDkyIV2ycbJogurPsATptdRGoJ4UlNkH9k7GW3hoElB+VEwflYUE5SLLGZYN9jUjOvpNHyMmVToraOyyvnXHkrjC6S1IoVsb5fNZ5jgCGwwpPREJsNkIkqllqKMxb10NEROt7A7HKIFwlUplnGwpzW2eQIfzByI5Ui1yUWgsVA0Hg3AlRBfUDdqQ15GxJtOGUMEKNoaUdOTH0OUaTN5uzoys/v4NRlkyKnIibydoSrB/YwahV9/h2pLg3cDBy7eWZHUX+UReMJHdvYiAqBmKASMoRHuxaye/eNA0V99+2GGWrgRae1VIjkeNEPywKO6+8GZcXkImI+56wdOkd+NquKcJYWfxtXfY4kvXiIO+4RAZYMEd0LAOYovE8E1oO+56ZWqwhAXRay8e+na8XN6TngC+U2mbRG8q9z6L/E1e+btvgjsqeT0Bvtc5RomIRcY/i2i3sgTS9xxyiRgA66I4D8cHotoNqWRXHHZ4jwi5IxxKdq84IfIE097WTDsAdTW68S3ghiFPwnkzDMQ358fnWvfeEMgv28YG6oHj5qGyxYoXinKPSaZvtjRHivhLnBu56AnYFdD7pyxfQZ0QlJnbFF4ewxxBQVtl4REDdNPhEqP2Vt+5mThq8INAqRmNne6ay5CZoyWTuqWzP5J+XsiWTBdT781AMso2yDrfVh/oDWuur3ahPYGb+gPb547MF2ygpeew6gG2VenFj7M2SeGy1QIjWFhrqBmYNrXKGO16buYMZQI8KFzGqqJFHdqABzXI9cWci+ABBLVHmGGxMod7gVqYqm3iNiplPuF3pqDgyAcgSOSbI2G2VJsII8say3ckDvsIwHg6Uqwzfpw/lFmMc2FjY5eEHXF0p4+lUU1hz+LXCn4MFn+OwDVQa6MBpNhlDz/g6MPZSGujAZxiI/tMYCXGJTmMLGyajMYZixaTiQVJRm6ex3m5pPDGoD65px0atOY2xs5hBjW+mYiY04/FWDW6I8lNvuOFkugFpFepLDTPHUGON0blksQRr7QdcLAh8sMjzUtvEemzNBNq088Vy9UDWQf3hfEGzsOc+tkH8Lg1JnDcJ74LNht2zdSXrL7PlJlgxYf3n5Xz2uI6T/GL+UBSbl4tFXpLOT9bRKkvz9K44WaXrRRCmixenpz8vzs4W64rGYiXwWzZqaGui23x2VhRTWdDlkLyJsjIKcvAlYNelV+FaySYbRSB3dk1tqt2DOnrNNV5Thv1d2yYKUXU78wjVqKYuS3fn9+yJ4bKXBLx1UAvT4stVEAcZFBz3Ko2360RjKoSXb32MeBqo4xFOh/FDJFJ9safwWxBvJRL1J3saXZRfngwW+1dHqX4SWyTUfrSn8/pxw17YEem0H1U65wsJJTIUFwoWJdkgw9sK/JUw7g176GBnAXi42FhQL29fL8OQcl4eDyHFYfIkqyoKPAlx4mgmt0maBGuiztLqqysP4jj9JmNbTLGneJMW0d1TvVR92FaP9/CEwQyOLeaD7ZcRyIG2A3nsa/kQ5Pm3NAs/kpwUQA1Q+hDqpQQom1rF89fXJed2FmOfixUoycrveyOERPOI3sJIMA10F0r64mMJp1/SUCJQfbGnIAR55glpoz/j9JaEPQfNE6q+OFDYfmHP5klEmo8Oe4sU7Jcm1vDOIIyZ21hCtzRIdYcsXGwsqN4oK9ENsgrtaBAEtVP/rTxn2NtjL68r/f99ZDRHb7fBQe5a7McHJTDWEMmOmMKibHDSxKkC6mOFOpbHvpbLLfMsKCKaFj/VjRXrgHM4nN+SVbwNZardV6dtqXzqqj/tzWQAdf69Z4TFPYjFtLCiMtbcuJbH3WnEX6XfkjgNwqt0K29cpKR9Q8DwUe8/0tON7jQyintTWxCt3efpdUDC69sCLvkEe3riG9E8QTFlmrmDa77Yy1tBXI9p+Xi2qART0+2pS+8l84SlJDeaKiknCda8fS6IseajA538LQVZLg1F99Wd0rJgr2/B9Jo0e6rCm888SSHBjR4zDIPINd8dqHEvwwvUuO8OqIhC2edLgIia7NLv5H5bGhaJvW6+OkjX7uVsQaB2n50ldfc8OCCiu0QHulnKjv7KuPDfXaQg+ni9KBLRbC6ajPo9e1GTUX900GSU79ULSozyiwOFqJCncf3J4aalevRRumtpPrrssbN1EFMQhCBFINl5DrMfOTiL6xRnitUkxWQDl+pCOVclV/3NRVJ/yEhWLeCynOZTHDSCMXv9MiFh6ywhqAWV1B6URU8DkLzOGUFXx7souYojunUFwaWmTn/nWZs3yHu6+uO+nTJwLx63s0btrd37xIGVH/ncgZ01npuKrTUv0XopuQ0550zfe9h1NA5j6IepJ2o5tdyQFbjwVgkuWwlmL8R50Yu7Cilx3+ApeRkNBai4Ae+NUQOZw4Bpvytf/Bbwjy1RNtXdV5f9Orjx6rXjukrXa+V6sv24p2D3BvPBAD/eSMlDVLmvDR2fMupK78GBSx+G0PFmRhjRU10RrDfyEbf97EAriwik0eS/O6qosozFKVI0VNXnaQ/x4Jmux1HOp8Lct6LblzrVr0LQtxrLtzL/eSvGdrtOdO6qQ9eKJqRW7+UCJXBcMXqvGEe5bEXvKJePcnm6S9UxrnrHuuLyfTHa51pvtyskF6lg6BLZ6a96L5IaEmMtkweqmJOjbPYePCkYp/vQmQiMpqsgj4Xmoh9I3pvBAz2bh04/IfBp7xmopzLyXnVkYzPhWQoRK5r3KnB60DsA8tbA9E4ATp13kGKOtPLGUk51brfqyiQk7NtsaZy1h9+OlbF8B9yMweXHmhv1DfV75SzQfnamVb8KAJBD3gvQYSnI5WNK881h/xvdJ0GxzeR7i+7z9GgUnfZV/9smkq/Gx7bJAjnSQk5ALOwAcO4WA/6qjLCDneZ5Sq5apxahT2LYtYgRcWyRHEvBeeTguLxmnyIus8Z3CIuYhXuVKFF8+zLT/GSY0yjr4gJPiMDB4w0FdGriDdu7ZrRFLBwwLDGA1zIUCbYbJUdEaBp8uLjAguQ62C9wpWzsFPRXuzjf9VF2e4vgkpxHeOgj9fZtZk1nRyABAktaakK6Aq4KD5zDaLTIgfLi1iMK0FiZfdvo3jZ6QAqj8jWq6/xmG8cX87sglk11NV33BR4l0JXN6YTLbukIho8FEg5rIFpqqh4xg0TY2k/E4N0340YJ+iVnaU829Zf2dxv0qw64JUQCKznD4nqVHMnr4F9yBK4qy3xGmfA1Cln0reVTXpD1Cctwsvwjrsy0uwx08xfdkbyKbHIxf3F69mI+u4yjIK/CtNWxxV7KD5lZBRs7+54FGyPheiEXdw9ZxqjkeSgE4+KUA525HRi065wOpQyIBijah/3OF3LJcwia1ZtCEeNsOaN/JXTgK5f2gjnW8WGvGPiYi1cLwIWWfrt94upQnpW7TkLyeDH/z7LMy9n1v93Wxb6bvc/oML+cnc7+y7nqKmhYVW/yNchWDwF70TF4fEuS++LhYv7jD8406yhiGqIv/vKjM9WOu/aEy9f3DHRb/9KKLLsxKqI1QUf5Kl1vtmUBxw60QcjkinTt5XUg2mmhHloOeEKIcbv8jjcaHsxzNW2wML+TSwwbVtH+wt6ec6QDRgnrTw6LCKbp/PcvXHkKBQUbuQI5EpjL3EVlTWkSPooUwGNpHbA0qAJziQP9L+vg8V9d2S8E5nJba7mi2Hpr04Iqolf/adYG8/LADS6clxsv2oIOOw9rBKta9QNG7g2wAFiMlTWz0KBXR56ZeAaewg+YbfLtgduUFksPOlFgymu3BsFUBjUMjpXVXxZ3UbIG7cKIsvX0iXZjfKkDhvx1qN3+nb34yZmkFHyHa7yf1e25SZ29neyc2baHrdJkOgLBPtxDw0XTaQ8Ee04QSBuiRqPy0D7JQFknHk770Pav1mqs+D10votXNWBVk2JU9ackWGCPPXH4wFXDzui8L4QPQKrhqoawtAlUpUOhM7A56/axx0kx9W+Ycd9HiPCeJl6kJeoJMuTIXjuB+Diyl74gPghVXlZ+VZ5tZCC/Gk8grJV1BQ4Tvrxz88JcNbSVF6L50POKENWqPx01jpUP1RwcvcoDZTVwlQY9PzijZ5y7O6utWBP4qv9QAnfhvXbuHQH7DrmemaAwVod/curP86EnpLH1bIYwVMeh6z90booPh6W7ionld1egeOANIe+KPk2AqSMAB2hXrO4x7bbHbWSr/roEdKc1+HTehrjaAWifDVyhlcZRU+vKQjW00wHzb+fTfZf2aV1YkYGCho9C1V/UcOGn9vUY7vNY5leN7lu77VN36lnt510z5VmP/1xVXa4LBRjU6bhW7NtaYSdMvS0VR7F8FMtHsQxODV83EK6Xqbu+/PV7caW9G3UV+fjtndczFeLpeMCLpVfVFAqZAXr1kTTquoBPBzycQMCo0fePeLSlA2akhVWX89Bw8ZncpgJXdNDGFIrl5NYSlcJAEzU5AJSnDUQbAGrU5QCKonTAmOeiMQ1cj8VATG4Q48sORHsVwckAKbv9YxfEyZ2ckwNjX+cPPP6ImheJWWRG4A61oG2bHQVWVWwMNx1tyCS7QTPGO7IeZ/PY7Y13xGQgGuIh0HdEreMWeRzZvbVA39uRtg5mZDfougBEam48DpB5qLmaXDdudcFBA8o33a3+rqSHYeWCk9QN451+W0dVaShfJ+GMLQd1oIC6Ayz8x0n14d02LqJNHK1olRfz05OTM4UfHQ3BzZinJSaINP+kEKSwIFkVy4NuhtnEi9QgkB+yKFlFmyDm2y9lst1mMq629OSUxlKpgDppU6HgXqxW3NKXYG/ighCPRo+EMrJj3eR8GALOlPhE75Nqdzq7XFXhYa6CfBWogWvLED1H5Lggh3PF3h1u8Bile4Kd9oQg1199PGzMYBFlHc87E6MFCvHFjZgyWtBIPSO0WA/gpGjRROmdAC1dvK7b4fuSkbDCxRTjW8B/PmzpggVNQ6rbrXyxDq7MjyAXCUMYQv77NFiSNCNIayB0e0IVGhVkLHDpw2f3U/7sDGiGJW2nYmrn0JpOYPXA1G6llmM4cG5YoRgd/NCC6ZMADtMNGlo3IgCN8UxGwqNlmPhhatEdw3SPJd/+AXEySTgEeXshEW8by0fD4EKDOS28ahdToB1NypgybUo0Qc60SI2612YnhZHgGb73WOL8XoG28KnPBVOYp++e48rhWQ51lEFzM36gpQxTIlB3z2dqqZx9TIxqfHTHBazDqy6wXk339MrOAWzYzVkD4ghaDWgPCa76J3h2hVc4IuIeLujDRP3hLuvu4nlfVnbmBH44+Cpd1oFWVN+fC5pUx/xDAJGPiyvcmmZ3AJhaiWE9+nuhuKh9gw9HhDTOzEBD2qTnIkhAx22kyn2RJQ2gRrfP2zUeppYrLmDYC9Givi/pQbjYgUJxd+RJconPRVCgD3TqRAX2LOKE6s6phIQNsg5ULByOPBCu4+pvRiX3/l4YT4ipHV8IH94FsPj+DeT5coC4kx71AQxjnicKdY8ZHRAYSwsso9jbOxu/ncFtRxZ+LjDbCwM/IEhDG09gn+1chKgSgIQV05/FXg0PpIHUK4SU2Bt0STEijiA7bJBBQUN2ibU60H4bJmKf8VUHCAGtX6qUZ4EpKA4KUqMYGWQPcGR70nQdyr3TPLiP1Q7Oh/wzHBOAo3Ivp2UKWoJkbVSikLyJsjKEUPAlUGICVKWWpFDcu+azzmEdcKBarh7IOriYh19SOvCV23uXngNwESuqBJhSRfUZIs5SzGRFv2OFvJgMVcPnMFdXOR8q1VSfIfKln6WRrHAEUoeBTwXHoctgXVc7YbHq2gyaGlsBZKoUVC0oNYO5oOqBjNZtwOvV1mVNv7E1xmpp0jV1tbFm7WrkrVGxWvk8mprF55vsqpcMZ7AWSNk0jRByujbD2ACbqq0rrS73sRqrVE11LIN1Xe2dH1Zdm0FTY5XHYXS5KyWsXi6LpuY2l7lqKcClWrGcAapWzGPdX/HYhHVZzKXpNZ/RYUJXuzJ8Mlfp2onMstgty8ii1iVhy7Pz4oZsApQchkWucfbss+wgTUBzWi4/lQ9WL5HpKL/gQrbiFBVw3C4T3l11oYlmXF5gn4XEMALOty13mg/KtrkrAW30ypJignzOFbtl0WUxBg/QVU2QnkPqohAuBusmHlNmYFdlsdOWqj767GJtSK/vImRtLzRYaSzU0Em7KAfmADqojd0xcATVg2NZjv88vIuaSBJQd20DT+DXUnxP+O86RsDLm0wDYqgvluAQtw6RMBQNkzIBsroQffUBVpgL4d3THF/LLoLpGnYZ9iUYzfHZhyPJXMgnnvaCQa3nM84O2Dka1paqTbdhgahO4Es3Kd66Kx7+8T7jnrw+O65qNXgKfKovBuhdSXGGOLigQh0F9Sd8X6UMZtZZHC9Q+nL2kZlrFDgO7pEaVmi6/TwY2k4sS0Yi9xv+pu8wVPdni+A1hnMDdy7zyQRePciXrb577TJ2BlDyjLBMT9NN2Z8H76zW88fn+EoqWb54m+S7+4aBxp1U/Iz1lF1W3SzwfhtcMnyOuqIS5wlwid42ZoYhH22sbRjk6fzR3bLZHT3a/JMf2kZiioVdN36+15WajkHwzTKvA/DOLtHy2KAAscCMJ2XQJCwAbpg661h8GllY0/qRHtBNGo8rMX0UdsjmnJZc0VqBHjJzFPtDnCF6U0U/TBDvOcXjfZXiueMWq4zWtM5HJ0ZYU5q3RlpjsDbtfFFdDNYf6E+6Q6PE36UhifPy6/ni4zZhL6VVv16RPLrvSJxTmglZCcZnbZ7r5C5tzOGkFjVZpEdY3pEiCIMiuMyK6C5YFTR5RfK8vMT/LYi35S3YFxJeJ++3xWZb0C6T9ZdY2FMyWzpd/ecLpc3n78vnkXMfXaDNjNjjcu+TX7ZRHLbtfgO8AYOQYEZ69dt3bCzZ2krun1pKN2liSahmX2tb+ImsNzEllr9PlsFX0qdtdO6+JffB6ol+/xqFbCJjRMwDIbL9/FUU3NPtcV7T6MrTnxTD4frxr/8HHqVwhu2qAQA= + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/201607190842411_CredentialLastUsed.Designer.cs b/src/NuGetGallery/Migrations/201607190842411_CredentialLastUsed.Designer.cs new file mode 100644 index 0000000000..8c857f61b4 --- /dev/null +++ b/src/NuGetGallery/Migrations/201607190842411_CredentialLastUsed.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 CredentialLastUsed : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CredentialLastUsed)); + + string IMigrationMetadata.Id + { + get { return "201607190842411_CredentialLastUsed"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201607190842411_CredentialLastUsed.cs b/src/NuGetGallery/Migrations/201607190842411_CredentialLastUsed.cs new file mode 100644 index 0000000000..c5a0ddcb23 --- /dev/null +++ b/src/NuGetGallery/Migrations/201607190842411_CredentialLastUsed.cs @@ -0,0 +1,18 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class CredentialLastUsed : DbMigration + { + public override void Up() + { + AddColumn("dbo.Credentials", "LastUsed", c => c.DateTime()); + } + + public override void Down() + { + DropColumn("dbo.Credentials", "LastUsed"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201607190842411_CredentialLastUsed.resx b/src/NuGetGallery/Migrations/201607190842411_CredentialLastUsed.resx new file mode 100644 index 0000000000..ac4ba4a4c4 --- /dev/null +++ b/src/NuGetGallery/Migrations/201607190842411_CredentialLastUsed.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/Migrations/201608251939567_AddPackageTypes.Designer.cs b/src/NuGetGallery/Migrations/201608251939567_AddPackageTypes.Designer.cs new file mode 100644 index 0000000000..bdb7e409fb --- /dev/null +++ b/src/NuGetGallery/Migrations/201608251939567_AddPackageTypes.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 AddPackageTypes : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddPackageTypes)); + + string IMigrationMetadata.Id + { + get { return "201608251939567_AddPackageTypes"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201608251939567_AddPackageTypes.cs b/src/NuGetGallery/Migrations/201608251939567_AddPackageTypes.cs new file mode 100644 index 0000000000..07141f03e3 --- /dev/null +++ b/src/NuGetGallery/Migrations/201608251939567_AddPackageTypes.cs @@ -0,0 +1,34 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddPackageTypes : DbMigration + { + public override void Up() + { + CreateTable( + "dbo.PackageTypes", + c => new + { + Key = c.Int(nullable: false, identity: true), + PackageKey = c.Int(nullable: false), + Name = c.String(maxLength: 512), + Version = c.String(maxLength: 128), + }) + .PrimaryKey(t => t.Key) + .ForeignKey("dbo.Packages", t => t.PackageKey, cascadeDelete: true) + .Index(t => t.PackageKey); + + AddColumn("dbo.Packages", "FlattenedPackageTypes", c => c.String()); + } + + public override void Down() + { + DropForeignKey("dbo.PackageTypes", "PackageKey", "dbo.Packages"); + DropIndex("dbo.PackageTypes", new[] { "PackageKey" }); + DropColumn("dbo.Packages", "FlattenedPackageTypes"); + DropTable("dbo.PackageTypes"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201608251939567_AddPackageTypes.resx b/src/NuGetGallery/Migrations/201608251939567_AddPackageTypes.resx new file mode 100644 index 0000000000..f0d5745fbf --- /dev/null +++ b/src/NuGetGallery/Migrations/201608251939567_AddPackageTypes.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/Migrations/201609092252096_AddIndexCredentialExpires.Designer.cs b/src/NuGetGallery/Migrations/201609092252096_AddIndexCredentialExpires.Designer.cs new file mode 100644 index 0000000000..baab6cef8c --- /dev/null +++ b/src/NuGetGallery/Migrations/201609092252096_AddIndexCredentialExpires.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 AddIndexCredentialExpires : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddIndexCredentialExpires)); + + string IMigrationMetadata.Id + { + get { return "201609092252096_AddIndexCredentialExpires"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201609092252096_AddIndexCredentialExpires.cs b/src/NuGetGallery/Migrations/201609092252096_AddIndexCredentialExpires.cs new file mode 100644 index 0000000000..e3d199c1e0 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609092252096_AddIndexCredentialExpires.cs @@ -0,0 +1,17 @@ +namespace NuGetGallery.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class AddIndexCredentialExpires : DbMigration + { + public override void Up() + { + Sql("IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'nci_wi_Credentials_Type_Expires' AND object_id = OBJECT_ID('Credentials')) CREATE NONCLUSTERED INDEX [nci_wi_Credentials_Type_Expires] ON [dbo].[Credentials] ([Type], [Expires]) INCLUDE ([Created], [UserKey])"); + } + + public override void Down() + { + Sql("DROP INDEX [nci_wi_Credentials_Type_Expires] ON [dbo].[Credentials]"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201609092252096_AddIndexCredentialExpires.resx b/src/NuGetGallery/Migrations/201609092252096_AddIndexCredentialExpires.resx new file mode 100644 index 0000000000..adca771c81 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609092252096_AddIndexCredentialExpires.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/Migrations/201609092255576_AddIndexUsersEmail.Designer.cs b/src/NuGetGallery/Migrations/201609092255576_AddIndexUsersEmail.Designer.cs new file mode 100644 index 0000000000..a9fd1ed13f --- /dev/null +++ b/src/NuGetGallery/Migrations/201609092255576_AddIndexUsersEmail.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 AddIndexUsersEmail : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddIndexUsersEmail)); + + string IMigrationMetadata.Id + { + get { return "201609092255576_AddIndexUsersEmail"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201609092255576_AddIndexUsersEmail.cs b/src/NuGetGallery/Migrations/201609092255576_AddIndexUsersEmail.cs new file mode 100644 index 0000000000..747f839bab --- /dev/null +++ b/src/NuGetGallery/Migrations/201609092255576_AddIndexUsersEmail.cs @@ -0,0 +1,17 @@ +namespace NuGetGallery.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class AddIndexUsersEmail : DbMigration + { + public override void Up() + { + Sql("IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'nci_wi_Users_EmailAllowed' AND object_id = OBJECT_ID('Users')) CREATE NONCLUSTERED INDEX [nci_wi_Users_EmailAllowed] ON [dbo].[Users] ([EmailAllowed], [EmailAddress]) INCLUDE ([Key], [Username])"); + } + + public override void Down() + { + Sql("DROP INDEX [nci_wi_Users_EmailAllowed] ON [dbo].[Users]"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201609092255576_AddIndexUsersEmail.resx b/src/NuGetGallery/Migrations/201609092255576_AddIndexUsersEmail.resx new file mode 100644 index 0000000000..adca771c81 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609092255576_AddIndexUsersEmail.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 + + + H4sIAAAAAAAEAO1d3XLcuHK+T1XeYWquklN7NJJ3z9Yel3ROaWV71xVbVnnsrdyp6CEkMcshZ0mOLZ1UniwXeaS8QgD+4qcbPyTImVGmdKMhgAbQ+NAAGt2N//3v/zn/++M6nn0lWR6lycX87OR0PiPJKg2j5P5ivi3u/vzT/O9/++d/On8drh9nvzX5vmf5aMkkv5g/FMXm5WKRrx7IOshP1tEqS/P0rjhZpetFEKaLF6enf12cnS0IJTGntGaz84/bpIjWpPxBf16lyYpsim0Qv09DEuf1d5qyLKnOroM1yTfBilzMr7e/kOKXII5J9jSfXcZRQNuwJPHdfBYkSVoEBW3hy885WRZZmtwvN/RDEH962hCa7y6Ic1K3/GWX3bYTpy9YJxZdwYbUapsX6dqR4Nn3NVcWcvFevJ23XKN8e035WzyxXpe8u5hfZSQkCWPFfCZX9/IqzlhWkbknXZHvZmJCmpHvWjCcnpR/382utnGxzchFQrZFxgrdbL/E0erfyNOn9HeSXCTbOOYbSZtJ04QP9NNNlm5IVjx9JHd102mm+WwhFlzIJdtyfKGqU2+T4vsXc9qFOA6+xKRFAceAZUG79AtJSBYUJLwJioJkdBDflv0v1OqlyijaMosK9URY2YYCxS6dgPPZ++DxHUnui4eL+Y8/zGdvokcSNh9qop+TiE5XWqbItsS50t+CeKur9cVffhyj2o6xg2vWV0QxzEa0qecV/fGJCp5eaLhK15ttSUxf5+vHTZSRXK1TX+xdkBcUSEBbpXLXwdfovmwpAMP57COJy8T8IdpUkpGbybdVnjdZuv6YxoJcKJNul+k2WzFEpHD6pyC7J4XYpvNFJ260Qqiq3FL8sMxHwWOA2jqI4sswpHDLR59Ln+m+ILmLsjUJp62XAiEJ1tMLx6qXcZx+62blzymdFkHiTOs6LaK7p5tg9XtwT262+cNwkmXzrqohKdFUol3DJQbGwaNxE+T5tzQLP5KcFDuqsZSwZZeZkHQVtPWa8LlY9Re1nVjMh0rcRqJiEreRyLZNe0/nJMUY3K6SYpeja5WQoKwCYiq0BuhaxGhomlMnS20pv8INqZIGrUTl3Kk7ZL0i8YWOK5N+kv2chrrdHf3Xg2RgkIH33vpyS9qHodJ3uf3yH2RVjN7HT2m/0wU6GxumgROSh/htl7Obm2AGZZrCuVzlRtX1IXJMlq2wlOslQRhBa8nBMh8lhmGHpN/i9ZxN2sNSz0UJRJW4XvWC1NW25NwbwvaGtkqarswRYHsFsPdBQqUMgjFu2G67jNwmEUhXz+ZQJlchWx9JzM3sMsLNbNK1zWwzDdq91RRrYq5zpS52nC6GY1I3bIN1mzXLP5L7KC+qk9tgmpdbNuZFtKID+FQ3duim7m2yirehl9M+0SlGfMsaYeXwNY+Vg6lusjuKGx4JuhbX2W/BYkr7NbkxqaQrMkhGgS22FFRA2aO0Mt0oaKbb2YufxtAPvkq/JXEahFfptjtPDj2effiWYJsGCKNN9m4u4LmUOaDJ6nUbAdUDiSFdPqvG+9leuO4rjhsKqyk6xibgKt08ZdH9w/h6mF3cJL4i+SqLNtXaMXL/6LQlQU5G2rl4lJzKbSvjaRDX6PqcxaO3/tcgf7iM79MsKh7WumXn1FNlO7igX6XJFKx8m7+jMyMfrI1t6CwLVmgotfIqfhNOPt9Zva/DCKzWUDJakSSfBv1RSOrqPpJNmg0eu3dBcr8t11sc5T5mUrnQ8hevk4xpLZfeRDFZRv8gnLBjl9au5+gsZZcOUwzzR/LHlpmx1EN9uWIWekGyGjy5ltv1OsjGvxz6FNyPv5B9iopYC1wvFhD1JnJyA4jrNFsHMUVt6K0FVmKsNPccfewEITbFlHpH970edFT5TUayarc2lNabmImzhISX2+IhzcbneVvhK7IhCT04rKIJRrqttRbGpTXw6LW+j5KrOKJnI/Pk+cHH5OlzE/6K4qgPJlE9QAskXA1w2+ZRTv5NEnbYb9Nd1RMi3DRtEzOqDeTT0VYKmVybKoglbWOFnLet1kJpNJhPUe9qM/dU87LNrLYLLIOm5Xwy1mAhT892/kolMz3O6bFRZXrSNFfKgbVYzjaWDt2L2g3pg15H59iTWh5rpqWYUZ2WfDo6LYVMrtNyud2wGUHCNxn9/S3Nfte2uM2lgYuSB2O2mtGV0aghTUNQtvPhv6MMHW4RXlOqxLqrwrUqdVS7Wh1AB6taJzafaKfNsI0EMqOUjcYQ9LbL/ZMrgruSRxRPg2Iv94RWp/XlhqxG1wxU8G7XB//1DZqfTptpbKaCO+4h01VSXrrNWKHwcdJOM2l1Hhuu2kfyx5ZwWsx+DbLRGY10M3OVrtec+foEM72GvMNRtCthOIs2GbEdJpK7p3HCeGdpq+bLm3wPMqyn9DrKrUFbXV/mRLjDlKvux33CWWl/lOk5BLJMT+OKV1bmCNZpFlk/QQzompwXwXozeKH+lEXEh31IeaeeZUy78CzuAqe6qZnOsmpKK6ep7FomtMeY1CZgQhuxowGC9wO5y20LsqUFb2R8aaFL4ogmuk3TtsybRrq+o3HdsNTFjnuWXe5ZJtuiHBf844J/XPCPC/6UFoee7MH3yMLdq4Wsm+m2P4Nhj7s0dzsTZEeEmaP42q419JEdG59saqK3fRv74rppYynHHdshWxE420vvwlrByYYJuw0FDZ2GTBju7tht1rQFj1PngO/o+9mpIdIcN2jrhdAaUUtSFCWnLPEpFjui0yCQyWMBunN5kpqu8qj0P2dHgdIr0E0k8WWP4z69N/Y1+VYOwWBC9RhS/Pmhx0dJvaJAneQsSzsAhyftfXho+KsT6PwcuO0KKEIdzIcJdjizcxxQcVSte6GU03dGym7VJ7mMj6NI5VvjbhLKSh2ll5Xf0odk8CG/pvTzkwehFeReDh2u6q3oPgkYDiaoWuNgVbNRN6urTLdcXmUmy1mw2avkGyNgTV2JxmdGymForeJUYyNeLvM8XUVlC+umw0FaxX6/TsKZVcTWCjFy7FcKHipbog2VJrRJF/M/KYw1VdAeYLsKKj2RSPj05ORMoU0lEcmqGNh05WbbkygpVLEVJatoE8Q2zZAK20o9NiptPXJKY5hc2DDapgFCOGO1IW19kmA2cet8wUFIjywxrC424EiscP1An8ksOP+QVNNidrliTaC7tCBfBaEqdOjECO3aAoDODdW9kAfyYwrEgZ23qZgLKr07nFUhdrUDKwWB1yNMJ6Og6L0dtSqWs5aaQ+fk2P9Ym9CnV7qG8a822XcWfWFgwhnaayIhDZ9iKiGDYVP1bqcSGEkYRYY2rDCHOz6eqAPwdAGJ3WavAwcgj+Q6LCLWULwIxA3Q39qeK5rKpuJN6ylpaKMaf0HhwkTrOurHqTSo8R0eZWlH+DKFREI4YFM1f1m3E6EEOgCahhoOrbFrAMI+ikqreB/gUZEIcWlKOEL8OCRMIo4zBhiY3NYUPEhXKs6rhcltB6vQ+9IBO8Y5tV65NRyZW/Ll48QyZMj01nZkwmmuHcBDmu6CybsBRbD9uwKeypHNGaNwLKPDgSbU/gkRCQ3OwQFRqw/AHR28QtBNJeBz76JUPzV8Dutkj5iDGsYYtQ1VQNS6uDjjCI1zdjjSDOnChIhEBuqQZJpgAGyJGYNkGw7K3dw+aRqxA0wdpKATDWRN53LQEHbn2gLYhldpVWWlPqqeAOLPlHoCiBOHJNtUw1jD2GusZJXx54yBneWcJmqkEf2qvOsxTW2kvUnM9+j3bgX7riT6YYpyOOSsYYANzz453ceMK+YNkXJ7oH0INLV8mxCqWq44iH7FJHmX96rSciZgDkOJy2uEyp1rDwHp8JThDmdRP4sA665NAXT7gT1UuIsvhBoAhzz7a2VGMJKA1j9k2n+yDQEvyKYJ4Qoyw6Z++QXgXe4oYLcGwzpt8HFQZKHoe+S8Q9V7Sey99ZVNLybcTmgHz6YdgjfS3mBXdmZxwRTq2TIqkjHfmIMENNKZXeEaGVGb5kA+crtEueLcYcAY7ukBGNdUnlTOOEa9RA4Fu1gHJsQrNk42TRCdq/YAnba6CNSvxxKboP7JWEtvDQLKj8qliPKwoBwkWeNAxL5GJGffySPkZk0nRe2lmNeuYXJXGN0lKRSb93w+6/yYADN2hSciITYbIRLVLDUU5n09ICKiL4iBWOWeoBKpnAUMhbmtM8gQ/mBkR6pFLkqthYqBIHDuhKiC+gE70hpytiTaqHcYocbs146c+BIFRpM34rSjKz/WhlGWDNyciJvJ2hKsn2PDqFVWJXakuBfTMHLtVa4dxTouDkatujKzI8U/0YXR465gDETFeCIQSTlQiV0r+Y2gpqHiVt4W7mxh0SK9WrUkctwqAkvVzt10xuUFxCvilyqsgnrP1LZrilxX9hG2vqgcyXqdkTdvIgMsmCN6TAJM0bhUCi2HnSpNLdaQADqt5WPfztfrJNJzwMlPbbPo5ufeZ9Gxjytft21wR2WXPqC3Wq8/UUeJ+P1x7Ra2U5reY55+IwAd9DOD+GD0R0MVtopHGs8RYUOlY4nOB20EvkCXALX3GcAdTW68S3ghiFPw9k7DMQ358fnWveGHMgt2XoO6oLivqWyxYoXidabSaZvtjRHiFhXnBu5TBXYF9KrqyxfQGUolJnbFF4ewJ29QVtm4+kDdNDj7qP2VTwFmThrce9AqRmNnezyz5CZoFGXuqWwa5Z+XslGUBdT781B8UAFlHe6EAvUHdENRu1Ef5sz8AR1PxmcLtlFS8th1ANsq9eLG2Jsl8QRsgRCtkT/UDczMX+UMd1I3cwez7B8VLmIcaSOP7EAD2pt74s5E8JEiBuNbAiGfeRXnsw/eEgjEcLjUffDFGiBsLcoegyUv1CvcllftGa9sMvMLt94ddYqZ5pblpDLNJjvITDR9YCNUy0MZfFFkPDcpF0a+D2bKXdE4sLGwfsTP/rpSxoO7prBGL2CFPwc7ScdhG6hP0YHTbJiHqj90YOylT9GBzzAQ/acxEtAWncYWlmLQLNPbiql4kLT35mmstw4bTwzqQ+nasVFrtGTsLGa25JupmKHSeLxVA5qi/NSbxzgZyEAKl/q+x8wx1CRmdC5ZLMFaKw0XOw0fLPK81DbxXVtjjDbtfLFcPZB1UH84X9As7O2rbRC/T0MS503C+2CzYVeQXcn6y2y5CVZMWP95OZ89ruMkv5g/FMXm5WKRl6Tzk3W0ytI8vStOVul6EYTp4sXp6V8XZ2eLdUVjsRL4LZuOtDXRExA7RoupLMR6SN5EWRnzPPgSsEvpq3CtZJNNT5DrzKY21bpEHb3mhrMpw/6vLUCFGNqdEYpEo+Mj3Z3fr5kFVBmeGryQUQvT4stVEAcZFBD7Ko2360RjkIWXbz25eBqoexdOp7o554lAd+k6Cr8F8VYiUX+yp9FF9ubJYPG+dZToWFTPe/GE2o/2dF4/bthzcyKd9qM9nfLJsVxuUPdVpXS+kPAmg3qhoFqSMvJEsZpGlVjvPYGgI6LF1IGLjTVpyivuyzCkYyiPrJDiMA2TVfV6BAlx4mgmt+meBGuizvfqqysP4jj9JoNSTLGneJ0W0d1TvejdbKs38XjCYAbHFvOPdJTvFwBtB/LY13IT5Pm3NAs/kpwUQA1Q+hDqpSwpm1q9A6KvS87tLBA/FytQJpbf90YIiTYovYWRYMrpLpT0xccSTj+noUSg+mJPQQgRzxPSxo7H6S0pc0RC1RcHCtsv7DVaiUjz0WGXkoL90kQq3xmEMZsmS+iWBsTukIWLjQXVa2UlukZWoR0NgqDA6n8o4Ayxe5wKdKX/v4+M5hDvNjjIrY39+KAExhoi2XFWWJQNTrU4VUARrVDH8tjXcrllniBFRNPip7qxYh1wDoeTYLKKt6FMtfvqtC2Vz2/1p72ZDODtQe8ZYXGjYjEtrKiMNTfeyuPuNOKv0m9JnAbhVbqVNy5S0r4hYPio9x/p6UZ3Ghl1lW6esuj+QUIA93l6bdIrkq+yaFNdjQq45BPs6X0kMQlyAog4MWWauYPr0Ni7fUFcj+nnLJbVaWq6PfVfg/zhMr5Ps6h4WIuEpSQ3miopJwm2ShOlo+1HBzr5OwqyXBqK7qs7pWXB3u6D6TVpjnrNTajODiHBjR6zvoPINd8dqFVWn8pA8N8dUBGFso+eABE12aXfyf22NFESe918dZCu7HlPVfnGfXaW1G+imCyjfyjaKSnRgW6WsqO/Mi78dxcp+MeW6eNr5l+u2L1YkKyILBLRbC6ajPU6yKTFqv3ooMkI7iWJXX1xoBAV8jSuPznc2VSPxUq3Ns1Hlz12tg5iCoIQpAgkO89h9iMHZ3Gd4kyxmqSYbOBSXSjnquSqv7lI6puMZNUCLstpPsVBIxizt3MTErYeKYJaUEntQVl05wDJ6zw+rOoQzUTBOnSWpLo63kfJVRzR7TEIYDV1+hva2hhD3jfWH/ftJIO7Y7mdZ2oP/t6nGqz8yGcb7Dzz3NR4rTGM1t3Mbci5AAu9h11H4zCGfpgKpJZTyw1ZgYt7leCyXWHWTVw4BHHnIiXuGzwld7GhABU3+b0xaiBzGDDtd62M3zT+sSXKxr376nImADd3vXZ1V+l6rVyBth/3FOzeYD4Y4MdbL3mIKj/EoeNTRuLpPThw6cMQOt6MHiN6ciyC9UY+RrefHWhlEYG0pvx3RzVYlrHYVYoWrPo8raIAPDf2OC76VMr7Vqb7Utn6VTr6VpX5vjB43sq33a4Tnd/x0LWiCbPWe7lACRxXjN4rxlEuW9E7yuWjXJ7u4naM6+SxrtF8X772uTrc7QqJRbNwWx1LV6feSyNc+jDWRfsTs68rxN3ihYvDMRQ0nb6zN3I0JMaCz4EqcuXwur0HT4rC6z50JgKj6bbIY6ExPgGS92bwQL/9odNPiHjcewbqqYwsw0c2gBSethGxonnzBqcHvSUibyVNb43g1HmnPeYmLh9E5FTndqvudULCvs2WJhTB8NvUMoj3gJtUuPxYc6O2aPignB3bz8606pdFAHLImyM6LAW5vNlpvjmcl6L7JCi2mXzP1X2eHo1iSArVJ7wJ4a3x+26yQM7dkGMaC6oB6GnESN8qI+xgp3nilqvWqUXoszp2LWJEHFskRwpxHjk4ILfZz43LrPFnw+LByaJdE767LzPNzw46jbIuIPiECBw83lC4sibQuL27UFvEwinIEgN4LUORYLtRckSEpsGHiwssOraDvQtXysauRW8KgPNdH167twguyXmEhz5Ed99m1nR2BBIgbKqlJqQr4KrwwDmMxkIdKC9uPaIAjQTbt43ubaMHpDAqX7R7m19v4/hifhfEsvm4puu+wKOEcbM5nXDZLZ0T8bFAgr0NREtN1SNmkPhx+4kYvPtm3Cgh7eQs7cmm/tL+bkPa1eHkhDh3JWdY1LqSI3kd2k6OL1dlmc8oE75GIYstt3zKC7I+YRlOln/ElVl/l4Fu/qI7klfRdi7mL07PXsxnl3EU5FUQwjpy3kv5MUSrUHpn37NQeiRcL+Ti7gH5GJU8D4VQc5xyoDPPBEPSndOhlAHRAEX7OOj5Qi55DkGzekwsYpwtZ/QvhA58FWahYM6efFA3Bj7mdtgCcKGl326fuDqUpynfJiF5vJj/Z1nm5eztv9/Wxb6bfcjoML+cnc7+y7nqKiReVW/yNchWDwF7FTZ4fEeS++LhYv7jD8406xh5GqIv/vKjM9WOu/aEyxc8DXRbn+eKLLthLKI1QUf5Kl1vtmUBxw60Ifbkitza2wXYc6HD61K000s9/BzwxBJj0vnFDRr6znM1bSA8v5NUDIlX0f7C3sF0pANGwOtPDot2p+n89y9ceQoFvBu5AjnK3TAZwLsijCIF8DhxBywNqqBz4kD/yzp4/FdX9gtB59zWbK4otm7btKCKVtd/mrWB6jxwgwtV58aLtqDDDsYawap2/oCRew0sABZjZc0sNKDbkWcmnoGn+QNmm3wL4TalxdKDTiaYEtytQTCVQQ2D48D1l8VdBLhBuzCibD19ot0YO+2AIf821G7/zl785ExSCizFNd7P6vbcpM7eTnbOXcDDVmkyXYPgl+Ch4aLJvgeCPScIpFVRI615aJ9kGK8TD6d9aPtXjzXeIx4638ViG7CqSfHX+lMSLP/Hnjh8ULaBejrOB8cHINVQbENY2gRh06HQGdicV8XY46S4mDTMuO8jRG44Dycv0hL1QBpyZK+dj3wc2UsfJB+EKu8+vyrP1tXCr8YTCNlmXYHDhC/v7rwwVw3b5oVoPvS8IkRs609HjdHmQzUHR2bzSVmMx+aBshqKTYPLH5xxOc7totUmrwnl1h8kwG19rzNBR8C+Q66nMSgw2+GfyfrzfOjZa2wNniGw2nHo+g+dm0rFYVNQRXnzu99QfASHkHdFnyZk2hGAA/Q2VjekdhvvNlZbfy0FuocbfO5vg7btALTPBq7QSuOoA3ZloRqs7ID5t/PpvksLui5QzkBBw8dV6y9quIBq+3rA93ng86ug960396mV9axQ9K7z8nxD8FyVaK4LBRim7LhW7NtaYSdMvS0VR7F8FMtHsQxODV93G67XtLu+VvZ7Jaa9dXUV+fi9oNczFftyXCd3rH61vs8bYa+EOOMeMAq86iZRkA24WBnpSkUXk+yAhxOIaTb6pMADgh0wIy0MBp2Hhgsh5jYVuKKDJC4UbsytJSqFgdaPcowyTzvINkbZqPsBKNDXAWOeCxg2cEMmxgpzgxhfdiDaqyBjBkjZHSC6OGPu5Jx8Y/v6FeEhctS8SFgtMwJ3qAZv2+wosKpiY3iAaaN62Q2aMSSX9Tibx25vHG8mA9EQ55O+I2odWsvjyO6tc8PejrR1vC27QdfFyFJz46GqzEPN1eS6casLDhpQvulu9XclPQwrFz+nbhjvT976QEtD+ToJZ2w5qGNQ1B1gEWpOqg/vt3ERbeJoRau8mJ+enJwp/OhoCB7sPC0xQaT5J4UghQXJqnAzdDPMJl6kxim9yaJkFW2CmG+/lMl2m8m42tKTUxpTtQLqpE2Fgue6WnFLX4K9iQtCyCQ9Esrgo3WT82EIOFNCaH1Iqt3p7HJVRTC6CvJVoMZWLqNIHZHjghzOy393uMHD6O4JdtoTglx/9fGwMYMFPXY870yMFigKHTdiymhBI/WM0GI9gJOiRRNIegK0dCHlbofvS0bCChf2jm8B//mwpQsW1w+pbrfyxTr+Nz+CXJAVYQj579NgSdKMIK2B0O0JVWjAmbHApY/w3k/5szOgGZa0nYqpnUNrOoHVA1O7lVqOEeu5YYXCv/BDC6ZPAjhMN2ho3YgANIbKGQmPli8ZDFOL7himeyz59g+Ik0nCIcjbC4l425i+GgYXGsxp4VX7GAPtaFLGlGlTognypkZq1D0UOimMhKADe48lzvEZaAuf+lwwhbl67zmuHF6OUUcZNDfjB1rKMCUCdfd8ppbK2cfEqMZJe1zAOjw8BOvVdK8D7RzAht2cNSCOoNWA9pDgqn8lald4hYNt7uGCPkzUH+6y7i6e92VlZ1EADgdfZcwCoBXV9+eCJjUywyGAyMfFFW5NszsATK3EsB79vVBc1M7hhyNCGm92oCFt0nMRJKDnPlLlvsiSBlCj2+ftGg9TyxUXMOyFaGlkChuYA1Bmlb6/QCuq789FpKgezkh9+yJP1Id0PSDJTrQoTrM8SS7xuWADfYlYBxDs/dcJ5cxUS40Nsg50cTmcVUW41K2/GVeX/TU7mBBTOzYrODwzAvGBLsh/6gBxJ706BphXPU8U6l5bOyAwlnZ8RrG3d5aiO4PbjuxEXWC2F2aidYP5UB9tVIp9tpYSYpMAElZMfxZ7NTwcC1KvEJhkb9AlRRo5guywQQaFntkl1ur3OtpgI/uMrzrMDGhDVaU8C0xB0XSQGsX4MnuAI9uTputQ7p3mwX2sdnA+5F/zmQAcVZACWqagJUjWxrYKyZsoKwNRBV8CJbJEVWpJCsVJcD7rwh4AbnjL1QNZBxfz8EtKB74KntCl5wBcxIoqAaZUUX2GiLMUM1nRe10hLyZD1fA5zNVVLqxKNdVniHzprWskKxyB1GHgU8Fx6DJY19VOWKy6NoOmxlYAmSoFVQtKzWAuqHogo3Ub8Hq1dVnTbyzWsVqadE1dbchquxp5m2asVj6PpmbxfTm76iXzK6wFUjZNI4Scrs0wNsCmautKKxMRrMYqVVMdy2BdV3tzjFXXZtDUWOVxGN3qphGrskrV1Ffds1rWxV1fYRVyWTS1trnMVUshWdWK5QxQtWIe6/6KRzSsy2IuTa/5jA7Co9oB4oKjStcKDZbFbguALKBdErYVcF5IkQ2HksOwoDbuyX2WOKQJaE7Lpa7yGuwlnh1lJVzIVnSjwpTb0cI7uS6Y1ozLC+zpkKhbwFm65U7zQdmidyWgTWVZUkyQz9Rityy6LEaNArqqCSt1SF0UAhxh3cSjIA3sqix22lLVR59drF0/9F2E/EOEBiuNhRo6aRflUDJAB7XRZgaOoHpILcvxn4d3URP7BOqubagU/AqM7wn/XccIeHmTaUAM9cUSHOLWQT2GomFSJkAWHmJ0CYAV5kJ49zRH5bKLYLqGXYZ9CUZzfPbhSDIX8omnvWBQ66uPswN254c1s2rTbVggqi740k2Kt+6Kiga8z7jvuc+OqxoUngKf6osBeudnnCEOTtNQR0FdDd9XKYOZdRbHC5S+nH1k5hoFjoNDr4YVmm4/D4a2E8uSkchdir/pOwzV/dki+Dni3MDdIX0ygVdF8mWr7167jJ0BlDwjLNPTdFP2QMM7q/VV8zm+kvqXL94m+e6+YaBxtyo/Yz1ll0XHIM2ORMg32mjzane+bPXdV6dVPxa84wafF5+dV+4BeAJcorexN+B8NIDbMMjToav+poO2hRfDFCfVkZhiYTiPKzV0paZjEHx1zys+vLNLNO02aH0sMONJAzYJC4Brtc78GJ9GFubKfqQHdH3I40pMH4Udsr2sJVe0ZraHzBzFwBNniN4W1A8TxMtdUadRpXjuuMUqo7Vd9NGJEdaU5kmg1tquTTtfVLeh9Qf6k25LKfH3aUjivPx6vvi4TdiDhtWvVySP7jsS55RmQlaCdV+b521ylzb2hlKLmizSW0nvSRGEQRFcZkV0F6wKmrwieV5aLvwWxNvy6u8LCd8mH7bFZlvQLpP1l1jYSDNjRV395wulzecfymfscx9doM2M2BuQH5Kft1Ectu1+AzzVhJBgVpD1E5VsLNnaSu6fWkrXaWJJqGZfa7z5iaw3MSWWf0iWwVfSp2107r4j98HqiX7/GoVsImNEzAMhsv38VRTc0+1xXtPoytOfFMPh+vFv/wekqkGzJ7wBAA== + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/201609211206577_ApiKeyDescription.Designer.cs b/src/NuGetGallery/Migrations/201609211206577_ApiKeyDescription.Designer.cs new file mode 100644 index 0000000000..0e0c207552 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609211206577_ApiKeyDescription.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 ApiKeyDescription : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ApiKeyDescription)); + + string IMigrationMetadata.Id + { + get { return "201609211206577_ApiKeyDescription"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201609211206577_ApiKeyDescription.cs b/src/NuGetGallery/Migrations/201609211206577_ApiKeyDescription.cs new file mode 100644 index 0000000000..9d5a04e219 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609211206577_ApiKeyDescription.cs @@ -0,0 +1,18 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class ApiKeyDescription : DbMigration + { + public override void Up() + { + AddColumn("dbo.Credentials", "Description", c => c.String(maxLength: 256)); + } + + public override void Down() + { + DropColumn("dbo.Credentials", "Description"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201609211206577_ApiKeyDescription.resx b/src/NuGetGallery/Migrations/201609211206577_ApiKeyDescription.resx new file mode 100644 index 0000000000..dada304c14 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609211206577_ApiKeyDescription.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/Migrations/201609260823310_ScopedCredential.Designer.cs b/src/NuGetGallery/Migrations/201609260823310_ScopedCredential.Designer.cs new file mode 100644 index 0000000000..96a0ef7788 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609260823310_ScopedCredential.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 ScopedCredential : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ScopedCredential)); + + string IMigrationMetadata.Id + { + get { return "201609260823310_ScopedCredential"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201609260823310_ScopedCredential.cs b/src/NuGetGallery/Migrations/201609260823310_ScopedCredential.cs new file mode 100644 index 0000000000..5cbc7fd304 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609260823310_ScopedCredential.cs @@ -0,0 +1,32 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class ScopedCredential : DbMigration + { + public override void Up() + { + CreateTable( + "dbo.Scopes", + c => new + { + Key = c.Int(nullable: false, identity: true), + Subject = c.String(), + AllowedAction = c.String(nullable: false), + Credential_Key = c.Int(nullable: false), + }) + .PrimaryKey(t => t.Key) + .ForeignKey("dbo.Credentials", t => t.Credential_Key, cascadeDelete: true) + .Index(t => t.Credential_Key); + + } + + public override void Down() + { + DropForeignKey("dbo.Scopes", "Credential_Key", "dbo.Credentials"); + DropIndex("dbo.Scopes", new[] { "Credential_Key" }); + DropTable("dbo.Scopes"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201609260823310_ScopedCredential.resx b/src/NuGetGallery/Migrations/201609260823310_ScopedCredential.resx new file mode 100644 index 0000000000..afc71a1b45 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609260823310_ScopedCredential.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/Migrations/201609270831462_AddExpirationColumn.Designer.cs b/src/NuGetGallery/Migrations/201609270831462_AddExpirationColumn.Designer.cs new file mode 100644 index 0000000000..4e3e199ce1 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609270831462_AddExpirationColumn.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 AddExpirationColumn : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddExpirationColumn)); + + string IMigrationMetadata.Id + { + get { return "201609270831462_AddExpirationColumn"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201609270831462_AddExpirationColumn.cs b/src/NuGetGallery/Migrations/201609270831462_AddExpirationColumn.cs new file mode 100644 index 0000000000..2adecc5d64 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609270831462_AddExpirationColumn.cs @@ -0,0 +1,18 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddExpirationColumn : DbMigration + { + public override void Up() + { + AddColumn("dbo.Credentials", "ExpirationTicks", c => c.Long()); + } + + public override void Down() + { + DropColumn("dbo.Credentials", "ExpirationTicks"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201609270831462_AddExpirationColumn.resx b/src/NuGetGallery/Migrations/201609270831462_AddExpirationColumn.resx new file mode 100644 index 0000000000..7877e39454 --- /dev/null +++ b/src/NuGetGallery/Migrations/201609270831462_AddExpirationColumn.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/Migrations/201610042351343_AddUserFailedLogin.Designer.cs b/src/NuGetGallery/Migrations/201610042351343_AddUserFailedLogin.Designer.cs new file mode 100644 index 0000000000..60cef33f81 --- /dev/null +++ b/src/NuGetGallery/Migrations/201610042351343_AddUserFailedLogin.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 AddUserFailedLogin : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddUserFailedLogin)); + + string IMigrationMetadata.Id + { + get { return "201610042351343_AddUserFailedLogin"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201610042351343_AddUserFailedLogin.cs b/src/NuGetGallery/Migrations/201610042351343_AddUserFailedLogin.cs new file mode 100644 index 0000000000..87b4a1e5f9 --- /dev/null +++ b/src/NuGetGallery/Migrations/201610042351343_AddUserFailedLogin.cs @@ -0,0 +1,20 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddUserFailedLogin : DbMigration + { + public override void Up() + { + AddColumn("dbo.Users", "LastFailedLoginUtc", c => c.DateTime()); + AddColumn("dbo.Users", "FailedLoginCount", c => c.Int(nullable: false, identity: false, defaultValue: 0)); + } + + public override void Down() + { + DropColumn("dbo.Users", "FailedLoginCount"); + DropColumn("dbo.Users", "LastFailedLoginUtc"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201610042351343_AddUserFailedLogin.resx b/src/NuGetGallery/Migrations/201610042351343_AddUserFailedLogin.resx new file mode 100644 index 0000000000..8dc6640d3d --- /dev/null +++ b/src/NuGetGallery/Migrations/201610042351343_AddUserFailedLogin.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/Migrations/201611240011320_AddTriggerForPackagesLastEdited.Designer.cs b/src/NuGetGallery/Migrations/201611240011320_AddTriggerForPackagesLastEdited.Designer.cs new file mode 100644 index 0000000000..26dfdb9e61 --- /dev/null +++ b/src/NuGetGallery/Migrations/201611240011320_AddTriggerForPackagesLastEdited.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 AddTriggerForPackagesLastEdited : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddTriggerForPackagesLastEdited)); + + string IMigrationMetadata.Id + { + get { return "201611240011320_AddTriggerForPackagesLastEdited"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201611240011320_AddTriggerForPackagesLastEdited.cs b/src/NuGetGallery/Migrations/201611240011320_AddTriggerForPackagesLastEdited.cs new file mode 100644 index 0000000000..919ff9f675 --- /dev/null +++ b/src/NuGetGallery/Migrations/201611240011320_AddTriggerForPackagesLastEdited.cs @@ -0,0 +1,38 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddTriggerForPackagesLastEdited : DbMigration + { + public override void Up() + { + // If the "LastEdited" field of a package was edited, update it to the current timestamp. + // This guarantees that all "LastEdited" timestamps were generated by the same source: the gallery database. + // + // Multiple instances of the gallery running simultaneously will have slightly different local machine times. + // Therefore, one source must generate all "LastEdited" timestamps because otherwise there will be slight discrepancies in "LastEdited" which can lead to packages being inserted out of order into the feed. + // + // Note that UPDATE(LastEdited) is true when a row is inserted for the first time, so we must add a INSERTED.LastEdited IS NOT NULL check to the UPDATE statement. + + Sql(@" +CREATE TRIGGER [dbo].[LastEditedTrigger] ON [dbo].[Packages] +AFTER INSERT, UPDATE +AS +BEGIN + IF (UPDATE(LastEdited)) + BEGIN + UPDATE [dbo].[Packages] + SET LastEdited = GETUTCDATE() + FROM INSERTED + WHERE [dbo].[Packages].[Key] = INSERTED.[Key] AND INSERTED.LastEdited IS NOT NULL + END +END"); + } + + public override void Down() + { + Sql(@"DROP TRIGGER [dbo].[LastEditedTrigger]"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201611240011320_AddTriggerForPackagesLastEdited.resx b/src/NuGetGallery/Migrations/201611240011320_AddTriggerForPackagesLastEdited.resx new file mode 100644 index 0000000000..087ee477a8 --- /dev/null +++ b/src/NuGetGallery/Migrations/201611240011320_AddTriggerForPackagesLastEdited.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/Migrations/201701120413341_AddScopeCredentialKey.Designer.cs b/src/NuGetGallery/Migrations/201701120413341_AddScopeCredentialKey.Designer.cs new file mode 100644 index 0000000000..a187245f77 --- /dev/null +++ b/src/NuGetGallery/Migrations/201701120413341_AddScopeCredentialKey.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 AddScopeCredentialKey : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddScopeCredentialKey)); + + string IMigrationMetadata.Id + { + get { return "201701120413341_AddScopeCredentialKey"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201701120413341_AddScopeCredentialKey.cs b/src/NuGetGallery/Migrations/201701120413341_AddScopeCredentialKey.cs new file mode 100644 index 0000000000..090efab4a5 --- /dev/null +++ b/src/NuGetGallery/Migrations/201701120413341_AddScopeCredentialKey.cs @@ -0,0 +1,20 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddScopeCredentialKey : DbMigration + { + public override void Up() + { + RenameColumn(table: "dbo.Scopes", name: "Credential_Key", newName: "CredentialKey"); + RenameIndex(table: "dbo.Scopes", name: "IX_Credential_Key", newName: "IX_CredentialKey"); + } + + public override void Down() + { + RenameIndex(table: "dbo.Scopes", name: "IX_CredentialKey", newName: "IX_Credential_Key"); + RenameColumn(table: "dbo.Scopes", name: "CredentialKey", newName: "Credential_Key"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201701120413341_AddScopeCredentialKey.resx b/src/NuGetGallery/Migrations/201701120413341_AddScopeCredentialKey.resx new file mode 100644 index 0000000000..f0653b8afa --- /dev/null +++ b/src/NuGetGallery/Migrations/201701120413341_AddScopeCredentialKey.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 165ee28cea..4747d52411 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -1,6 +1,5 @@  - Debug @@ -25,18 +24,15 @@ 12.0 443 - true enabled disabled false 0 - 001c21d7 - 061576ae - - true false + + true @@ -48,7 +44,7 @@ 4 false true - ..\Frontend.ruleset + ..\FrontendSDL.ruleset False False True @@ -135,14 +131,8 @@ False ..\..\packages\DynamicData.EFCodeFirstProvider.0.3.0.0\lib\net40\DynamicData.EFCodeFirstProvider.dll - - False - ..\..\packages\elmah.corelibrary.1.2.2\lib\Elmah.dll - True - - - False - ..\..\packages\Elmah.Contrib.Mvc.1.0\lib\net40\Elmah.Contrib.Mvc.dll + + ..\..\packages\elmah.corelibrary.strongname.1.2.2\lib\Elmah.dll True @@ -155,6 +145,10 @@ ..\..\packages\entityframework.6.1.3\lib\net45\EntityFramework.SqlServer.dll True + + ..\..\packages\Hyak.Common.1.0.2\lib\net45\Hyak.Common.dll + True + False ..\..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll @@ -220,39 +214,36 @@ ..\..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll True - - False - ..\..\packages\Microsoft.ApplicationInsights.Agent.Intercept.1.2.1\lib\net45\Microsoft.AI.Agent.Intercept.dll + + ..\..\packages\Microsoft.ApplicationInsights.Agent.Intercept.2.0.6\lib\net45\Microsoft.AI.Agent.Intercept.dll True - - False - ..\..\packages\Microsoft.ApplicationInsights.DependencyCollector.2.0.0\lib\net45\Microsoft.AI.DependencyCollector.dll + + ..\..\packages\Microsoft.ApplicationInsights.DependencyCollector.2.2.0\lib\net45\Microsoft.AI.DependencyCollector.dll True - - False - ..\..\packages\Microsoft.ApplicationInsights.PerfCounterCollector.2.0.0\lib\net45\Microsoft.AI.PerfCounterCollector.dll + + ..\..\packages\Microsoft.ApplicationInsights.PerfCounterCollector.2.2.0\lib\net45\Microsoft.AI.PerfCounterCollector.dll True - - False - ..\..\packages\Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.2.0.0\lib\net45\Microsoft.AI.ServerTelemetryChannel.dll + + ..\..\packages\Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.2.2.0\lib\net45\Microsoft.AI.ServerTelemetryChannel.dll True - - False - ..\..\packages\Microsoft.ApplicationInsights.Web.2.0.0\lib\net45\Microsoft.AI.Web.dll + + ..\..\packages\Microsoft.ApplicationInsights.Web.2.2.0\lib\net45\Microsoft.AI.Web.dll True - - False - ..\..\packages\Microsoft.ApplicationInsights.WindowsServer.2.0.0\lib\net45\Microsoft.AI.WindowsServer.dll + + ..\..\packages\Microsoft.ApplicationInsights.WindowsServer.2.2.0\lib\net45\Microsoft.AI.WindowsServer.dll True - - False - ..\..\packages\Microsoft.ApplicationInsights.2.0.0\lib\net45\Microsoft.ApplicationInsights.dll + + ..\..\packages\Microsoft.ApplicationInsights.2.2.0\lib\net45\Microsoft.ApplicationInsights.dll + True + + + ..\..\packages\Microsoft.ApplicationInsights.TraceListener.2.2.0\lib\net45\Microsoft.ApplicationInsights.TraceListener.dll True @@ -284,6 +275,29 @@ ..\..\packages\Microsoft.AspNet.WebApi.MessageHandlers.Compression.1.3.0\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1\Microsoft.AspNet.WebApi.MessageHandlers.Compression.dll True + + ..\..\packages\Microsoft.AspNetCore.Cryptography.Internal.1.0.0\lib\net451\Microsoft.AspNetCore.Cryptography.Internal.dll + True + + + ..\..\packages\Microsoft.AspNetCore.Cryptography.KeyDerivation.1.0.0\lib\net451\Microsoft.AspNetCore.Cryptography.KeyDerivation.dll + True + + + ..\..\packages\Microsoft.Azure.Common.2.0.4\lib\net45\Microsoft.Azure.Common.dll + True + + + ..\..\packages\Microsoft.Azure.Common.2.0.4\lib\net45\Microsoft.Azure.Common.NetFramework.dll + True + + + ..\..\packages\Microsoft.Azure.KeyVault.1.0.0\lib\net45\Microsoft.Azure.KeyVault.dll + True + + + ..\..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll + @@ -306,6 +320,14 @@ ..\..\packages\Microsoft.Data.Services.Client.5.6.5-beta\lib\net40\Microsoft.Data.Services.Client.dll True + + ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.4\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + True + + + ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.4\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll + True + ..\..\packages\Microsoft.IdentityModel.Protocol.Extensions.1.0.0\lib\net45\Microsoft.IdentityModel.Protocol.Extensions.dll True @@ -339,6 +361,18 @@ ..\..\packages\Microsoft.Owin.Security.OpenIdConnect.3.0.1\lib\net45\Microsoft.Owin.Security.OpenIdConnect.dll True + + ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll + True + + + ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll + True + + + ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll + True + False ..\..\packages\WindowsAzure.Caching.1.7.0.0\lib\net35-full\Microsoft.Web.DistributedCache.dll @@ -369,10 +403,8 @@ C:\Program Files\Microsoft SDKs\Azure\.NET SDK\v2.7\ref\Microsoft.WindowsAzure.ServiceRuntime.dll False - - False - ..\..\packages\WindowsAzure.Storage.4.3.0\lib\net40\Microsoft.WindowsAzure.Storage.dll - True + + ..\..\packages\WindowsAzure.Storage.7.0.0\lib\net40\Microsoft.WindowsAzure.Storage.dll False @@ -385,38 +417,31 @@ True - - ..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll - True + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - - False - ..\..\packages\NuGet.Common.3.5.0-beta-final\lib\net45\NuGet.Common.dll - True + + ..\..\packages\NuGet.Common.4.0.0\lib\net45\NuGet.Common.dll - - False - ..\..\packages\NuGet.Frameworks.3.5.0-beta-final\lib\net45\NuGet.Frameworks.dll - True + + ..\..\packages\NuGet.Frameworks.4.0.0\lib\net45\NuGet.Frameworks.dll False ..\..\packages\NuGet.Logging.3.5.0-beta-1160\lib\net45\NuGet.Logging.dll True - - False - ..\..\packages\NuGet.Packaging.3.5.0-beta-final\lib\net45\NuGet.Packaging.dll - True + + ..\..\packages\NuGet.Packaging.4.0.0\lib\net45\NuGet.Packaging.dll - - False - ..\..\packages\NuGet.Packaging.Core.3.5.0-beta-final\lib\net45\NuGet.Packaging.Core.dll - True + + ..\..\packages\NuGet.Packaging.Core.4.0.0\lib\net45\NuGet.Packaging.Core.dll - - False - ..\..\packages\NuGet.Packaging.Core.Types.3.5.0-beta-final\lib\net45\NuGet.Packaging.Core.Types.dll + + ..\..\packages\NuGet.Packaging.Core.Types.4.0.0\lib\net45\NuGet.Packaging.Core.Types.dll + + + ..\..\packages\NuGet.Services.KeyVault.1.0.0.0\lib\net45\NuGet.Services.KeyVault.dll True @@ -424,10 +449,8 @@ ..\..\packages\NuGet.Services.Platform.Client.3.0.29-r-master\lib\portable-net45+wp80+win\NuGet.Services.Platform.Client.dll True - - False - ..\..\packages\NuGet.Versioning.3.5.0-beta-final\lib\net45\NuGet.Versioning.dll - True + + ..\..\packages\NuGet.Versioning.4.0.0\lib\net45\NuGet.Versioning.dll False @@ -459,6 +482,8 @@ ..\..\packages\RouteMagic.1.1.3\lib\net40\RouteMagic.dll True + + @@ -613,6 +638,7 @@ + @@ -655,13 +681,23 @@ + + + + + + + + + + @@ -700,6 +736,16 @@ + + + + + + + + + + 201602181939424_RemovePackageStatistics.cs @@ -724,10 +770,73 @@ 201605250755294_AddIndexForUserEmailAddress.cs + + + 201605310704169_RemoveOldCredentialColumnsFromUsersTable.cs + + + + 201606012049351_AddIndexForCredentialsUserKey.cs + + + + 201606012058492_AddIndexForPackageLicenseReportsPackageKey.cs + + + + 201606020741056_CredentialExpires.cs + + + + 201607190813558_CredentialDoesNotExpire.cs + + + + 201607190842411_CredentialLastUsed.cs + + + + 201608251939567_AddPackageTypes.cs + + + + 201609092252096_AddIndexCredentialExpires.cs + + + + 201609092255576_AddIndexUsersEmail.cs + + + + 201610042351343_AddUserFailedLogin.cs + + + + 201609211206577_ApiKeyDescription.cs + + + + 201609260823310_ScopedCredential.cs + + + + 201609270831462_AddExpirationColumn.cs + + + + 201701120413341_AddScopeCredentialKey.cs + + + + 201611240011320_AddTriggerForPackagesLastEdited.cs + + + + @@ -1003,7 +1112,7 @@ - + @@ -1016,9 +1125,7 @@ - - @@ -1028,7 +1135,6 @@ - @@ -1296,20 +1402,35 @@ - - + + + + + + + + + + + + + + + + PreserveNewest + Designer @@ -1317,6 +1438,15 @@ + + + + + + + + + @@ -1505,7 +1635,7 @@ - + @@ -1520,6 +1650,7 @@ + @@ -1548,6 +1679,11 @@ + + + + + @@ -1583,6 +1719,7 @@ + @@ -1686,8 +1823,8 @@ - + @@ -1826,6 +1963,51 @@ 201605250755294_AddIndexForUserEmailAddress.cs + + 201605310704169_RemoveOldCredentialColumnsFromUsersTable.cs + + + 201606012049351_AddIndexForCredentialsUserKey.cs + + + 201606012058492_AddIndexForPackageLicenseReportsPackageKey.cs + + + 201606020741056_CredentialExpires.cs + + + 201607190813558_CredentialDoesNotExpire.cs + + + 201607190842411_CredentialLastUsed.cs + + + 201608251939567_AddPackageTypes.cs + + + 201609092252096_AddIndexCredentialExpires.cs + + + 201609092255576_AddIndexUsersEmail.cs + + + 201610042351343_AddUserFailedLogin.cs + + + 201609211206577_ApiKeyDescription.cs + + + 201609260823310_ScopedCredential.cs + + + 201609270831462_AddExpirationColumn.cs + + + 201701120413341_AddScopeCredentialKey.cs + + + 201611240011320_AddTriggerForPackagesLastEdited.cs + PublicResXFileCodeGenerator Strings.Designer.cs @@ -1905,8 +2087,5 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - \ No newline at end of file diff --git a/src/NuGetGallery/OData/NuGetODataController.cs b/src/NuGetGallery/OData/NuGetODataController.cs index 335bbc9437..fb1dae29fe 100644 --- a/src/NuGetGallery/OData/NuGetODataController.cs +++ b/src/NuGetGallery/OData/NuGetODataController.cs @@ -15,9 +15,9 @@ namespace NuGetGallery.OData public abstract class NuGetODataController : ODataController { - private readonly ConfigurationService _configurationService; + private readonly IGalleryConfigurationService _configurationService; - protected NuGetODataController(ConfigurationService configurationService) + protected NuGetODataController(IGalleryConfigurationService configurationService) { _configurationService = configurationService; } diff --git a/src/NuGetGallery/OData/QueryAllowed/Data/apiv1packages.json b/src/NuGetGallery/OData/QueryAllowed/Data/apiv1packages.json new file mode 100644 index 0000000000..3b8126d821 --- /dev/null +++ b/src/NuGetGallery/OData/QueryAllowed/Data/apiv1packages.json @@ -0,0 +1,20 @@ +{ + "AllowedOperatorPatterns": [ + "filter, orderby, top", + "filter, top", + "filter, orderby, skip, top", + "filter, select", + "top", + "filter, orderby", + "orderby", + "skip", + "filter", + "orderby, skip, top", + "filter, skip", + "filter, orderby, skip", + "filter, orderby, select, top", + "filter, skip, top", + "orderby, skip", + "skip, top" + ] +} \ No newline at end of file diff --git a/src/NuGetGallery/OData/QueryAllowed/Data/apiv1search.json b/src/NuGetGallery/OData/QueryAllowed/Data/apiv1search.json new file mode 100644 index 0000000000..1c8c488e85 --- /dev/null +++ b/src/NuGetGallery/OData/QueryAllowed/Data/apiv1search.json @@ -0,0 +1,7 @@ +{ + "AllowedOperatorPatterns": [ + "filter, orderby, skip, top", + "filter, skip, top", + "filter, top" + + ] } \ No newline at end of file diff --git a/src/NuGetGallery/OData/QueryAllowed/Data/apiv2getupdates.json b/src/NuGetGallery/OData/QueryAllowed/Data/apiv2getupdates.json new file mode 100644 index 0000000000..694a211f5d --- /dev/null +++ b/src/NuGetGallery/OData/QueryAllowed/Data/apiv2getupdates.json @@ -0,0 +1,13 @@ +{ + "AllowedOperatorPatterns": [ + "orderby, top", + "filter, orderby, top", + "filter", + "orderby, skip, top", + "filter, skip, top", + "skip", + "skiptoken", + "filter, orderby, skip, top", + "orderby" + ] +} \ No newline at end of file diff --git a/src/NuGetGallery/OData/QueryAllowed/Data/apiv2packages.json b/src/NuGetGallery/OData/QueryAllowed/Data/apiv2packages.json new file mode 100644 index 0000000000..436126c3bd --- /dev/null +++ b/src/NuGetGallery/OData/QueryAllowed/Data/apiv2packages.json @@ -0,0 +1,41 @@ +{ + "AllowedOperatorPatterns": [ + "filter", + "filter, orderby, top", + "filter, top", + "filter, orderby, skip", + "filter, orderby", + "filter, orderby, select, top", + "filter, skip", + "filter, orderby, skip, top", + "skip", + "filter, format, orderby, select, skip, top", + "filter, select", + "orderby, top", + "filter, inlinecount, orderby, skip, top", + "filter, orderby, skiptoken, top", + "filter, format, select", + "orderby, skip", + "filter, skip, top", + "filter, inlinecount, orderby, select, skip, top", + "filter, orderby, select, skip, top", + "filter, orderby, select", + "filter, select, skip, top", + "filter, format, select, top", + "orderby, skip, top", + "top", + "filter, orderby, select, skip", + "orderby", + "skip, top", + "filter, select, top", + "filter, inlinecount, select, top", + "orderby, select", + "filter, select, skip", + "inlinecount", + "select, skip", + "format, top", + "filter, format", + "orderby, skiptoken, top", + "filter, orderby, skip, skiptoken, top" + ] +} \ No newline at end of file diff --git a/src/NuGetGallery/OData/QueryAllowed/Data/apiv2search.json b/src/NuGetGallery/OData/QueryAllowed/Data/apiv2search.json new file mode 100644 index 0000000000..862b1fd595 --- /dev/null +++ b/src/NuGetGallery/OData/QueryAllowed/Data/apiv2search.json @@ -0,0 +1,17 @@ +{ + "AllowedOperatorPatterns": [ + "filter, skip, top", + "filter, orderby, skip, top", + "top", + "filter, orderby", + "filter, orderby, skip", + "filter, top", + "filter, inlinecount, orderby, skip, top", + "orderby, skip, top", + "skip, top", + "filter, orderby, top", + "filter, inlinecount, orderby, select, skip, top", + "orderby, top", + "filter" + ] +} \ No newline at end of file diff --git a/src/NuGetGallery/OData/QueryAllowed/ODataQueryFilter.cs b/src/NuGetGallery/OData/QueryAllowed/ODataQueryFilter.cs new file mode 100644 index 0000000000..df66a87d9f --- /dev/null +++ b/src/NuGetGallery/OData/QueryAllowed/ODataQueryFilter.cs @@ -0,0 +1,104 @@ +// 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.IO; +using System.Linq; +using System.Reflection; +using System.Web.Http.OData.Query; +using Newtonsoft.Json; + +namespace NuGetGallery.OData.QueryFilter +{ + /// + /// IODataQueryFilter interface + /// + public class ODataQueryFilter + { + [Flags] + public enum ODataOperators + { + None = 0, + Expand = 1, + Filter = 1 << 1, + Format = 1 << 2, + InlineCount = 1 << 3, + OrderBy = 1 << 4, + Select = 1 << 5, + Skip = 1 << 6, + SkipToken = 1 << 7, + Top = 1 << 8 + } + + private static readonly string ResourcesNamespace = "NuGetGallery.OData.QueryAllowed.Data"; + private HashSet _allowedOperatorPatterns = null; + + /// + /// Initialization for a query filter. + /// + /// + public ODataQueryFilter(string fileName) + { + var assembly = Assembly.GetExecutingAssembly(); + using (var sr = new StreamReader(assembly.GetManifestResourceStream($"{ResourcesNamespace}.{fileName}"))) + { + var json = sr.ReadToEnd(); + var data = JsonConvert.DeserializeObject(json); + _allowedOperatorPatterns = new HashSet(data.AllowedOperatorPatterns + .Select((op) => { return (ODataOperators)Enum.Parse(typeof(ODataOperators), op, ignoreCase:true); })); + if (!_allowedOperatorPatterns.Contains(ODataOperators.None)) + { + _allowedOperatorPatterns.Add(ODataOperators.None); + } + } + } + + public ODataQueryFilter() + { + } + + /// + /// Verifies if queryFormat is allowed. + /// + /// The to be validated. + /// Returns true if the queryFormat is allowed. + public virtual bool IsAllowed(ODataQueryOptions odataOptions) + { + return odataOptions == null ? true : _allowedOperatorPatterns.Contains(ODataOptionsMap(odataOptions)); + } + + /// + /// The allowed operators for this API + /// + public HashSet AllowedOperatorPatterns => _allowedOperatorPatterns; + + /// + /// Parses used parameters and returns + /// that represents the set of operators used by this odataOptions. + /// + /// The entity type for the odataOptions. + /// The to be validated. + /// The representation of the operators in the OData options. + /// If no operator is used the result will be . + public static ODataOperators ODataOptionsMap(ODataQueryOptions odataOptions) + { + if(odataOptions == null) + { + return 0; + } + ODataOperators result = ODataOperators.None; + + foreach (var odataOperator in Enum.GetNames(typeof(ODataOperators))) + { + var rawValuesProperty = typeof(ODataRawQueryOptions).GetProperty(odataOperator); + if (rawValuesProperty != null && rawValuesProperty.GetValue(odataOptions.RawValues, null) != null) + { + result |= (ODataOperators)Enum.Parse(typeof(ODataOperators), odataOperator, ignoreCase:true); + } + } + + return result; + } + } +} diff --git a/src/NuGetGallery/OData/QueryAllowed/ODataQueryRequest.cs b/src/NuGetGallery/OData/QueryAllowed/ODataQueryRequest.cs new file mode 100644 index 0000000000..fc4ec8c2f2 --- /dev/null +++ b/src/NuGetGallery/OData/QueryAllowed/ODataQueryRequest.cs @@ -0,0 +1,17 @@ + +// 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.OData.QueryFilter +{ + public class ODataQueryRequest + { + public List AllowedOperatorPatterns + { + get; + set; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/OData/QueryAllowed/ODataQueryVerifier.cs b/src/NuGetGallery/OData/QueryAllowed/ODataQueryVerifier.cs new file mode 100644 index 0000000000..b15605742d --- /dev/null +++ b/src/NuGetGallery/OData/QueryAllowed/ODataQueryVerifier.cs @@ -0,0 +1,149 @@ +// 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.Web.Http.OData.Query; + +namespace NuGetGallery.OData.QueryFilter +{ + public class ODataQueryVerifier + { + private static Lazy _v2GetUpdates = + new Lazy(() => { return new ODataQueryFilter("apiv2getupdates.json"); }, isThreadSafe: true); + private static Lazy _v2Packages = + new Lazy(() =>{ return new ODataQueryFilter("apiv2packages.json"); }, isThreadSafe: true); + private static Lazy _v2Search = + new Lazy(() =>{ return new ODataQueryFilter("apiv2search.json"); }, isThreadSafe: true); + private static Lazy _v1Packages = + new Lazy(() => { return new ODataQueryFilter("apiv1packages.json"); }, isThreadSafe: true); + private static Lazy _v1Search = + new Lazy(() => { return new ODataQueryFilter("apiv1search.json");}, isThreadSafe: true); + + private static ITelemetryService _telemetryService = new TelemetryService(); + + #region Filters for ODataV2FeedController + /// + /// The OData query filter for /api/v2/GetUpdates(). + /// + public static ODataQueryFilter V2GetUpdates + { + get + { + return _v2GetUpdates.Value; + } + set + { + _v2GetUpdates = new Lazy(() => { return value; }); + } + } + + /// + /// The OData query filter for /api/v2/Packages. + /// + public static ODataQueryFilter V2Packages + { + get + { + return _v2Packages.Value; + } + set + { + _v2Packages = new Lazy(() => { return value; }); + } + } + + /// + /// The OData query filter for /api/v2/Search(). + /// + public static ODataQueryFilter V2Search + { + get + { + return _v2Search.Value; + } + set + { + _v2Search = new Lazy(() => { return value; }); + } + } + #endregion Filters for ODataV2FeedController + + #region Filters for ODataV1FeedController + /// + /// The OData query filter for /api/v1/Packages. + /// + public static ODataQueryFilter V1Packages + { + get + { + return _v1Packages.Value; + } + set + { + _v1Packages = new Lazy(() => { return value; }); + } + } + + /// + /// The OData query filter for /api/v1/Search() + /// + public static ODataQueryFilter V1Search + { + get + { + return _v1Search.Value; + } + set + { + _v1Search = new Lazy(() => { return value; }); + } + } + #endregion Filters for ODataV1FeedController + + /// + /// Verifies whether or not the are allowed or not. + /// + /// + /// The odata options from the request. + /// Each web api will have their individual allowed set of query strutures. + /// The configuration state for the feature. + /// Information to be used by the telemetry events. + /// True if the options are allowed. + public static bool AreODataOptionsAllowed(ODataQueryOptions odataOptions, + ODataQueryFilter allowedQueryStructure, + bool isFeatureEnabled, + string telemetryContext) + { + // If validation of the ODataQueryOptions fails, we will not reject the request. + var isAllowed = true; + + try + { + isAllowed = allowedQueryStructure.IsAllowed(odataOptions); + } + 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.TrackODataQueryFilterEvent( + callContext: $"{telemetryContext}:{nameof(AreODataOptionsAllowed)}", + isEnabled: isFeatureEnabled, + isAllowed: isAllowed, + queryPattern: ODataQueryFilter.ODataOptionsMap(odataOptions).ToString()); + + return isFeatureEnabled ? isAllowed : true; + } + + internal static string GetValidationFailedMessage(ODataQueryOptions options) + { + return $"A query with \"{ODataQueryFilter.ODataOptionsMap(options)}\" set of operators is not supported. Please refer to : https://github.com/NuGet/Home/wiki/Filter-OData-query-requests for additional information."; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/OData/SearchService/SearchAdaptor.cs b/src/NuGetGallery/OData/SearchService/SearchAdaptor.cs index b522b4c03d..72a411a367 100644 --- a/src/NuGetGallery/OData/SearchService/SearchAdaptor.cs +++ b/src/NuGetGallery/OData/SearchService/SearchAdaptor.cs @@ -99,12 +99,14 @@ public static async Task FindByIdAndVersionCore( // We can only use Lucene if: // a) The Index contains all versions of each package // b) The sort order is something Lucene can handle - if (TryReadSearchFilter(searchService.ContainsAllVersions, request.RawUrl, searchService.ContainsAllVersions, out searchFilter)) + if (TryReadSearchFilter(searchService.ContainsAllVersions, request.RawUrl, searchService.ContainsAllVersions, out searchFilter) && !string.IsNullOrWhiteSpace(id)) { - var searchTerm = string.Format(CultureInfo.CurrentCulture, "Id:\"{0}\"", id); + var normalizedRegistrationId = id.Normalize(NormalizationForm.FormC); + + var searchTerm = string.Format(CultureInfo.CurrentCulture, "Id:\"{0}\"", normalizedRegistrationId); if (!string.IsNullOrEmpty(version)) { - searchTerm = string.Format(CultureInfo.CurrentCulture, "Id:\"{0}\" AND Version:\"{1}\"", id, version); + searchTerm = string.Format(CultureInfo.CurrentCulture, "Id:\"{0}\" AND Version:\"{1}\"", normalizedRegistrationId, version); searchFilter.Take = 1; // only one result is needed in this case } diff --git a/src/NuGetGallery/OData/SearchService/SearchHijacker.cs b/src/NuGetGallery/OData/SearchService/SearchHijacker.cs index b328ed127a..02e4f3f750 100644 --- a/src/NuGetGallery/OData/SearchService/SearchHijacker.cs +++ b/src/NuGetGallery/OData/SearchService/SearchHijacker.cs @@ -8,7 +8,9 @@ using System.Reflection; using System.Web.Http.OData.Query; using Microsoft.Data.Edm; +using Microsoft.Data.OData; using Microsoft.Data.OData.Query; +using Microsoft.Data.OData.Query.SemanticAst; using NuGetGallery.WebApi; namespace NuGetGallery.OData @@ -29,17 +31,11 @@ public class SearchHijacker public static bool IsHijackable(ODataQueryOptions options, out HijackableQueryParameters hijackable) { - if (options.Filter?.FilterClause != null - && options.Filter.FilterClause.Expression.Kind == QueryNodeKind.SingleValueFunctionCall - && options.Filter.FilterClause.ItemType.Definition.TypeKind == EdmTypeKind.Entity) + // Check if we can process the filter clause + if (!CanProcessFilterClause(options)) { - var functionCallExpression = (SingleValueFunctionCallNode) options.Filter.FilterClause.Expression; - if (string.Equals(functionCallExpression.Name, "substringof", StringComparison.OrdinalIgnoreCase)) - { - // The 'substringof' function cannot be applied to an enumeration-typed argument - hijackable = null; - return false; - } + hijackable = null; + return false; } // Build expression (this works around all internal classes in the OData library - all we want is an expression tree) @@ -93,6 +89,58 @@ public static bool IsHijackable(ODataQueryOptions options, out Hi return false; } + private static bool CanProcessFilterClause(ODataQueryOptions options) + { + // Check if we can read the filter clause + try + { + var dummy = options.Filter?.FilterClause; + } + catch (ODataException) + { + // If that fails, we can't process the filter clause + return false; + } + + // If the filter clause can be read, it may not be a valid expression tree. + // Example is '/api/v2/Packages?$filter=substringof(null,Id)' which throws ODataException: + // "The 'substringof' function cannot be applied to an enumeration-typed argument." + // Skip hijacking queries that use a substringof filter due to the cost of validating PropertyAccess nodes + if (options.Filter?.FilterClause != null + && options.Filter.FilterClause.ItemType.Definition.TypeKind == EdmTypeKind.Entity) + { + var current = options.Filter.FilterClause.Expression; + while (current.Kind == QueryNodeKind.BinaryOperator) + { + var currentBinaryNode = ((BinaryOperatorNode)current); + if (IsSubstringOfFunctionCall(currentBinaryNode.Right)) + { + return false; + } + current = currentBinaryNode.Left; + } + return !IsSubstringOfFunctionCall(current); + } + + return true; + } + + private static bool IsSubstringOfFunctionCall(SingleValueNode expression) + { + // If necessary, unwrap SingleValueFunctionCall from ConvertNode + if (expression.Kind == QueryNodeKind.Convert) + { + expression = ((ConvertNode)expression).Source; + } + + if (expression.Kind == QueryNodeKind.SingleValueFunctionCall) + { + var functionCallExpression = (SingleValueFunctionCallNode)expression; + return string.Equals(functionCallExpression.Name, "substringof", StringComparison.OrdinalIgnoreCase); + } + return false; + } + private static IEnumerable> ExtractComparison(MethodCallExpression outerWhere) { // We expect to see an expression that looks like this: diff --git a/src/NuGetGallery/PackageCurators/WebMatrixPackageCurator.cs b/src/NuGetGallery/PackageCurators/WebMatrixPackageCurator.cs index 5a9cab8da1..6631c30026 100644 --- a/src/NuGetGallery/PackageCurators/WebMatrixPackageCurator.cs +++ b/src/NuGetGallery/PackageCurators/WebMatrixPackageCurator.cs @@ -62,7 +62,7 @@ internal static bool ShouldCuratePackage( // Must have AspNetWebPages tag ContainsAspNetWebPagesTag(galleryPackage) || - // OR: Must not contain powershell or T4 + // OR: Must not contain PowerShell or T4 DoesNotContainUnsupportedFiles(packageArchiveReader) ) && diff --git a/src/NuGetGallery/Properties/AssemblyInfo.cs b/src/NuGetGallery/Properties/AssemblyInfo.cs index ec3e69515b..fe69a5db38 100644 --- a/src/NuGetGallery/Properties/AssemblyInfo.cs +++ b/src/NuGetGallery/Properties/AssemblyInfo.cs @@ -1,7 +1,37 @@ // 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.Reflection; +using System.Resources; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; [assembly: AssemblyTitle("NuGetGallery")] -[assembly: InternalsVisibleTo("NuGetGallery.Facts")] \ No newline at end of file +[assembly: InternalsVisibleTo("NuGetGallery.Facts")] + +[assembly: AssemblyCompany(".NET Foundation")] +[assembly: AssemblyProduct("NuGet Services")] +[assembly: AssemblyCopyright("\x00a9 .NET Foundation. All rights reserved.")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: InternalsVisibleTo("NuGetGallery.Facts")] + +#if !PORTABLE +[assembly: ComVisible(false)] +#endif + +#if DEBUG +[assembly: AssemblyConfiguration("Debug")] +#else +[assembly: AssemblyConfiguration("Release")] +#endif + +[assembly: CLSCompliant(false)] +[assembly: NeutralResourcesLanguage("en-us")] + +// The build will automatically inject the following attributes: +// AssemblyVersion, AssemblyFileVersion, AssemblyInformationalVersion, AssemblyMetadata (for Branch, CommitId, and BuildDateUtc) + +[assembly: AssemblyMetadata("RepositoryUrl", "https://www.github.com/NuGet/NuGetGallery")] diff --git a/src/NuGetGallery/Public/Blocked.html b/src/NuGetGallery/Public/Blocked.html index 89d00108bd..baafea9d65 100644 --- a/src/NuGetGallery/Public/Blocked.html +++ b/src/NuGetGallery/Public/Blocked.html @@ -18,7 +18,7 @@
  • Home
  • Packages
  • Documentation
  • -
  • Upload Package
  • +
  • Upload Package
  • diff --git a/src/NuGetGallery/Public/Error.html b/src/NuGetGallery/Public/Error.html index 40cf6c43dd..22669084a3 100644 --- a/src/NuGetGallery/Public/Error.html +++ b/src/NuGetGallery/Public/Error.html @@ -18,7 +18,7 @@
  • Home
  • Packages
  • Documentation
  • -
  • Upload Package
  • +
  • Upload Package
  • diff --git a/src/NuGetGallery/Public/clientaccesspolicy.xml b/src/NuGetGallery/Public/clientaccesspolicy.xml index a2683ab13a..aee9221f88 100644 --- a/src/NuGetGallery/Public/clientaccesspolicy.xml +++ b/src/NuGetGallery/Public/clientaccesspolicy.xml @@ -1,4 +1,4 @@ - + diff --git a/src/NuGetGallery/Public/favicon.ico b/src/NuGetGallery/Public/favicon.ico index eba7b28eaf..903a535d2c 100644 Binary files a/src/NuGetGallery/Public/favicon.ico and b/src/NuGetGallery/Public/favicon.ico differ diff --git a/src/NuGetGallery/Public/opensearch.xml b/src/NuGetGallery/Public/opensearch.xml index 7df746afb0..579c6f57e4 100644 --- a/src/NuGetGallery/Public/opensearch.xml +++ b/src/NuGetGallery/Public/opensearch.xml @@ -1,4 +1,4 @@ - + NuGet packages Search NuGet packages nuget package component @@ -7,4 +7,4 @@ UTF-8 https://www.nuget.org/favicon.ico - \ No newline at end of file + diff --git a/src/NuGetGallery/Queries/AutoCompleteDatabasePackageIdsQuery.cs b/src/NuGetGallery/Queries/AutoCompleteDatabasePackageIdsQuery.cs new file mode 100644 index 0000000000..968d18437e --- /dev/null +++ b/src/NuGetGallery/Queries/AutoCompleteDatabasePackageIdsQuery.cs @@ -0,0 +1,52 @@ +// 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.Globalization; +using System.Threading.Tasks; + +namespace NuGetGallery +{ + public class AutoCompleteDatabasePackageIdsQuery + : AutoCompleteDatabaseQuery, IAutoCompletePackageIdsQuery + { + private const string _partialIdSqlFormat = @"SELECT TOP 30 pr.ID +FROM Packages p (NOLOCK) + JOIN PackageRegistrations pr (NOLOCK) on pr.[Key] = p.PackageRegistrationKey +WHERE pr.ID LIKE {{0}} + {0} +GROUP BY pr.ID +ORDER BY pr.ID"; + + private const string _noPartialIdSql = @"SELECT TOP 30 pr.ID +FROM Packages p (NOLOCK) + JOIN PackageRegistrations pr (NOLOCK) on pr.[Key] = p.PackageRegistrationKey +GROUP BY pr.ID +ORDER BY MAX(pr.DownloadCount) DESC"; + + public AutoCompleteDatabasePackageIdsQuery(IEntitiesContext entities) + : base(entities) + { + } + + public Task> Execute( + string partialId, + bool? includePrerelease = false) + { + if (string.IsNullOrWhiteSpace(partialId)) + { + return RunQuery(_noPartialIdSql); + } + + var prereleaseFilter = string.Empty; + if (!includePrerelease.HasValue || !includePrerelease.Value) + { + prereleaseFilter = "AND p.IsPrerelease = {1}"; + } + + var sql = string.Format(CultureInfo.InvariantCulture, _partialIdSqlFormat, prereleaseFilter); + + return RunQuery(sql, partialId + "%", includePrerelease ?? false); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Queries/AutoCompleteDatabasePackageVersionsQuery.cs b/src/NuGetGallery/Queries/AutoCompleteDatabasePackageVersionsQuery.cs new file mode 100644 index 0000000000..c118222698 --- /dev/null +++ b/src/NuGetGallery/Queries/AutoCompleteDatabasePackageVersionsQuery.cs @@ -0,0 +1,43 @@ +// 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.Globalization; +using System.Threading.Tasks; + +namespace NuGetGallery +{ + public class AutoCompleteDatabasePackageVersionsQuery + : AutoCompleteDatabaseQuery, IAutoCompletePackageVersionsQuery + { + private const string _sqlFormat = @"SELECT p.[Version] +FROM Packages p (NOLOCK) + JOIN PackageRegistrations pr (NOLOCK) on pr.[Key] = p.PackageRegistrationKey +WHERE pr.ID = {{0}} + {0}"; + + public AutoCompleteDatabasePackageVersionsQuery(IEntitiesContext entities) + : base(entities) + { + } + + public Task> Execute( + string id, + bool? includePrerelease = false) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException(nameof(id)); + } + + var prereleaseFilter = string.Empty; + if (!includePrerelease.HasValue || !includePrerelease.Value) + { + prereleaseFilter = "AND p.IsPrerelease = 0"; + } + + return RunQuery(string.Format(CultureInfo.InvariantCulture, _sqlFormat, prereleaseFilter), id); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Queries/AutoCompleteDatabaseQuery.cs b/src/NuGetGallery/Queries/AutoCompleteDatabaseQuery.cs new file mode 100644 index 0000000000..3d24af2ea2 --- /dev/null +++ b/src/NuGetGallery/Queries/AutoCompleteDatabaseQuery.cs @@ -0,0 +1,31 @@ +// 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.Data.Entity; +using System.Linq; +using System.Threading.Tasks; + +namespace NuGetGallery +{ + public class AutoCompleteDatabaseQuery + { + private readonly DbContext _dbContext; + + public AutoCompleteDatabaseQuery(IEntitiesContext entities) + { + if (entities == null) + { + throw new ArgumentNullException(nameof(entities)); + } + + _dbContext = (DbContext)entities; + } + + public Task> RunQuery(string sql, params object[] sqlParameters) + { + return Task.FromResult(_dbContext.Database.SqlQuery(sql, sqlParameters).AsEnumerable()); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Queries/AutoCompleteServiceQuery.cs b/src/NuGetGallery/Queries/AutoCompleteServiceQuery.cs new file mode 100644 index 0000000000..13b8f623e8 --- /dev/null +++ b/src/NuGetGallery/Queries/AutoCompleteServiceQuery.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. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NuGet.Services.Search.Client; +using NuGetGallery.Configuration; + +namespace NuGetGallery +{ + public class AutoCompleteServiceQuery + { + private readonly ServiceDiscoveryClient _serviceDiscoveryClient; + private readonly string _autocompleteServiceResourceType; + private readonly RetryingHttpClientWrapper _httpClient; + + public AutoCompleteServiceQuery(IAppConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + _serviceDiscoveryClient = new ServiceDiscoveryClient(configuration.ServiceDiscoveryUri); + _autocompleteServiceResourceType = configuration.AutocompleteServiceResourceType; + _httpClient = new RetryingHttpClientWrapper(new HttpClient()); + } + + public async Task> RunQuery(string queryString, bool? includePrerelease) + { + queryString += $"&prerelease={includePrerelease ?? false}"; + var endpoints = await _serviceDiscoveryClient.GetEndpointsForResourceType(_autocompleteServiceResourceType); + endpoints = endpoints.Select(e => new Uri(e + "?" + queryString)).AsEnumerable(); + + var result = await _httpClient.GetStringAsync(endpoints); + var resultObject = JObject.Parse(result); + + return resultObject["data"].Select(entry => entry.ToString()); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Queries/AutocompleteServicePackageIdsQuery.cs b/src/NuGetGallery/Queries/AutocompleteServicePackageIdsQuery.cs new file mode 100644 index 0000000000..b9c97bf283 --- /dev/null +++ b/src/NuGetGallery/Queries/AutocompleteServicePackageIdsQuery.cs @@ -0,0 +1,28 @@ +// 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.Threading.Tasks; +using NuGetGallery.Configuration; + +namespace NuGetGallery +{ + public class AutoCompleteServicePackageIdsQuery + : AutoCompleteServiceQuery, IAutoCompletePackageIdsQuery + { + public AutoCompleteServicePackageIdsQuery(IAppConfiguration configuration) + : base(configuration) + { + } + + public async Task> Execute( + string partialId, + bool? includePrerelease) + { + partialId = partialId ?? string.Empty; + + return await RunQuery("take=30&q=" + Uri.EscapeUriString(partialId), includePrerelease); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Queries/AutocompleteServicePackageVersionsQuery.cs b/src/NuGetGallery/Queries/AutocompleteServicePackageVersionsQuery.cs new file mode 100644 index 0000000000..87395ecdb3 --- /dev/null +++ b/src/NuGetGallery/Queries/AutocompleteServicePackageVersionsQuery.cs @@ -0,0 +1,31 @@ +// 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.Threading.Tasks; +using NuGetGallery.Configuration; + +namespace NuGetGallery +{ + public class AutoCompleteServicePackageVersionsQuery + : AutoCompleteServiceQuery, IAutoCompletePackageVersionsQuery + { + public AutoCompleteServicePackageVersionsQuery(IAppConfiguration configuration) + : base(configuration) + { + } + + public async Task> Execute( + string id, + bool? includePrerelease) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException(nameof(id)); + } + + return await RunQuery("id=" + Uri.EscapeUriString(id), includePrerelease); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Operations/Infrastructure/IDbExecutorFactory.cs b/src/NuGetGallery/Queries/IAutoCompletePackageIdsQuery.cs similarity index 51% rename from src/NuGetGallery.Operations/Infrastructure/IDbExecutorFactory.cs rename to src/NuGetGallery/Queries/IAutoCompletePackageIdsQuery.cs index 939de17f95..5d59ebd9d0 100644 --- a/src/NuGetGallery.Operations/Infrastructure/IDbExecutorFactory.cs +++ b/src/NuGetGallery/Queries/IAutoCompletePackageIdsQuery.cs @@ -1,16 +1,15 @@ // 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.Text; using System.Threading.Tasks; -using AnglicanGeek.DbExecutor; -namespace NuGetGallery.Operations.Infrastructure +namespace NuGetGallery { - public interface IDbExecutorFactory + public interface IAutoCompletePackageIdsQuery { - IDbExecutor OpenConnection(string connectionString); + Task> Execute( + string partialId, + bool? includePrerelease = false); } -} +} \ No newline at end of file diff --git a/src/NuGetGallery/Queries/IAutoCompletePackageVersionsQuery.cs b/src/NuGetGallery/Queries/IAutoCompletePackageVersionsQuery.cs new file mode 100644 index 0000000000..b5acb174fe --- /dev/null +++ b/src/NuGetGallery/Queries/IAutoCompletePackageVersionsQuery.cs @@ -0,0 +1,15 @@ +// 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.Threading.Tasks; + +namespace NuGetGallery +{ + public interface IAutoCompletePackageVersionsQuery + { + Task> Execute( + string id, + bool? includePrerelease = false); + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Queries/PackageIdsQuery.cs b/src/NuGetGallery/Queries/PackageIdsQuery.cs deleted file mode 100644 index 2729ccee50..0000000000 --- a/src/NuGetGallery/Queries/PackageIdsQuery.cs +++ /dev/null @@ -1,107 +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 System.Data.Entity; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using NuGet.Services.Search.Client; -using NuGetGallery.Configuration; - -namespace NuGetGallery -{ - public interface IPackageIdsQuery - { - Task> Execute( - string partialId, - bool? includePrerelease = false); - } - - public class AutocompleteServicePackageIdsQuery : IPackageIdsQuery - { - private readonly ServiceDiscoveryClient _serviceDiscoveryClient; - private readonly string _autocompleteServiceResourceType; - private readonly RetryingHttpClientWrapper _httpClient; - - public AutocompleteServicePackageIdsQuery(IAppConfiguration configuration) - { - _serviceDiscoveryClient = new ServiceDiscoveryClient(configuration.ServiceDiscoveryUri); - _autocompleteServiceResourceType = configuration.AutocompleteServiceResourceType; - _httpClient = new RetryingHttpClientWrapper(new HttpClient()); - } - - public async Task> Execute(string partialId, bool? includePrerelease) - { - if (partialId == null) - { - partialId = string.Empty; - } - - var queryString = "take=30&q=" + Uri.EscapeUriString(partialId); - if (!includePrerelease.HasValue) - { - queryString += "&prerelease=false"; - } - else - { - queryString += "&prerelease=" + includePrerelease.Value; - } - - var endpoints = await _serviceDiscoveryClient.GetEndpointsForResourceType(_autocompleteServiceResourceType); - endpoints = endpoints.Select(e => new Uri(e + "?" + queryString)).AsEnumerable(); - - var result = await _httpClient.GetStringAsync(endpoints); - var resultObject = JObject.Parse(result); - - return resultObject["data"].Select(entry => entry.ToString()); - } - } - - public class PackageIdsQuery : IPackageIdsQuery - { - private const string PartialIdSqlFormat = @"SELECT TOP 30 pr.ID -FROM Packages p - JOIN PackageRegistrations pr on pr.[Key] = p.PackageRegistrationKey -WHERE pr.ID LIKE {{0}} - {0} -GROUP BY pr.ID -ORDER BY pr.ID"; - - private const string NoPartialIdSql = @"SELECT TOP 30 pr.ID -FROM Packages p - JOIN PackageRegistrations pr on pr.[Key] = p.PackageRegistrationKey -GROUP BY pr.ID -ORDER BY MAX(pr.DownloadCount) DESC"; - - private readonly IEntitiesContext _entities; - - public PackageIdsQuery(IEntitiesContext entities) - { - _entities = entities; - } - - public Task> Execute( - string partialId, - bool? includePrerelease = false) - { - var dbContext = (DbContext)_entities; - - if (String.IsNullOrWhiteSpace(partialId)) - { - return Task.FromResult(dbContext.Database.SqlQuery(NoPartialIdSql).AsEnumerable()); - } - - var prereleaseFilter = String.Empty; - if (!includePrerelease.HasValue || !includePrerelease.Value) - { - prereleaseFilter = "AND p.IsPrerelease = {1}"; - } - return Task.FromResult(dbContext.Database.SqlQuery( - String.Format(CultureInfo.InvariantCulture, PartialIdSqlFormat, prereleaseFilter), partialId + "%", includePrerelease ?? false).AsEnumerable()); - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery/Queries/PackageVersionsQuery.cs b/src/NuGetGallery/Queries/PackageVersionsQuery.cs deleted file mode 100644 index 6e3223d3a0..0000000000 --- a/src/NuGetGallery/Queries/PackageVersionsQuery.cs +++ /dev/null @@ -1,99 +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 System.Data.Entity; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using NuGet.Services.Search.Client; -using NuGetGallery.Configuration; - -namespace NuGetGallery -{ - public interface IPackageVersionsQuery - { - Task> Execute( - string id, - bool? includePrerelease = false); - } - - public class AutocompleteServicePackageVersionsQuery : IPackageVersionsQuery - { - private readonly ServiceDiscoveryClient _serviceDiscoveryClient; - private readonly string _autocompleteServiceResourceType; - private readonly RetryingHttpClientWrapper _httpClient; - - public AutocompleteServicePackageVersionsQuery(IAppConfiguration configuration) - { - _serviceDiscoveryClient = new ServiceDiscoveryClient(configuration.ServiceDiscoveryUri); - _autocompleteServiceResourceType = configuration.AutocompleteServiceResourceType; - _httpClient = new RetryingHttpClientWrapper(new HttpClient()); - } - - public async Task> Execute(string id, bool? includePrerelease) - { - if (string.IsNullOrWhiteSpace(id)) - { - throw new ArgumentNullException(nameof(id)); - } - - var queryString = "id=" + Uri.EscapeUriString(id); - if (!includePrerelease.HasValue) - { - queryString += "&prerelease=false"; - } - else - { - queryString += "&prerelease=" + includePrerelease.Value; - } - - var endpoints = await _serviceDiscoveryClient.GetEndpointsForResourceType(_autocompleteServiceResourceType); - endpoints = endpoints.Select(e => new Uri(e + "?" + queryString)).AsEnumerable(); - - var result = await _httpClient.GetStringAsync(endpoints); - var resultObject = JObject.Parse(result); - - return resultObject["data"].Select(entry => entry.ToString()); - } - } - - public class PackageVersionsQuery : IPackageVersionsQuery - { - private const string SqlFormat = @"SELECT p.[Version] -FROM Packages p - JOIN PackageRegistrations pr on pr.[Key] = p.PackageRegistrationKey -WHERE pr.ID = {{0}} - {0}"; - - private readonly IEntitiesContext _entities; - - public PackageVersionsQuery(IEntitiesContext entities) - { - _entities = entities; - } - - public Task> Execute( - string id, - bool? includePrerelease = false) - { - if (String.IsNullOrWhiteSpace(id)) - { - throw new ArgumentNullException(nameof(id)); - } - - var dbContext = (DbContext)_entities; - - var prereleaseFilter = String.Empty; - if (!includePrerelease.HasValue || !includePrerelease.Value) - { - prereleaseFilter = "AND p.IsPrerelease = 0"; - } - return Task.FromResult(dbContext.Database.SqlQuery( - String.Format(CultureInfo.InvariantCulture, SqlFormat, prereleaseFilter), id).AsEnumerable()); - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery/RequestModels/EditPackageVersionRequest.cs b/src/NuGetGallery/RequestModels/EditPackageVersionRequest.cs index 53bfa205b6..ed7926fadf 100644 --- a/src/NuGetGallery/RequestModels/EditPackageVersionRequest.cs +++ b/src/NuGetGallery/RequestModels/EditPackageVersionRequest.cs @@ -1,6 +1,11 @@ // 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.ComponentModel.DataAnnotations; +using System.Text; +using NuGetGallery.Packaging; namespace NuGetGallery { @@ -103,5 +108,27 @@ public EditPackageVersionRequest(Package package, PackageEdit pendingMetadata) [Display(Name = RequiresLicenseAcceptanceStr)] public bool RequiresLicenseAcceptance { get; set; } + + /// + /// Applied the edit to a package + /// + /// Package to apply edits to + /// Edited package + public Package ApplyTo(Package package) + { + package.FlattenedAuthors = Authors; + package.Copyright = Copyright; + package.Description = Description; + package.IconUrl = IconUrl; + package.LicenseUrl = LicenseUrl; + package.ProjectUrl = ProjectUrl; + package.ReleaseNotes = ReleaseNotes; + package.RequiresLicenseAcceptance = RequiresLicenseAcceptance; + package.Summary = Summary; + package.Tags = Tags; + package.Title = VersionTitle; + + return package; + } } } diff --git a/src/NuGetGallery/RouteNames.cs b/src/NuGetGallery/RouteNames.cs index d1cedfe161..9599998796 100644 --- a/src/NuGetGallery/RouteNames.cs +++ b/src/NuGetGallery/RouteNames.cs @@ -35,6 +35,7 @@ public static class RouteName public const string PasswordSet = "PasswordSet"; public const string NewSubmission = "NewSubmission"; public const string VerifyPackage = "VerifyPackage"; + public const string CreatePackageVerificationKey = "CreatePackageVerificationKey"; public const string VerifyPackageKey = "VerifyPackageKey"; public const string CancelUpload = "CancelUpload"; public const string CuratedFeed = "CuratedFeed"; diff --git a/src/NuGetGallery/Scripts/nugetgallery.js b/src/NuGetGallery/Scripts/nugetgallery.js index 02f7c26211..c6bacc0beb 100644 --- a/src/NuGetGallery/Scripts/nugetgallery.js +++ b/src/NuGetGallery/Scripts/nugetgallery.js @@ -70,7 +70,7 @@ }); $('.s-confirm[data-confirm]').delegate('', 'click', function (evt) { if (!confirm($(this).data().confirm)) { - evt.preventDefault(); + evt.stopPropagation(); } }); if (!hasMimeTypeSupport("application/x-shockwave-flash")) { @@ -86,7 +86,7 @@ } ampm = "PM"; } - $(this).text(utc.getFullYear() + "-" + padInt(utc.getMonth() + 1, 2) + "-" + padInt(utc.getDate(), 2) + " " + hrs + ":" + padInt(utc.getMinutes(), 2) + " " + ampm + " Local Time"); + $(this).text(utc.getFullYear() + "-" + padInt(utc.getMonth() + 1, 2) + "-" + padInt(utc.getDate(), 2) + " " + hrs + ":" + padInt(utc.getMinutes(), 2) + " " + ampm + " (UTC)"); }); $('time.timeago').timeago(); } diff --git a/src/NuGetGallery/Scripts/stats.js b/src/NuGetGallery/Scripts/stats.js index 96c14e26f5..e44c47e2e9 100644 --- a/src/NuGetGallery/Scripts/stats.js +++ b/src/NuGetGallery/Scripts/stats.js @@ -65,5 +65,4 @@ $(document).ready(function () { if (elem != null && elem.length > 0) { getStats(); } - }); diff --git a/src/NuGetGallery/Scripts/statsdimensions.js b/src/NuGetGallery/Scripts/statsdimensions.js index 7e92f5b763..22fe029b03 100644 --- a/src/NuGetGallery/Scripts/statsdimensions.js +++ b/src/NuGetGallery/Scripts/statsdimensions.js @@ -1,8 +1,5 @@ - -var groupbyNavigation = function () { - +var groupbyNavigation = function () { $('.dimension-checkbox').click(function () { - $('#dimension-form').submit(); }); } \ No newline at end of file diff --git a/src/NuGetGallery/Scripts/statsgraphs.js b/src/NuGetGallery/Scripts/statsgraphs.js index b73436d0e7..719210ddd9 100644 --- a/src/NuGetGallery/Scripts/statsgraphs.js +++ b/src/NuGetGallery/Scripts/statsgraphs.js @@ -1,5 +1,4 @@ - -var drawNugetClientVersionBarChart = function () { +var drawNugetClientVersionBarChart = function () { var margin = { top: 20, right: 30, bottom: 80, left: 80 }, width = 460 - margin.left - margin.right, diff --git a/src/NuGetGallery/Scripts/supportrequests.js b/src/NuGetGallery/Scripts/supportrequests.js index 029a2fb70b..4e1c49e01b 100644 --- a/src/NuGetGallery/Scripts/supportrequests.js +++ b/src/NuGetGallery/Scripts/supportrequests.js @@ -208,6 +208,10 @@ function SupportRequestsViewModel(editUrl, filterUrl, historyUrl) { return '#'; } + this.generatePackageDetailsUrl = function(supportRequestViewModel) { + return supportRequestViewModel.SiteRoot + 'packages/' + supportRequestViewModel.PackageId + '/' + supportRequestViewModel.PackageVersion; + } + this.generateHistoryUrl = function (supportRequestViewModel) { return $self.historyUrl + '?id=' + supportRequestViewModel.Key; } diff --git a/src/NuGetGallery/Services/CloudBlobFileStorageService.cs b/src/NuGetGallery/Services/CloudBlobFileStorageService.cs index d738cd7b38..81a3bc0056 100644 --- a/src/NuGetGallery/Services/CloudBlobFileStorageService.cs +++ b/src/NuGetGallery/Services/CloudBlobFileStorageService.cs @@ -100,11 +100,22 @@ public async Task GetFileReferenceAsync(string folderName, strin } } - public async Task SaveFileAsync(string folderName, string fileName, Stream packageFile) + public async Task SaveFileAsync(string folderName, string fileName, Stream packageFile, bool overwrite = true) { ICloudBlobContainer container = await GetContainer(folderName); var blob = container.GetBlobReference(fileName); - await blob.DeleteIfExistsAsync(); + + if (overwrite) + { + await blob.DeleteIfExistsAsync(); + } + else if (await blob.ExistsAsync()) + { + throw new InvalidOperationException( + String.Format(CultureInfo.CurrentCulture, "There is already a blob with name {0} in container {1}.", + fileName, folderName)); + } + await blob.UploadFromStreamAsync(packageFile); blob.Properties.ContentType = GetContentType(folderName); await blob.SetPropertiesAsync(); @@ -242,11 +253,29 @@ internal async Task CreateDownloadFileActionResult( internal Uri GetRedirectUri(Uri requestUrl, Uri blobUri) { - string host = String.IsNullOrEmpty(_configuration.AzureCdnHost) ? blobUri.Host : _configuration.AzureCdnHost; + var host = string.IsNullOrEmpty(_configuration.AzureCdnHost) + ? blobUri.Host + : _configuration.AzureCdnHost; + + // When a blob query string is passed, that one always wins. + // This will only happen on private NuGet gallery instances, + // not on NuGet.org. + // When no blob query string is passed, we forward the request + // URI's query string to the CDN. See https://github.com/NuGet/NuGetGallery/issues/3168 + // and related PR's. + var queryString = !string.IsNullOrEmpty(blobUri.Query) + ? blobUri.Query + : requestUrl.Query; + + if (!string.IsNullOrEmpty(queryString)) + { + queryString = queryString.TrimStart('?'); + } + var urlBuilder = new UriBuilder(requestUrl.Scheme, host) { Path = blobUri.LocalPath, - Query = blobUri.Query + Query = queryString }; return urlBuilder.Uri; diff --git a/src/NuGetGallery/Services/FileSystemFileStorageService.cs b/src/NuGetGallery/Services/FileSystemFileStorageService.cs index 55ea2b135c..4d02f3c210 100644 --- a/src/NuGetGallery/Services/FileSystemFileStorageService.cs +++ b/src/NuGetGallery/Services/FileSystemFileStorageService.cs @@ -124,7 +124,7 @@ public Task GetFileReferenceAsync(string folderName, string file return Task.FromResult(file.Exists ? new LocalFileReference(file) : null); } - public Task SaveFileAsync(string folderName, string fileName, Stream packageFile) + public Task SaveFileAsync(string folderName, string fileName, Stream packageFile, bool overwrite = true) { if (String.IsNullOrWhiteSpace(folderName)) { @@ -141,25 +141,25 @@ public Task SaveFileAsync(string folderName, string fileName, Stream packageFile throw new ArgumentNullException(nameof(packageFile)); } - var storageDirectory = ResolvePath(_configuration.FileStorageDirectory); + var filePath = BuildPath(_configuration.FileStorageDirectory, folderName, fileName); - if (!_fileSystemService.DirectoryExists(storageDirectory)) - { - _fileSystemService.CreateDirectory(storageDirectory); - } + var dirPath = System.IO.Path.GetDirectoryName(filePath); - var folderPath = Path.Combine(storageDirectory, folderName); - if (!_fileSystemService.DirectoryExists(folderPath)) - { - _fileSystemService.CreateDirectory(folderPath); - } + _fileSystemService.CreateDirectory(dirPath); - var filePath = BuildPath(_configuration.FileStorageDirectory, folderName, fileName); - folderPath = Path.GetDirectoryName(filePath); - if (!_fileSystemService.DirectoryExists(folderPath)) + if (_fileSystemService.FileExists(filePath)) { - _fileSystemService.CreateDirectory(folderPath); + if (overwrite) + { + _fileSystemService.DeleteFile(filePath); + } + else + { + throw new InvalidOperationException( + String.Format(CultureInfo.CurrentCulture, "There is already a file with name {0} in folder {1}.", fileName, folderName)); + } } + using (var file = _fileSystemService.OpenWrite(filePath)) { packageFile.CopyTo(file); @@ -181,7 +181,7 @@ private static string BuildPath(string fileStorageDirectory, string folderName, return Path.Combine(fileStorageDirectory, folderName, fileName); } - private static string ResolvePath(string fileStorageDirectory) + public static string ResolvePath(string fileStorageDirectory) { if (fileStorageDirectory.StartsWith("~/", StringComparison.OrdinalIgnoreCase) && HostingEnvironment.IsHosted) { diff --git a/src/NuGetGallery/Services/FormsAuthenticationService.cs b/src/NuGetGallery/Services/FormsAuthenticationService.cs index 18f4b67ac2..d07fa1682e 100644 --- a/src/NuGetGallery/Services/FormsAuthenticationService.cs +++ b/src/NuGetGallery/Services/FormsAuthenticationService.cs @@ -52,7 +52,9 @@ public void SetAuthCookie( if (_configuration.RequireSSL) { // Drop a second cookie indicating that the user is logged in via SSL (no secret data, just tells us to redirect them to SSL) - context.Response.Cookies.Add(new HttpCookie(ForceSSLCookieName, "true")); + HttpCookie responseCookie = new HttpCookie(ForceSSLCookieName, "true"); + responseCookie.HttpOnly = true; + context.Response.Cookies.Add(responseCookie); } } diff --git a/src/NuGetGallery/Services/IFileStorageService.cs b/src/NuGetGallery/Services/IFileStorageService.cs index 8ccf5998e5..1733d2a48e 100644 --- a/src/NuGetGallery/Services/IFileStorageService.cs +++ b/src/NuGetGallery/Services/IFileStorageService.cs @@ -26,7 +26,7 @@ public interface IFileStorageService /// A representing the file reference Task GetFileReferenceAsync(string folderName, string fileName, string ifNoneMatch = null); - Task SaveFileAsync(string folderName, string fileName, Stream packageFile); + Task SaveFileAsync(string folderName, string fileName, Stream packageFile, bool overwrite = true); Task IsAvailableAsync(); } diff --git a/src/NuGetGallery/Services/IMessageService.cs b/src/NuGetGallery/Services/IMessageService.cs index f6cd7dc4c2..de038e1b0c 100644 --- a/src/NuGetGallery/Services/IMessageService.cs +++ b/src/NuGetGallery/Services/IMessageService.cs @@ -15,7 +15,7 @@ public interface IMessageService void SendEmailChangeConfirmationNotice(MailAddress newEmailAddress, string confirmationUrl); void SendPasswordResetInstructions(User user, string resetPasswordUrl, bool forgotPassword); void SendEmailChangeNoticeToPreviousEmailAddress(User user, string oldEmailAddress); - void SendPackageOwnerRequest(User fromUser, User toUser, PackageRegistration package, string confirmationUrl); + void SendPackageOwnerRequest(User fromUser, User toUser, PackageRegistration package, string confirmationUrl, string message); void SendPackageOwnerRemovedNotice(User fromUser, User toUser, PackageRegistration package); void SendCredentialRemovedNotice(User user, Credential removed); void SendCredentialAddedNotice(User user, Credential added); diff --git a/src/NuGetGallery/Services/INuGetExeDownloaderService.cs b/src/NuGetGallery/Services/INuGetExeDownloaderService.cs index 623459e278..effd44afbf 100644 --- a/src/NuGetGallery/Services/INuGetExeDownloaderService.cs +++ b/src/NuGetGallery/Services/INuGetExeDownloaderService.cs @@ -1,10 +1,9 @@ // 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.Threading.Tasks; using System.Web.Mvc; -using NuGet; -using NuGetGallery.Packaging; namespace NuGetGallery { diff --git a/src/NuGetGallery/Services/IPackageService.cs b/src/NuGetGallery/Services/IPackageService.cs index 78cbe8deb5..ad6f3bfb10 100644 --- a/src/NuGetGallery/Services/IPackageService.cs +++ b/src/NuGetGallery/Services/IPackageService.cs @@ -13,6 +13,7 @@ public interface IPackageService PackageRegistration FindPackageRegistrationById(string id); Package FindPackageByIdAndVersion(string id, string version, bool allowPrerelease = true); IEnumerable FindPackagesByOwner(User user, bool includeUnlisted); + IEnumerable FindPackageRegistrationsByOwner(User user); IEnumerable FindDependentPackages(Package package); Task UpdateIsLatestAsync(PackageRegistration packageRegistration, bool commitChanges = true); @@ -30,6 +31,8 @@ public interface IPackageService /// The created package entity. Task CreatePackageAsync(PackageArchiveReader nugetPackage, PackageStreamMetadata packageStreamMetadata, User user, bool commitChanges = true); + Package EnrichPackageFromNuGetPackage(Package package, PackageArchiveReader packageArchive, PackageMetadata packageMetadata, PackageStreamMetadata packageStreamMetadata, User user); + Task PublishPackageAsync(string id, string version, bool commitChanges = true); Task PublishPackageAsync(Package package, bool commitChanges = true); @@ -44,5 +47,7 @@ public interface IPackageService Task SetLicenseReportVisibilityAsync(Package package, bool visible, bool commitChanges = true); void EnsureValid(PackageArchiveReader packageArchiveReader); + + Task IncrementDownloadCountAsync(string id, string version, bool commitChanges = true); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/ITelemetryService.cs b/src/NuGetGallery/Services/ITelemetryService.cs new file mode 100644 index 0000000000..306afcc5f2 --- /dev/null +++ b/src/NuGetGallery/Services/ITelemetryService.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.Security.Principal; + +namespace NuGetGallery +{ + public interface ITelemetryService + { + void TrackODataQueryFilterEvent(string callContext, bool isEnabled, bool isAllowed, string queryPattern); + + void TrackPackagePushEvent(Package package, User user, IIdentity identity); + + void TrackCreatePackageVerificationKeyEvent(string packageId, string packageVersion, User user, IIdentity identity); + + void TrackVerifyPackageKeyEvent(string packageId, string packageVersion, User user, IIdentity identity, int statusCode); + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/MessageService.cs b/src/NuGetGallery/Services/MessageService.cs index a1b743060e..add5b90ec5 100644 --- a/src/NuGetGallery/Services/MessageService.cs +++ b/src/NuGetGallery/Services/MessageService.cs @@ -195,7 +195,7 @@ We can't wait to see what packages you'll upload. { mailMessage.Subject = String.Format(CultureInfo.CurrentCulture, "[{0}] Please verify your account.", Config.GalleryOwner.DisplayName); mailMessage.Body = body; - mailMessage.From = Config.GalleryOwner; + mailMessage.From = Config.GalleryNoReplyAddress; mailMessage.To.Add(toAddress); SendMessage(mailMessage); @@ -225,7 +225,7 @@ public void SendEmailChangeConfirmationNotice(MailAddress newEmailAddress, strin mailMessage.Subject = String.Format( CultureInfo.CurrentCulture, "[{0}] Please verify your new email address.", Config.GalleryOwner.DisplayName); mailMessage.Body = body; - mailMessage.From = Config.GalleryOwner; + mailMessage.From = Config.GalleryNoReplyAddress; mailMessage.To.Add(newEmailAddress); SendMessage(mailMessage); @@ -255,7 +255,7 @@ changed from _{1}_ to _{2}_. { mailMessage.Subject = subject; mailMessage.Body = body; - mailMessage.From = Config.GalleryOwner; + mailMessage.From = Config.GalleryNoReplyAddress; mailMessage.To.Add(new MailAddress(oldEmailAddress, user.Username)); SendMessage(mailMessage); @@ -264,27 +264,28 @@ changed from _{1}_ to _{2}_. public void SendPasswordResetInstructions(User user, string resetPasswordUrl, bool forgotPassword) { - string body = String.Format( + string body = string.Format( CultureInfo.CurrentCulture, forgotPassword ? Strings.Emails_ForgotPassword_Body : Strings.Emails_SetPassword_Body, - Constants.DefaultPasswordResetTokenExpirationHours, resetPasswordUrl, Config.GalleryOwner.DisplayName); - string subject = String.Format(CultureInfo.CurrentCulture, forgotPassword ? Strings.Emails_ForgotPassword_Subject : Strings.Emails_SetPassword_Subject, Config.GalleryOwner.DisplayName); + string subject = string.Format( + CultureInfo.CurrentCulture, forgotPassword ? Strings.Emails_ForgotPassword_Subject : Strings.Emails_SetPassword_Subject, + Config.GalleryOwner.DisplayName); + using (var mailMessage = new MailMessage()) { mailMessage.Subject = subject; mailMessage.Body = body; - mailMessage.From = Config.GalleryOwner; + mailMessage.From = Config.GalleryNoReplyAddress; mailMessage.To.Add(user.ToMailAddress()); SendMessage(mailMessage); } } - - public void SendPackageOwnerRequest(User fromUser, User toUser, PackageRegistration package, string confirmationUrl) + public void SendPackageOwnerRequest(User fromUser, User toUser, PackageRegistration package, string confirmationUrl, string message) { if (!toUser.EmailAllowed) { @@ -293,23 +294,29 @@ public void SendPackageOwnerRequest(User fromUser, User toUser, PackageRegistrat const string subject = "[{0}] The user '{1}' wants to add you as an owner of the package '{2}'."; - string body = @"The user '{0}' wants to add you as an owner of the package '{1}'. + string body = string.Format(CultureInfo.CurrentCulture, $@"The user '{fromUser.Username}' wants to add you as an owner of the package '{package.Id}'. If you do not want to be listed as an owner of this package, simply delete this email. To accept this request and become a listed owner of the package, click the following URL: -[{2}]({2}) +[{confirmationUrl}]({confirmationUrl})"); -Thanks, -The {3} Team"; - body = String.Format(CultureInfo.CurrentCulture, body, fromUser.Username, package.Id, confirmationUrl, Config.GalleryOwner.DisplayName); + if (!string.IsNullOrWhiteSpace(message)) + { + body += Environment.NewLine + Environment.NewLine + string.Format(CultureInfo.CurrentCulture, $@"The user '{fromUser.Username}' added the following message for you: + +'{message}'"); + } + + body += Environment.NewLine + Environment.NewLine + $@"Thanks, +The {Config.GalleryOwner.DisplayName} Team"; using (var mailMessage = new MailMessage()) { mailMessage.Subject = String.Format(CultureInfo.CurrentCulture, subject, Config.GalleryOwner.DisplayName, fromUser.Username, package.Id); mailMessage.Body = body; - mailMessage.From = Config.GalleryOwner; + mailMessage.From = Config.GalleryNoReplyAddress; mailMessage.ReplyToList.Add(fromUser.ToMailAddress()); mailMessage.To.Add(toUser.ToMailAddress()); @@ -338,7 +345,7 @@ public void SendPackageOwnerRemovedNotice(User fromUser, User toUser, PackageReg { mailMessage.Subject = String.Format(CultureInfo.CurrentCulture, subject, Config.GalleryOwner.DisplayName, fromUser.Username, package.Id); mailMessage.Body = body; - mailMessage.From = Config.GalleryOwner; + mailMessage.From = Config.GalleryNoReplyAddress; mailMessage.ReplyToList.Add(fromUser.ToMailAddress()); mailMessage.To.Add(toUser.ToMailAddress()); @@ -348,20 +355,61 @@ public void SendPackageOwnerRemovedNotice(User fromUser, User toUser, PackageReg public void SendCredentialRemovedNotice(User user, Credential removed) { - SendCredentialChangeNotice( - user, - removed, - Strings.Emails_CredentialRemoved_Body, - Strings.Emails_CredentialRemoved_Subject); + if (CredentialTypes.IsApiKey(removed.Type)) + { + SendApiKeyChangeNotice( + user, + removed, + Strings.Emails_ApiKeyRemoved_Body, + Strings.Emails_CredentialRemoved_Subject); + } + else + { + SendCredentialChangeNotice( + user, + removed, + Strings.Emails_CredentialRemoved_Body, + Strings.Emails_CredentialRemoved_Subject); + } + } public void SendCredentialAddedNotice(User user, Credential added) { - SendCredentialChangeNotice( - user, - added, - Strings.Emails_CredentialAdded_Body, - Strings.Emails_CredentialAdded_Subject); + if (CredentialTypes.IsApiKey(added.Type)) + { + SendApiKeyChangeNotice( + user, + added, + Strings.Emails_ApiKeyAdded_Body, + Strings.Emails_CredentialAdded_Subject); + } + else + { + SendCredentialChangeNotice( + user, + added, + Strings.Emails_CredentialAdded_Body, + Strings.Emails_CredentialAdded_Subject); + } + } + + private void SendApiKeyChangeNotice(User user, Credential changed, string bodyTemplate, string subjectTemplate) + { + var credViewModel = AuthService.DescribeCredential(changed); + + string body = String.Format( + CultureInfo.CurrentCulture, + bodyTemplate, + credViewModel.Description); + + string subject = String.Format( + CultureInfo.CurrentCulture, + subjectTemplate, + Config.GalleryOwner.DisplayName, + Strings.CredentialType_ApiKey); + + SendSupportMessage(user, body, subject); } private void SendCredentialChangeNotice(User user, Credential changed, string bodyTemplate, string subjectTemplate) @@ -374,11 +422,13 @@ private void SendCredentialChangeNotice(User user, Credential changed, string bo CultureInfo.CurrentCulture, bodyTemplate, name); + string subject = String.Format( CultureInfo.CurrentCulture, subjectTemplate, Config.GalleryOwner.DisplayName, name); + SendSupportMessage(user, body, subject); } @@ -439,15 +489,15 @@ private void SendMessage(MailMessage mailMessage, bool copySender = false) var senderCopy = new MailMessage( Config.GalleryOwner, mailMessage.ReplyToList.First()) - { - Subject = mailMessage.Subject + " [Sender Copy]", - Body = String.Format( + { + Subject = mailMessage.Subject + " [Sender Copy]", + Body = String.Format( CultureInfo.CurrentCulture, "You sent the following message via {0}: {1}{1}{2}", Config.GalleryOwner.DisplayName, Environment.NewLine, mailMessage.Body), - }; + }; senderCopy.ReplyToList.Add(mailMessage.ReplyToList.First()); MailSender.Send(senderCopy); } @@ -478,8 +528,8 @@ [change your email notification settings]({5}). body = String.Format( CultureInfo.CurrentCulture, body, - Config.GalleryOwner.DisplayName, - package.PackageRegistration.Id, + Config.GalleryOwner.DisplayName, + package.PackageRegistration.Id, package.Version, packageUrl, packageSupportUrl, @@ -491,7 +541,7 @@ [change your email notification settings]({5}). { mailMessage.Subject = subject; mailMessage.Body = body; - mailMessage.From = Config.GalleryOwner; + mailMessage.From = Config.GalleryNoReplyAddress; AddOwnersSubscribedToPackagePushedNotification(package.PackageRegistration, mailMessage); diff --git a/src/NuGetGallery/Services/PackageDeleteService.cs b/src/NuGetGallery/Services/PackageDeleteService.cs index fb0042ca91..fa7ec2dfdf 100644 --- a/src/NuGetGallery/Services/PackageDeleteService.cs +++ b/src/NuGetGallery/Services/PackageDeleteService.cs @@ -3,11 +3,8 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Data; using System.Data.Entity; using System.Data.SqlClient; -using System.Globalization; using System.Linq; using System.Threading.Tasks; using NuGet.Versioning; @@ -37,7 +34,7 @@ DELETE pr FROM PackageRegistrations AS pr private readonly IPackageService _packageService; private readonly IIndexingService _indexingService; private readonly IPackageFileService _packageFileService; - private readonly AuditingService _auditingService; + private readonly IAuditingService _auditingService; public PackageDeleteService( IEntityRepository packageRepository, @@ -46,7 +43,7 @@ public PackageDeleteService( IPackageService packageService, IIndexingService indexingService, IPackageFileService packageFileService, - AuditingService auditingService) + IAuditingService auditingService) { _packageRepository = packageRepository; _packageDeletesRepository = packageDeletesRepository; @@ -90,7 +87,7 @@ public async Task SoftDeletePackagesAsync(IEnumerable packages, User de package.Deleted = true; packageDelete.Packages.Add(package); - await _auditingService.SaveAuditRecord(CreateAuditRecord(package, package.PackageRegistration, PackageAuditAction.SoftDeleted, reason)); + await _auditingService.SaveAuditRecordAsync(CreateAuditRecord(package, package.PackageRegistration, AuditedPackageAction.SoftDelete, reason)); } _packageDeletesRepository.InsertOnCommit(packageDelete); @@ -138,7 +135,7 @@ await ExecuteSqlCommandAsync(_entitiesContext.GetDatabase(), "DELETE pf FROM PackageFrameworks pf JOIN Packages p ON p.[Key] = pf.Package_Key WHERE p.[Key] = @key", new SqlParameter("@key", package.Key)); - await _auditingService.SaveAuditRecord(CreateAuditRecord(package, package.PackageRegistration, PackageAuditAction.Deleted, reason)); + await _auditingService.SaveAuditRecordAsync(CreateAuditRecord(package, package.PackageRegistration, AuditedPackageAction.Delete, reason)); package.PackageRegistration.Packages.Remove(package); _packageRepository.DeleteOnCommit(package); @@ -221,40 +218,9 @@ private void UpdateSearchIndex() _indexingService.UpdateIndex(true); } - protected virtual PackageAuditRecord CreateAuditRecord(Package package, PackageRegistration packageRegistration, PackageAuditAction action, string reason) + protected virtual PackageAuditRecord CreateAuditRecord(Package package, PackageRegistration packageRegistration, AuditedPackageAction action, string reason) { - return new PackageAuditRecord(package, ConvertToDataTable(package), ConvertToDataTable(packageRegistration), action, reason); - } - - public static DataTable ConvertToDataTable(T instance) - { - PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T)); - DataTable table = new DataTable() { Locale = CultureInfo.CurrentCulture }; - - List values = new List(); - for (int i = 0; i < properties.Count; i++) - { - var propertyDescriptor = properties[i]; - var propertyType = Nullable.GetUnderlyingType(propertyDescriptor.PropertyType) ?? propertyDescriptor.PropertyType; - if (!IsComplexType(propertyType)) - { - table.Columns.Add(propertyDescriptor.Name, propertyType); - values.Add(propertyDescriptor.GetValue(instance) ?? DBNull.Value); - } - } - - table.Rows.Add(values.ToArray()); - - return table; - } - - public static bool IsComplexType(Type type) - { - if (type.IsSubclassOf(typeof (ValueType)) || type == typeof (string)) - { - return false; - } - return true; + return new PackageAuditRecord(package, action, reason); } } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/PackageFileService.cs b/src/NuGetGallery/Services/PackageFileService.cs index 5db7008bb3..afaabb3141 100644 --- a/src/NuGetGallery/Services/PackageFileService.cs +++ b/src/NuGetGallery/Services/PackageFileService.cs @@ -56,7 +56,7 @@ public Task SavePackageFileAsync(Package package, Stream packageFile) } var fileName = BuildFileName(package); - return _fileStorageService.SaveFileAsync(Constants.PackagesFolderName, fileName, packageFile); + return _fileStorageService.SaveFileAsync(Constants.PackagesFolderName, fileName, packageFile, overwrite: false); } public Task StorePackageFileInBackupLocationAsync(Package package, Stream packageFile) diff --git a/src/NuGetGallery/Services/PackageNamingConflictValidator.cs b/src/NuGetGallery/Services/PackageNamingConflictValidator.cs index b8fda93b99..8d6e0c85de 100644 --- a/src/NuGetGallery/Services/PackageNamingConflictValidator.cs +++ b/src/NuGetGallery/Services/PackageNamingConflictValidator.cs @@ -6,7 +6,7 @@ namespace NuGetGallery { - public class PackageNamingConflictValidator + public class PackageNamingConflictValidator : IPackageNamingConflictValidator { private readonly IEntityRepository _packageRegistrationRepository; @@ -35,7 +35,7 @@ public bool TitleConflictsWithExistingRegistrationId(string registrationId, stri var packageRegistration = _packageRegistrationRepository.GetAll() .SingleOrDefault(pr => pr.Id.ToLower() == cleanedTitle); - if (packageRegistration != null + if (packageRegistration != null && !String.Equals(packageRegistration.Id, registrationId, StringComparison.OrdinalIgnoreCase)) { return true; @@ -56,7 +56,7 @@ public bool IdConflictsWithExistingPackageTitle(string registrationId) registrationId = registrationId.ToLowerInvariant(); return _packageRepository.GetAll() - .Any(p => p.Title.ToLower() == registrationId); + .Any(p => !p.Deleted && p.Title.ToLower() == registrationId); } } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/PackageService.cs b/src/NuGetGallery/Services/PackageService.cs index 6109ee4e5e..0f88e21894 100644 --- a/src/NuGetGallery/Services/PackageService.cs +++ b/src/NuGetGallery/Services/PackageService.cs @@ -9,6 +9,7 @@ using NuGet.Frameworks; using NuGet.Packaging; using NuGet.Versioning; +using NuGetGallery.Auditing; using NuGetGallery.Packaging; namespace NuGetGallery @@ -20,19 +21,52 @@ public class PackageService : IPackageService private readonly IEntityRepository _packageRegistrationRepository; private readonly IEntityRepository _packageRepository; private readonly IPackageNamingConflictValidator _packageNamingConflictValidator; + private readonly IAuditingService _auditingService; public PackageService( IEntityRepository packageRegistrationRepository, IEntityRepository packageRepository, IEntityRepository packageOwnerRequestRepository, IIndexingService indexingService, - IPackageNamingConflictValidator packageNamingConflictValidator) + IPackageNamingConflictValidator packageNamingConflictValidator, + IAuditingService auditingService) { + if (packageRegistrationRepository == null) + { + throw new ArgumentNullException(nameof(packageRegistrationRepository)); + } + + if (packageRepository == null) + { + throw new ArgumentNullException(nameof(packageRepository)); + } + + if (packageOwnerRequestRepository == null) + { + throw new ArgumentNullException(nameof(packageOwnerRequestRepository)); + } + + if (indexingService == null) + { + throw new ArgumentNullException(nameof(indexingService)); + } + + if (packageNamingConflictValidator == null) + { + throw new ArgumentNullException(nameof(packageNamingConflictValidator)); + } + + if (auditingService == null) + { + throw new ArgumentNullException(nameof(auditingService)); + } + _packageRegistrationRepository = packageRegistrationRepository; _packageRepository = packageRepository; _packageOwnerRequestRepository = packageOwnerRequestRepository; _indexingService = indexingService; _packageNamingConflictValidator = packageNamingConflictValidator; + _auditingService = auditingService; } public void EnsureValid(PackageArchiveReader packageArchiveReader) @@ -92,7 +126,7 @@ public virtual Package FindPackageByIdAndVersion(string id, string version, bool throw new ArgumentNullException(nameof(id)); } - // Optimization: Everytime we look at a package we almost always want to see + // Optimization: Every time we look at a package we almost always want to see // all the other packages with the same ID via the PackageRegistration property. // This resulted in a gnarly query. // Instead, we can always query for all packages with the ID. @@ -187,6 +221,11 @@ public IEnumerable FindPackagesByOwner(User user, bool includeUnlisted) return mergedResults.Values; } + public IEnumerable FindPackageRegistrationsByOwner(User user) + { + return _packageRegistrationRepository.GetAll().Where(p => p.Owners.Any(o => o.Username == user.Username)); + } + public IEnumerable FindDependentPackages(Package package) { // Grab all candidates @@ -244,6 +283,9 @@ public async Task AddPackageOwnerAsync(PackageRegistration package, User user) _packageOwnerRequestRepository.DeleteOnCommit(request); await _packageOwnerRequestRepository.CommitChangesAsync(); } + + await _auditingService.SaveAuditRecordAsync( + new PackageRegistrationAuditRecord(package, AuditedPackageRegistrationAction.AddOwner, user.Username)); } public async Task RemovePackageOwnerAsync(PackageRegistration package, User user) @@ -263,6 +305,9 @@ public async Task RemovePackageOwnerAsync(PackageRegistration package, User user package.Owners.Remove(user); await _packageRepository.CommitChangesAsync(); + + await _auditingService.SaveAuditRecordAsync( + new PackageRegistrationAuditRecord(package, AuditedPackageRegistrationAction.RemoveOwner, user.Username)); } public async Task MarkPackageListedAsync(Package package, bool commitChanges = true) @@ -288,9 +333,12 @@ public async Task MarkPackageListedAsync(Package package, bool commitChanges = t package.Listed = true; package.LastUpdated = DateTime.UtcNow; + // NOTE: LastEdited will be overwritten by a trigger defined in the migration named "AddTriggerForPackagesLastEdited". package.LastEdited = DateTime.UtcNow; await UpdateIsLatestAsync(package.PackageRegistration, false); + + await _auditingService.SaveAuditRecordAsync(new PackageAuditRecord(package, AuditedPackageAction.List)); if (commitChanges) { @@ -311,6 +359,7 @@ public async Task MarkPackageUnlistedAsync(Package package, bool commitChanges = package.Listed = false; package.LastUpdated = DateTime.UtcNow; + // NOTE: LastEdited will be overwritten by a trigger defined in the migration named "AddTriggerForPackagesLastEdited". package.LastEdited = DateTime.UtcNow; if (package.IsLatest || package.IsLatestStable) @@ -318,6 +367,8 @@ public async Task MarkPackageUnlistedAsync(Package package, bool commitChanges = await UpdateIsLatestAsync(package.PackageRegistration, false); } + await _auditingService.SaveAuditRecordAsync(new PackageAuditRecord(package, AuditedPackageAction.Unlist)); + if (commitChanges) { await _packageRepository.CommitChangesAsync(); @@ -417,60 +468,86 @@ private Package CreatePackageFromNuGetPackage(PackageRegistration packageRegistr "A package with identifier '{0}' and version '{1}' already exists.", packageRegistration.Id, package.Version); } - package = new Package - { - // Version must always be the exact string from the nuspec, which ToString will return to us. - // However, we do also store a normalized copy for looking up later. - Version = packageMetadata.Version.ToString(), - NormalizedVersion = packageMetadata.Version.ToNormalizedString(), - - Description = packageMetadata.Description, - ReleaseNotes = packageMetadata.ReleaseNotes, - HashAlgorithm = packageStreamMetadata.HashAlgorithm, - Hash = packageStreamMetadata.Hash, - PackageFileSize = packageStreamMetadata.Size, - Language = packageMetadata.Language, - Copyright = packageMetadata.Copyright, - FlattenedAuthors = packageMetadata.Authors.Flatten(), - IsPrerelease = packageMetadata.Version.IsPrerelease, - Listed = true, - PackageRegistration = packageRegistration, - RequiresLicenseAcceptance = packageMetadata.RequireLicenseAcceptance, - Summary = packageMetadata.Summary, - Tags = PackageHelper.ParseTags(packageMetadata.Tags), - Title = packageMetadata.Title, - User = user, - }; + package = new Package(); + package.PackageRegistration = packageRegistration; + + package = EnrichPackageFromNuGetPackage(package, nugetPackage, packageMetadata, packageStreamMetadata, user); + + return package; + } + + public virtual Package EnrichPackageFromNuGetPackage( + Package package, + PackageArchiveReader packageArchive, + PackageMetadata packageMetadata, + PackageStreamMetadata packageStreamMetadata, + User user) + { + // Version must always be the exact string from the nuspec, which ToString will return to us. + // However, we do also store a normalized copy for looking up later. + package.Version = packageMetadata.Version.ToString(); + package.NormalizedVersion = packageMetadata.Version.ToNormalizedString(); + + package.Description = packageMetadata.Description; + package.ReleaseNotes = packageMetadata.ReleaseNotes; + package.HashAlgorithm = packageStreamMetadata.HashAlgorithm; + package.Hash = packageStreamMetadata.Hash; + package.PackageFileSize = packageStreamMetadata.Size; + package.Language = packageMetadata.Language; + package.Copyright = packageMetadata.Copyright; + package.FlattenedAuthors = packageMetadata.Authors.Flatten(); + package.IsPrerelease = packageMetadata.Version.IsPrerelease; + package.Listed = true; + package.RequiresLicenseAcceptance = packageMetadata.RequireLicenseAcceptance; + package.Summary = packageMetadata.Summary; + package.Tags = PackageHelper.ParseTags(packageMetadata.Tags); + package.Title = packageMetadata.Title; + package.User = user; package.IconUrl = packageMetadata.IconUrl.ToEncodedUrlStringOrNull(); package.LicenseUrl = packageMetadata.LicenseUrl.ToEncodedUrlStringOrNull(); package.ProjectUrl = packageMetadata.ProjectUrl.ToEncodedUrlStringOrNull(); package.MinClientVersion = packageMetadata.MinClientVersion.ToStringOrNull(); -#pragma warning disable 618 // TODO: remove Package.Authors completely once prodution services definitely no longer need it +#pragma warning disable 618 // TODO: remove Package.Authors completely once production services definitely no longer need it foreach (var author in packageMetadata.Authors) { package.Authors.Add(new PackageAuthor { Name = author }); } #pragma warning restore 618 - var supportedFrameworks = GetSupportedFrameworks(nugetPackage).Select(fn => fn.ToShortNameOrNull()).ToArray(); - if (!supportedFrameworks.AnySafe(sf => sf == null)) + var supportedFrameworks = GetSupportedFrameworks(packageArchive) + .ToArray(); + + if (!supportedFrameworks.Any(fx => fx != null && fx.IsAny)) { - ValidateSupportedFrameworks(supportedFrameworks); + var supportedFrameworkNames = supportedFrameworks + .Select(fn => fn.ToShortNameOrNull()) + .Where(fn => fn != null) + .ToArray(); + + ValidateSupportedFrameworks(supportedFrameworkNames); - foreach (var supportedFramework in supportedFrameworks) + foreach (var supportedFramework in supportedFrameworkNames) { package.SupportedFrameworks.Add(new PackageFramework { TargetFramework = supportedFramework }); } } - + package.Dependencies = packageMetadata .GetDependencyGroups() .AsPackageDependencyEnumerable() .ToList(); + + package.PackageTypes = packageMetadata + .GetPackageTypes() + .AsPackageTypeEnumerable() + .ToList(); + package.FlattenedDependencies = package.Dependencies.Flatten(); + package.FlattenedPackageTypes = package.PackageTypes.Flatten(); + return package; } @@ -676,7 +753,6 @@ private void NotifyIndexingService() _indexingService.UpdateIndex(); } - public async Task SetLicenseReportVisibilityAsync(Package package, bool visible, bool commitChanges = true) { if (package == null) @@ -688,7 +764,20 @@ public async Task SetLicenseReportVisibilityAsync(Package package, bool visible, { await _packageRepository.CommitChangesAsync(); } - await _packageRepository.CommitChangesAsync(); + } + + public async Task IncrementDownloadCountAsync(string id, string version, bool commitChanges = true) + { + var package = FindPackageByIdAndVersion(id, version); + if (package != null) + { + package.DownloadCount++; + package.PackageRegistration.DownloadCount++; + if (commitChanges) + { + await _packageRepository.CommitChangesAsync(); + } + } } } } diff --git a/src/NuGetGallery/Services/ReflowPackageService.cs b/src/NuGetGallery/Services/ReflowPackageService.cs new file mode 100644 index 0000000000..6787353359 --- /dev/null +++ b/src/NuGetGallery/Services/ReflowPackageService.cs @@ -0,0 +1,115 @@ +// 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.Linq; +using System.Threading.Tasks; +using NuGet.Packaging; +using NuGetGallery.Packaging; + +namespace NuGetGallery +{ + public class ReflowPackageService + { + private readonly IEntitiesContext _entitiesContext; + private readonly IPackageService _packageService; + private readonly IPackageFileService _packageFileService; + + public ReflowPackageService( + IEntitiesContext entitiesContext, + IPackageService packageService, + IPackageFileService packageFileService) + { + _entitiesContext = entitiesContext; + _packageService = packageService; + _packageFileService = packageFileService; + } + + public async Task ReflowAsync(string id, string version) + { + var package = _packageService.FindPackageByIdAndVersion(id, version); + + if (package == null) + { + return null; + } + + EntitiesConfiguration.SuspendExecutionStrategy = true; + using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) + { + // 1) Download package binary to memory + using (var packageStream = await _packageFileService.DownloadPackageFileAsync(package)) + { + using (var packageArchive = new PackageArchiveReader(packageStream, leaveStreamOpen: false)) + { + // 2) Determine package metadata from binary + var packageStreamMetadata = new PackageStreamMetadata + { + HashAlgorithm = Constants.Sha512HashAlgorithmId, + Hash = CryptographyService.GenerateHash(packageStream.AsSeekableStream()), + Size = packageStream.Length, + }; + + var packageMetadata = PackageMetadata.FromNuspecReader(packageArchive.GetNuspecReader()); + + // 3) Clear referenced objects that will be reflowed + ClearSupportedFrameworks(package); + ClearAuthors(package); + ClearDependencies(package); + + // 4) Reflow the package + var listed = package.Listed; + + package = _packageService.EnrichPackageFromNuGetPackage( + package, + packageArchive, + packageMetadata, + packageStreamMetadata, + package.User); + + package.LastEdited = DateTime.UtcNow; + package.Listed = listed; + + // 5) Save and profit + await _entitiesContext.SaveChangesAsync(); + } + } + + // Commit transaction + transaction.Commit(); + } + EntitiesConfiguration.SuspendExecutionStrategy = false; + + return package; + } + + private void ClearSupportedFrameworks(Package package) + { + foreach (var supportedFramework in package.SupportedFrameworks.ToList()) + { + _entitiesContext.Set().Remove(supportedFramework); + } + package.SupportedFrameworks.Clear(); + } + + private void ClearAuthors(Package package) + { +#pragma warning disable 618 + foreach (var packageAuthor in package.Authors.ToList()) + { + _entitiesContext.Set().Remove(packageAuthor); + } + package.Authors.Clear(); +#pragma warning restore 618 + } + + private void ClearDependencies(Package package) + { + foreach (var packageDependency in package.Dependencies.ToList()) + { + _entitiesContext.Set().Remove(packageDependency); + } + package.Dependencies.Clear(); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/TelemetryService.cs b/src/NuGetGallery/Services/TelemetryService.cs new file mode 100644 index 0000000000..15f7841583 --- /dev/null +++ b/src/NuGetGallery/Services/TelemetryService.cs @@ -0,0 +1,137 @@ +// 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.Security.Principal; +using System.Web; + +namespace NuGetGallery +{ + public class TelemetryService : ITelemetryService + { + // Event types + public const string ODataQueryFilterEvent = "ODataQueryFilter"; + public const string PackagePushEvent = "PackagePush"; + public const string CreatePackageVerificationKeyEvent = "CreatePackageVerificationKeyEvent"; + public const string VerifyPackageKeyEvent = "VerifyPackageKeyEvent"; + + // ODataQueryFilter properties + public const string CallContext = "CallContext"; + public const string IsEnabled = "IsEnabled"; + public const string IsAllowed = "IsAllowed"; + public const string QueryPattern = "QueryPattern"; + + // Package push properties + public const string AuthenticationMethod = "AuthenticationMethod"; + public const string AccountCreationDate = "AccountCreationDate"; + public const string ClientVersion = "ClientVersion"; + public const string IsScoped = "IsScoped"; + public const string PackageId = "PackageId"; + public const string PackageVersion = "PackageVersion"; + + // Verify package properties + public const string IsVerificationKeyUsed = "IsVerificationKeyUsed"; + public const string VerifyPackageKeyStatusCode = "VerifyPackageKeyStatusCode"; + + public void TrackODataQueryFilterEvent(string callContext, bool isEnabled, bool isAllowed, string queryPattern) + { + TrackEvent(ODataQueryFilterEvent, properties => + { + properties.Add(CallContext, callContext); + properties.Add(IsEnabled, $"{isEnabled}"); + + properties.Add(IsAllowed, $"{isAllowed}"); + properties.Add(QueryPattern, queryPattern); + }); + } + + public void TrackPackagePushEvent(Package package, User user, IIdentity identity) + { + if (package == null) + { + throw new ArgumentNullException(nameof(package)); + } + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + TrackEvent(PackagePushEvent, properties => { + properties.Add(ClientVersion, GetClientVersion()); + properties.Add(PackageId, package.PackageRegistration.Id); + properties.Add(PackageVersion, package.Version); + properties.Add(AuthenticationMethod, identity.GetAuthenticationType()); + properties.Add(AccountCreationDate, GetAccountCreationDate(user)); + properties.Add(IsScoped, identity.IsScopedAuthentication().ToString()); + }); + } + + public void TrackCreatePackageVerificationKeyEvent(string packageId, string packageVersion, User user, IIdentity identity) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + TrackEvent(CreatePackageVerificationKeyEvent, properties => { + properties.Add(ClientVersion, GetClientVersion()); + properties.Add(PackageId, packageId); + properties.Add(PackageVersion, packageVersion); + properties.Add(AccountCreationDate, GetAccountCreationDate(user)); + properties.Add(IsScoped, identity.IsScopedAuthentication().ToString()); + }); + } + + public void TrackVerifyPackageKeyEvent(string packageId, string packageVersion, User user, IIdentity identity, int statusCode) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + TrackEvent(VerifyPackageKeyEvent, properties => + { + properties.Add(PackageId, packageId); + properties.Add(PackageVersion, packageVersion); + properties.Add(IsVerificationKeyUsed, identity.HasVerifyScope().ToString()); + properties.Add(VerifyPackageKeyStatusCode, statusCode.ToString()); + }); + } + + private static string GetClientVersion() + { + return HttpContext.Current?.Request?.Headers[Constants.ClientVersionHeaderName]; + } + + private static string GetAccountCreationDate(User user) + { + return user.CreatedUtc != null ? user.CreatedUtc.Value.ToString("o") : "N/A"; + } + + private static void TrackEvent(string eventName, Action> addProperties) + { + var telemetryProperties = new Dictionary(); + + addProperties(telemetryProperties); + + Telemetry.TrackEvent(eventName, telemetryProperties, metrics: null); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index b8d2f4b22f..529ccb377a 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -18,7 +18,7 @@ public class UserService : IUserService public IAppConfiguration Config { get; protected set; } public IEntityRepository UserRepository { get; protected set; } public IEntityRepository CredentialRepository { get; protected set; } - public AuditingService Auditing { get; protected set; } + public IAuditingService Auditing { get; protected set; } protected UserService() { } @@ -26,7 +26,7 @@ public UserService( IAppConfiguration config, IEntityRepository userRepository, IEntityRepository credentialRepository, - AuditingService auditing) + IAuditingService auditing) : this() { Config = config; @@ -97,7 +97,7 @@ public async Task ChangeEmailAddress(User user, string newEmailAddress) throw new EntityException(Strings.EmailAddressBeingUsed, newEmailAddress); } - await Auditing.SaveAuditRecord(new UserAuditRecord(user, UserAuditAction.ChangeEmail, newEmailAddress)); + await Auditing.SaveAuditRecordAsync(new UserAuditRecord(user, AuditedUserAction.ChangeEmail, newEmailAddress)); user.UpdateEmailAddress(newEmailAddress, Crypto.GenerateToken); await UserRepository.CommitChangesAsync(); @@ -105,7 +105,7 @@ public async Task ChangeEmailAddress(User user, string newEmailAddress) public async Task CancelChangeEmailAddress(User user) { - await Auditing.SaveAuditRecord(new UserAuditRecord(user, UserAuditAction.CancelChangeEmail, user.UnconfirmedEmailAddress)); + await Auditing.SaveAuditRecordAsync(new UserAuditRecord(user, AuditedUserAction.CancelChangeEmail, user.UnconfirmedEmailAddress)); user.CancelChangeEmailAddress(); await UserRepository.CommitChangesAsync(); @@ -144,7 +144,7 @@ public async Task ConfirmEmailAddress(User user, string token) throw new EntityException(Strings.EmailAddressBeingUsed, user.UnconfirmedEmailAddress); } - await Auditing.SaveAuditRecord(new UserAuditRecord(user, UserAuditAction.ConfirmEmail, user.UnconfirmedEmailAddress)); + await Auditing.SaveAuditRecordAsync(new UserAuditRecord(user, AuditedUserAction.ConfirmEmail, user.UnconfirmedEmailAddress)); user.ConfirmEmailAddress(); diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index 45a6014a5c..cbf1289995 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -70,29 +70,56 @@ public static string AlreadyLoggedIn { } /// - /// Looks up a localized string similar to The specified API key is invalid or does not have permission to access the specified package.. + /// Looks up a localized string similar to a minute. /// - public static string ApiKeyNotAuthorized { + public static string AMinute { get { - return ResourceManager.GetString("ApiKeyNotAuthorized", resourceCulture); + return ResourceManager.GetString("AMinute", resourceCulture); } } /// - /// Looks up a localized string similar to An API key must be provided in the 'X-NuGet-ApiKey' header to use this service. + /// Looks up a localized string similar to API key can not be the default Guid.. /// - public static string ApiKeyRequired { + public static string ApiKeyCanNotBeDefaultGuid { get { - return ResourceManager.GetString("ApiKeyRequired", resourceCulture); + return ResourceManager.GetString("ApiKeyCanNotBeDefaultGuid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't generate an API key without a description.. + /// + public static string ApiKeyDescriptionRequired { + get { + return ResourceManager.GetString("ApiKeyDescriptionRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A new API key has been generated. Check below and make sure to copy the value, as now is the only time it will be visible.. + /// + public static string ApiKeyGenerated { + get { + return ResourceManager.GetString("ApiKeyGenerated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified API key is invalid, has expired, or does not have permission to access the specified package.. + /// + public static string ApiKeyNotAuthorized { + get { + return ResourceManager.GetString("ApiKeyNotAuthorized", resourceCulture); } } /// - /// Looks up a localized string similar to Your API Key has been reset, check the new value below.. + /// Looks up a localized string similar to An API key must be provided in the 'X-NuGet-ApiKey' header to use this service. /// - public static string ApiKeyReset { + public static string ApiKeyRequired { get { - return ResourceManager.GetString("ApiKeyReset", resourceCulture); + return ResourceManager.GetString("ApiKeyRequired", resourceCulture); } } @@ -188,7 +215,25 @@ public static string CouldNotFindAnyoneWithThatEmail { } /// - /// Looks up a localized string similar to The external account has been removed. + /// Looks up a localized string similar to The credential has been expired.. + /// + public static string CredentialExpired { + get { + return ResourceManager.GetString("CredentialExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Credential not found.. + /// + public static string CredentialNotFound { + get { + return ResourceManager.GetString("CredentialNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The credential has been removed.. /// public static string CredentialRemoved { get { @@ -197,7 +242,7 @@ public static string CredentialRemoved { } /// - /// Looks up a localized string similar to API Key. + /// Looks up a localized string similar to API key. /// public static string CredentialType_ApiKey { get { @@ -259,6 +304,24 @@ public static string EmailPreferencesUpdated { } } + /// + /// Looks up a localized string similar to API key '{0}' was added to your account and can now be used. If you did not request this change, please reply to this email to contact support.. + /// + public static string Emails_ApiKeyAdded_Body { + get { + return ResourceManager.GetString("Emails_ApiKeyAdded_Body", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to API key '{0}' was removed from your account and can no longer be used. If you did not request this change, please reply to this email to contact support.. + /// + public static string Emails_ApiKeyRemoved_Body { + get { + return ResourceManager.GetString("Emails_ApiKeyRemoved_Body", resourceCulture); + } + } + /// /// Looks up a localized string similar to A {0} was added to your account and can now be used to log in. If you did not request this change, please reply to this email to contact support.. /// @@ -299,12 +362,12 @@ public static string Emails_CredentialRemoved_Subject { /// Looks up a localized string similar to The word on the street is you lost your password. Sorry to hear it! ///If you haven't forgotten your password you can safely ignore this email. Your password has not been changed. /// - ///Click the following link within the next {0} hours to reset your password: + ///Click the following link within the next hour to reset your password: /// - ///[{1}]({1}) + ///[{0}]({0}) /// ///Thanks, - ///The {2} Team. + ///The {1} Team. /// public static string Emails_ForgotPassword_Body { get { @@ -325,12 +388,12 @@ public static string Emails_ForgotPassword_Subject { /// Looks up a localized string similar to The word on the street is you want to set a password for your account. ///If you didn't request a password, you can safely ignore this message. A password has not yet been set. /// - ///Click the following link within the next {0} hours to set your password: + ///Click the following link within the next hour to set your password: /// - ///[{1}]({1}) + ///[{0}]({0}) /// ///Thanks, - ///The {2} Team. + ///The {1} Team. /// public static string Emails_SetPassword_Body { get { @@ -392,24 +455,6 @@ public static string FailedToReadUploadFile { } } - /// - /// Looks up a localized string similar to (404) Error - Not Found. - /// - public static string Http404NotFound { - get { - return ResourceManager.GetString("Http404NotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Index does not exist. - /// - public static string IndexDoesNotExist { - get { - return ResourceManager.GetString("IndexDoesNotExist", resourceCulture); - } - } - /// /// Looks up a localized string similar to The API key '{0}' is invalid.. /// @@ -528,29 +573,29 @@ public static string MicrosoftAccount_SignInMessage { } /// - /// Looks up a localized string similar to Missing required configuration value: '{0}'. + /// Looks up a localized string similar to {0} minutes. /// - public static string MissingRequiredConfigurationValue { + public static string Minutes { get { - return ResourceManager.GetString("MissingRequiredConfigurationValue", resourceCulture); + return ResourceManager.GetString("Minutes", resourceCulture); } } /// - /// Looks up a localized string similar to Multiple Credentials match '{0}' credential with Key {1}. + /// Looks up a localized string similar to Missing required configuration value: '{0}'. /// - public static string MultipleMatchingCredentials { + public static string MissingRequiredConfigurationValue { get { - return ResourceManager.GetString("MultipleMatchingCredentials", resourceCulture); + return ResourceManager.GetString("MissingRequiredConfigurationValue", resourceCulture); } } /// - /// Looks up a localized string similar to Negative indexes are invalid.. + /// Looks up a localized string similar to Multiple Credentials match '{0}' credential with Key {1}. /// - public static string NegativeIndexesAreInvalid { + public static string MultipleMatchingCredentials { get { - return ResourceManager.GetString("NegativeIndexesAreInvalid", resourceCulture); + return ResourceManager.GetString("MultipleMatchingCredentials", resourceCulture); } } @@ -572,6 +617,15 @@ public static string No { } } + /// + /// Looks up a localized string similar to Full access API key. + /// + public static string NonScopedApiKeyDescription { + get { + return ResourceManager.GetString("NonScopedApiKeyDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to A nuget package's {0} property may not be more than {1} characters long.. /// @@ -599,6 +653,15 @@ public static string NuGetPackageReleaseVersionWithDot { } } + /// + /// Looks up a localized string similar to Package created from API.. + /// + public static string PackageCreatedFromApi { + get { + return ResourceManager.GetString("PackageCreatedFromApi", resourceCulture); + } + } + /// /// Looks up a localized string similar to The package is invalid and cannot be uploaded. One or more files, such as '{0}' have a date in the future.. /// @@ -609,7 +672,7 @@ public static string PackageEntryFromTheFuture { } /// - /// Looks up a localized string similar to A package with id '{0}' and version '{1}' already exists and cannot be modified.. + /// Looks up a localized string similar to A package with ID '{0}' and version '{1}' already exists and cannot be modified.. /// public static string PackageExistsAndCannotBeModified { get { @@ -645,7 +708,7 @@ public static string PackageIsMissingRequiredData { } /// - /// Looks up a localized string similar to A package with id '{0}' and version '{1}' does not exist.. + /// Looks up a localized string similar to A package with ID '{0}' and version '{1}' does not exist.. /// public static string PackageWithIdAndVersionNotFound { get { @@ -672,7 +735,7 @@ public static string ParameterCannotBeNullOrEmpty { } /// - /// Looks up a localized string similar to Your password has been changed. + /// Looks up a localized string similar to Your password has been changed.. /// public static string PasswordChanged { get { @@ -690,7 +753,7 @@ public static string PasswordCredentialsCannotBeUsedHere { } /// - /// Looks up a localized string similar to Your password has been removed. + /// Looks up a localized string similar to Your password has been removed.. /// public static string PasswordRemoved { get { @@ -699,7 +762,7 @@ public static string PasswordRemoved { } /// - /// Looks up a localized string similar to Your password has been set. + /// Looks up a localized string similar to Your password has been set.. /// public static string PasswordSet { get { @@ -707,6 +770,60 @@ public static string PasswordSet { } } + /// + /// Looks up a localized string similar to All. + /// + public static string ScopeDescription_All { + get { + return ResourceManager.GetString("ScopeDescription_All", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Push new packages and package versions. + /// + public static string ScopeDescription_PushPackage { + get { + return ResourceManager.GetString("ScopeDescription_PushPackage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Push only new package versions. + /// + public static string ScopeDescription_PushPackageVersion { + get { + return ResourceManager.GetString("ScopeDescription_PushPackageVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown. + /// + public static string ScopeDescription_Unknown { + get { + return ResourceManager.GetString("ScopeDescription_Unknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unlist package. + /// + public static string ScopeDescription_UnlistPackage { + get { + return ResourceManager.GetString("ScopeDescription_UnlistPackage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Verify package ownership. + /// + public static string ScopeDescription_VerifyPackage { + get { + return ResourceManager.GetString("ScopeDescription_VerifyPackage", resourceCulture); + } + } + /// /// Looks up a localized string similar to The requested resource can only be accessed via SSL.. /// @@ -761,6 +878,15 @@ public static string UnknownAuthenticationProvider { } } + /// + /// Looks up a localized string similar to Unsupported. + /// + public static string Unsupported { + get { + return ResourceManager.GetString("Unsupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to A package file is required.. /// @@ -780,7 +906,16 @@ public static string UploadFileMustBeNuGetPackage { } /// - /// Looks up a localized string similar to The NuGet package contains an invalid .nuspec file. The error encountered was:'{0}'. Correct the error and try again.. + /// Looks up a localized string similar to There is a conflict with the ID and version of your package and another package. Please change your package's ID or version and try again.. + /// + public static string UploadPackage_IdVersionConflict { + get { + return ResourceManager.GetString("UploadPackage_IdVersionConflict", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The NuGet package contains an invalid .nuspec file. The error encountered was: '{0}'. Correct the error and try again.. /// public static string UploadPackage_InvalidNuspec { get { @@ -788,6 +923,15 @@ public static string UploadPackage_InvalidNuspec { } } + /// + /// Looks up a localized string similar to The NuGet package contains an invalid .nuspec file. The errors encountered were: '{0}'. Correct the errors and try again.. + /// + public static string UploadPackage_InvalidNuspecMultiple { + get { + return ResourceManager.GetString("UploadPackage_InvalidNuspecMultiple", resourceCulture); + } + } + /// /// Looks up a localized string similar to The NuGet package is invalid. The error encountered was:'{0}'. Correct the error and try again.. /// @@ -806,6 +950,15 @@ public static string UploadPackage_MinClientVersionOutOfRange { } } + /// + /// Looks up a localized string similar to Your account was locked after too many unsuccessful sign-in attempts. Please try again in {0}.. + /// + public static string UserAccountLocked { + get { + return ResourceManager.GetString("UserAccountLocked", resourceCulture); + } + } + /// /// Looks up a localized string similar to You cannot reset your password until you confirm your account.. /// @@ -851,6 +1004,33 @@ public static string UserNotFound { } } + /// + /// Looks up a localized string similar to API '{0}' is deprecated and may be removed in a future version.. + /// + public static string WarningApiDeprecated { + get { + return ResourceManager.GetString("WarningApiDeprecated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your API key expires in {0} days. Visit {1} to regenerate your API key.. + /// + public static string WarningApiKeyAboutToExpire { + get { + return ResourceManager.GetString("WarningApiKeyAboutToExpire", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your API key has expired. Visit {0} to generate a new API key.. + /// + public static string WarningApiKeyExpired { + get { + return ResourceManager.GetString("WarningApiKeyExpired", resourceCulture); + } + } + /// /// Looks up a localized string similar to Yes. /// diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index 1327a81ba1..517e87bb98 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -130,16 +130,16 @@ The package ID '{0}' is not available. - A package with id '{0}' and version '{1}' does not exist. + A package with ID '{0}' and version '{1}' does not exist. A nuget package's {0} property may not be more than {1} characters long. - The specified API key is invalid or does not have permission to access the specified package. + The specified API key is invalid, has expired, or does not have permission to access the specified package. - A package with id '{0}' and version '{1}' already exists and cannot be modified. + A package with ID '{0}' and version '{1}' already exists and cannot be modified. The current password you provided is incorrect. @@ -211,7 +211,7 @@ Microsoft account - API Key + API key Password @@ -219,34 +219,37 @@ Microsoft account - - Your API Key has been reset, check the new value below. + + A new API key has been generated. Check below and make sure to copy the value, as now is the only time it will be visible. + + + The credential has been expired. Oops! You can't remove the only credential that can be used to log in to the website! - The external account has been removed + The credential has been removed. - Your password has been changed + Your password has been changed. - Your password has been removed + Your password has been removed. - Your password has been set + Your password has been set. The word on the street is you lost your password. Sorry to hear it! If you haven't forgotten your password you can safely ignore this email. Your password has not been changed. -Click the following link within the next {0} hours to reset your password: +Click the following link within the next hour to reset your password: -[{1}]({1}) +[{0}]({0}) Thanks, -The {2} Team +The {1} Team [{0}] Please reset your password. @@ -255,12 +258,12 @@ The {2} Team The word on the street is you want to set a password for your account. If you didn't request a password, you can safely ignore this message. A password has not yet been set. -Click the following link within the next {0} hours to set your password: +Click the following link within the next hour to set your password: -[{1}]({1}) +[{0}]({0}) Thanks, -The {2} Team +The {1} Team [{0}] Please set your password. @@ -309,14 +312,14 @@ The {2} Team The NuGet package is invalid. The error encountered was:'{0}'. Correct the error and try again. - The NuGet package contains an invalid .nuspec file. The error encountered was:'{0}'. Correct the error and try again. + The NuGet package contains an invalid .nuspec file. The error encountered was: '{0}'. Correct the error and try again. Package {0} invalid: no '.' allowed in the release label. Package {0} invalid: the release label can not only contain numerics. - + SKIPPED! Package file blob {0} already exists @@ -326,18 +329,9 @@ The {2} Team Job Log Blob name is invalid Bob! Expected [jobname].[yyyy-MM-dd].json or [jobname].json. Got: {0} - - Negative indexes are invalid. - - - Index does not exist - The package is missing required data. - - (404) Error - Not Found - Error in sending mail : {0} @@ -380,6 +374,72 @@ The {2} Team Azure Active Directory + + API key can not be the default Guid. + + + Your API key expires in {0} days. Visit {1} to regenerate your API key. + + + Your API key has expired. Visit {0} to generate a new API key. + + + Credential not found. + + + Package created from API. + + + a minute + + + {0} minutes + + + Your account was locked after too many unsuccessful sign-in attempts. Please try again in {0}. + + + All + + + Push new packages and package versions + + + Push only new package versions + + + Unlist package + + + Unknown + + + Can't generate an API key without a description. + + + Unsupported + + + The NuGet package contains an invalid .nuspec file. The errors encountered were: '{0}'. Correct the errors and try again. + + + There is a conflict with the ID and version of your package and another package. Please change your package's ID or version and try again. + + + API key '{0}' was added to your account and can now be used. If you did not request this change, please reply to this email to contact support. + + + API key '{0}' was removed from your account and can no longer be used. If you did not request this change, please reply to this email to contact support. + + + Full access API key + + + Verify package ownership + + + API '{0}' is deprecated and may be removed in a future version. + You can use this Microsoft account to sign in to NuGet.org diff --git a/src/NuGetGallery/T4MVC.tt.settings.t4 b/src/NuGetGallery/T4MVC.tt.settings.t4 index 393c544f7f..5e06ecfe91 100644 --- a/src/NuGetGallery/T4MVC.tt.settings.t4 +++ b/src/NuGetGallery/T4MVC.tt.settings.t4 @@ -34,7 +34,7 @@ const string ControllersFolder = "Controllers"; // The folder under the project that contains the views const string ViewsRootFolder = "Views"; -// Views in DisplayTemplates and EditorTemplates folders shouldn't be fully qualifed as it breaks +// Views in DisplayTemplates and EditorTemplates folders shouldn't be fully qualified as it breaks // the templated helper code readonly string[] NonQualifiedViewFolders = new string[] { "DisplayTemplates", diff --git a/src/NuGetGallery/UrlExtensions.cs b/src/NuGetGallery/UrlExtensions.cs index 590fc99636..9fa89746d2 100644 --- a/src/NuGetGallery/UrlExtensions.cs +++ b/src/NuGetGallery/UrlExtensions.cs @@ -244,6 +244,18 @@ public static string EditPackage(this UrlHelper url, string id, string version) return url.RouteUrl(RouteName.PackageVersionAction, new { action = "Edit", id, version }); } + public static string ReflowPackage(this UrlHelper url, IPackageVersionModel package) + { + return url.Action( + actionName: "Reflow", + controllerName: "Packages", + routeValues: new + { + id = package.Id, + version = package.Version + }); + } + public static string DeletePackage(this UrlHelper url, IPackageVersionModel package) { return url.Action( diff --git a/src/NuGetGallery/ViewModels/AccountViewModel.cs b/src/NuGetGallery/ViewModels/AccountViewModel.cs index 36b99c593a..107404aef1 100644 --- a/src/NuGetGallery/ViewModels/AccountViewModel.cs +++ b/src/NuGetGallery/ViewModels/AccountViewModel.cs @@ -1,18 +1,29 @@ // 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.ComponentModel.DataAnnotations; using System.Web.Mvc; using NuGetGallery.Authentication.Providers; +using NuGetGallery.Infrastructure; namespace NuGetGallery { public class AccountViewModel { + public AccountViewModel() + { + ChangePassword = new ChangePasswordViewModel(); + Packages = new List(); + } + public IEnumerable CuratedFeeds { get; set; } public IList Credentials { get; set; } + public List Packages { get; set; } public ChangePasswordViewModel ChangePassword { get; set; } public ChangeEmailViewModel ChangeEmail { get; set; } + public int ExpirationInDaysForApiKeyV1 { get; set; } } public class ChangeEmailViewModel @@ -41,18 +52,34 @@ public class ChangePasswordViewModel [Required] [Display(Name = "New Password")] + [PasswordValidation] [AllowHtml] public string NewPassword { get; set; } } public class CredentialViewModel { + public int Key { get; set; } public string Type { get; set; } public string TypeCaption { get; set; } public string Identity { get; set; } - public string Value { get; set; } + public DateTime Created { get; set; } + public DateTime? Expires { get; set; } public CredentialKind Kind { get; set; } public AuthenticatorUI AuthUI { get; set; } + public string Description { get; set; } + public List Scopes { get; set; } + public bool HasExpired { get; set; } + public string Value { get; set; } + public TimeSpan? ExpirationDuration { get; set; } + + public bool IsNonScopedV1ApiKey + { + get + { + return string.Equals(Type, CredentialTypes.ApiKey.V1, StringComparison.OrdinalIgnoreCase); + } + } } public enum CredentialKind @@ -61,4 +88,4 @@ public enum CredentialKind Token, External } -} \ No newline at end of file +} diff --git a/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs b/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs index 7749293d15..db2199b6f4 100644 --- a/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs +++ b/src/NuGetGallery/ViewModels/ListPackageItemViewModel.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 NuGetGallery.Helpers; using System.Collections.Generic; using System.Linq; using System.Security.Principal; @@ -8,6 +9,9 @@ namespace NuGetGallery { public class ListPackageItemViewModel : PackageViewModel { + private const int _descriptionLengthLimit = 300; + private const string _omissionString = "..."; + public ListPackageItemViewModel(Package package) : base(package) { @@ -15,13 +19,19 @@ public ListPackageItemViewModel(Package package) Authors = package.FlattenedAuthors; MinClientVersion = package.MinClientVersion; - Owners = package.PackageRegistration.Owners; + Owners = package.PackageRegistration?.Owners; + + bool wasTruncated; + ShortDescription = Description.TruncateAtWordBoundary(_descriptionLengthLimit, _omissionString, out wasTruncated); + IsDescriptionTruncated = wasTruncated; } public string Authors { get; set; } public ICollection Owners { get; set; } public IEnumerable Tags { get; set; } public string MinClientVersion { get; set; } + public string ShortDescription { get; set; } + public bool IsDescriptionTruncated { get; set; } public bool UseVersion { diff --git a/src/NuGetGallery/ViewModels/LogOnViewModel.cs b/src/NuGetGallery/ViewModels/LogOnViewModel.cs index f50e5107e9..130a0a4c1b 100644 --- a/src/NuGetGallery/ViewModels/LogOnViewModel.cs +++ b/src/NuGetGallery/ViewModels/LogOnViewModel.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Text.RegularExpressions; using System.Web.Mvc; using NuGetGallery.Authentication.Providers; +using NuGetGallery.Infrastructure; namespace NuGetGallery { @@ -62,7 +62,7 @@ public SignInViewModel(string userNameOrEmail, string password) public class RegisterViewModel { - // Note: regexes must be tested to work in javascript + // Note: regexes must be tested to work in JavaScript // We do NOT follow strictly the RFCs at this time, and we choose not to support many obscure email address variants. // Specifically the following are not supported by-design: // * Addresses containing () or [] @@ -74,40 +74,32 @@ public class RegisterViewModel internal const string EmailValidationRegex = "^" + FirstPart + "@" + SecondPart + "$"; internal const string EmailValidationErrorMessage = "This doesn't appear to be a valid email address."; + public const string EmailHint = "Your email will not be public unless you choose to disclose it. " + + "It is required to verify your registration and for password retrieval, important notifications, etc. "; internal const string UsernameValidationRegex = @"[A-Za-z0-9][A-Za-z0-9_\.-]+[A-Za-z0-9]"; - - /// - /// Regex that matches INVALID username characters, to make it easy to strip those characters out. - /// - internal static readonly Regex UsernameNormalizationRegex = - new Regex(@"[^A-Za-z0-9_\.-]"); - + internal const string UsernameValidationErrorMessage = "User names must start and end with a letter or number, and may only contain letters, numbers, underscores, periods, and hyphens in between."; + public const string UserNameHint = "Choose something unique so others will know which contributions are yours."; + [Required] [StringLength(255)] [Display(Name = "Email")] - //[DataType(DataType.EmailAddress)] - does not work with client side validation [RegularExpression(EmailValidationRegex, ErrorMessage = EmailValidationErrorMessage)] - [Hint( - "Your email will not be public unless you choose to disclose it. " + - "It is required to verify your registration and for password retrieval, important notifications, etc. ")] [Subtext("We use Gravatar to get your profile picture", AllowHtml = true)] public string EmailAddress { get; set; } [Required] [StringLength(64)] [RegularExpression(UsernameValidationRegex, ErrorMessage = UsernameValidationErrorMessage)] - [Hint("Choose something unique so others will know which contributions are yours.")] public string Username { get; set; } [Required] [DataType(DataType.Password)] - [StringLength(64, MinimumLength = 7)] - [Hint("Passwords must be at least 7 characters long.")] + [PasswordValidation] [AllowHtml] public string Password { get; set; } } diff --git a/src/NuGetGallery/ViewModels/PackageListSearchViewModel.cs b/src/NuGetGallery/ViewModels/PackageListSearchViewModel.cs new file mode 100644 index 0000000000..d7451ed6c4 --- /dev/null +++ b/src/NuGetGallery/ViewModels/PackageListSearchViewModel.cs @@ -0,0 +1,11 @@ +using System.Web.Mvc; + +namespace NuGetGallery +{ + public class PackageListSearchViewModel + { + [AllowHtml] + public string Q { get; set; } + public int Page { get; set; } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/ViewModels/PasswordResetViewModel.cs b/src/NuGetGallery/ViewModels/PasswordResetViewModel.cs index 13fcfca9e5..f2e442b103 100644 --- a/src/NuGetGallery/ViewModels/PasswordResetViewModel.cs +++ b/src/NuGetGallery/ViewModels/PasswordResetViewModel.cs @@ -1,6 +1,7 @@ // 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; +using NuGetGallery.Infrastructure; namespace NuGetGallery { @@ -9,8 +10,7 @@ public class PasswordResetViewModel [Required] [DataType(DataType.Password)] [Display(Name = "New password")] - [StringLength(64, MinimumLength = 7)] - [Hint("Passwords must be at least 7 characters long.")] + [PasswordValidation] public string NewPassword { get; set; } [Required] diff --git a/src/NuGetGallery/ViewModels/ReportPackageReason.cs b/src/NuGetGallery/ViewModels/ReportPackageReason.cs index bf91430c0c..3a7dab76f2 100644 --- a/src/NuGetGallery/ViewModels/ReportPackageReason.cs +++ b/src/NuGetGallery/ViewModels/ReportPackageReason.cs @@ -25,10 +25,7 @@ public enum ReportPackageReason [Description("The package contains private/confidential data")] ContainsPrivateAndConfidentialData, - [Description("The package was published as the wrong version")] - PublishedWithWrongVersion, - - [Description("The package was not intended to be published publically on nuget.org")] + [Description("The package was not intended to be published publicly on nuget.org")] ReleasedInPublicByAccident, } -} \ No newline at end of file +} diff --git a/src/NuGetGallery/ViewModels/ScopeViewModel.cs b/src/NuGetGallery/ViewModels/ScopeViewModel.cs new file mode 100644 index 0000000000..8f075a3cec --- /dev/null +++ b/src/NuGetGallery/ViewModels/ScopeViewModel.cs @@ -0,0 +1,17 @@ +// 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 class ScopeViewModel + { + public string Subject { get; set; } + public string AllowedAction { get; set; } + + public ScopeViewModel(string subject, string allowedAction) + { + Subject = subject; + AllowedAction = allowedAction; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/ViewModels/StatisticsFact.cs b/src/NuGetGallery/ViewModels/StatisticsFact.cs index cca91c9c9b..09e1f14bc1 100644 --- a/src/NuGetGallery/ViewModels/StatisticsFact.cs +++ b/src/NuGetGallery/ViewModels/StatisticsFact.cs @@ -1,6 +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; + using System.Collections.Generic; namespace NuGetGallery diff --git a/src/NuGetGallery/ViewModels/StatisticsPivot.cs b/src/NuGetGallery/ViewModels/StatisticsPivot.cs index 1f84944a19..ea4f47d1f3 100644 --- a/src/NuGetGallery/ViewModels/StatisticsPivot.cs +++ b/src/NuGetGallery/ViewModels/StatisticsPivot.cs @@ -136,7 +136,7 @@ private static void Assign(Level result, StatisticsFact fact, string[] dimension } } - // The count in the tree is the count of values. It is equivallent to the count of rows if we + // The count in the tree is the count of values. It is equivalent to the count of rows if we // were to represent this in a table. private static int Count(Level level) @@ -223,7 +223,7 @@ public Level(IDictionary next) public int Count { get; set; } - // Total is the sum Total of all the Amounts in all the decendents. (See Total function above.) + // Total is the sum Total of all the Amounts in all the descendants. (See Total function above.) public int Total { get; set; } diff --git a/src/NuGetGallery/Views/Authentication/_Register.cshtml b/src/NuGetGallery/Views/Authentication/_Register.cshtml index 8e1828485a..6f7cf21a3c 100644 --- a/src/NuGetGallery/Views/Authentication/_Register.cshtml +++ b/src/NuGetGallery/Views/Authentication/_Register.cshtml @@ -1,4 +1,5 @@ -@model LogOnViewModel +@using NuGetGallery.Configuration +@model LogOnViewModel
    @using (Html.BeginForm("Register", "Authentication")) @@ -19,7 +20,8 @@
    @Html.LabelFor(m => m.Register.Username) @Html.EditorFor(m => m.Register.Username) - @Html.ValidationMessageFor(m => m.Register.Username) + @Html.ValidationMessageFor(m => m.Register.Username) + @RegisterViewModel.UserNameHint
    @if (Model.External == null) @@ -28,6 +30,7 @@ @Html.LabelFor(m => m.Register.Password) @Html.EditorFor(m => m.Register.Password) @Html.ValidationMessageFor(m => m.Register.Password) + @PasswordHint()
    } @@ -35,6 +38,7 @@ @Html.EditorFor(m => m.Register.EmailAddress) @Html.ValidationMessageFor(m => m.Register.EmailAddress) + @RegisterViewModel.EmailHint Blue border on left means required. @@ -46,5 +50,12 @@ - } + } + + @helper PasswordHint() { + var config = DependencyResolver.Current.GetService(); + string hint = config.Current.UserPasswordHint; + @hint + } + \ No newline at end of file diff --git a/src/NuGetGallery/Views/NuGetViewBase.cs b/src/NuGetGallery/Views/NuGetViewBase.cs index bf212cc36a..4a1b5b8052 100644 --- a/src/NuGetGallery/Views/NuGetViewBase.cs +++ b/src/NuGetGallery/Views/NuGetViewBase.cs @@ -16,7 +16,7 @@ public NuGetContext NuGetContext get { return _nugetContext.Value; } } - public ConfigurationService Config + public IGalleryConfigurationService Config { get { return NuGetContext.Config; } } @@ -54,7 +54,7 @@ public NuGetContext NuGetContext get { return _nugetContext.Value; } } - public ConfigurationService Config + public IGalleryConfigurationService Config { get { return NuGetContext.Config; } } diff --git a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml index 5b300616e8..4ec9a1722c 100644 --- a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml @@ -1,4 +1,5 @@ @model DisplayPackageViewModel + @{ ViewBag.Title = Model.Title + " " + Model.Version; ViewBag.Tab = "Packages"; @@ -18,8 +19,8 @@ - - + + } @section Meta { @if (!Model.Listed || Model.Deleted) @@ -28,7 +29,7 @@ } } @section SideColumn { - +

    @Model.TotalDownloadCount.ToNuGetNumberString()

    @@ -49,17 +50,17 @@
    \ No newline at end of file + diff --git a/src/NuGetGallery/Views/Shared/SiteMenu.cshtml b/src/NuGetGallery/Views/Shared/SiteMenu.cshtml index a79099d0b2..104bb2b3ec 100644 --- a/src/NuGetGallery/Views/Shared/SiteMenu.cshtml +++ b/src/NuGetGallery/Views/Shared/SiteMenu.cshtml @@ -19,6 +19,6 @@
  • Admin
  • }
  • Documentation
  • -
  • Downloads
  • +
  • Downloads
  • Blog
  • \ No newline at end of file diff --git a/src/NuGetGallery/Views/Shared/_ListPackage.cshtml b/src/NuGetGallery/Views/Shared/_ListPackage.cshtml index 7b81a5b146..c0df7e57a1 100644 --- a/src/NuGetGallery/Views/Shared/_ListPackage.cshtml +++ b/src/NuGetGallery/Views/Shared/_ListPackage.cshtml @@ -2,7 +2,7 @@
    @@ -25,7 +25,11 @@

    - @Model.Description.TruncateAtWordBoundary(350, "...", string.Format(System.Globalization.CultureInfo.CurrentCulture, " More information", @Url.Package(Model))) + @Model.ShortDescription + @if (Model.IsDescriptionTruncated) + { + @Html.RouteLink("More information", RouteName.DisplayPackage, new { Model.Id, Model.Version }) + }

    @if (!String.IsNullOrEmpty(Model.MinClientVersion)) @@ -46,10 +50,10 @@

    Tags

      - @foreach (var tag in Model.Tags) - { -
    • @tag
    • - } + @foreach (var tag in Model.Tags) + { +
    • @tag
    • + }
    } diff --git a/src/NuGetGallery/Views/Statistics/PackageDownloadsByVersion.cshtml b/src/NuGetGallery/Views/Statistics/PackageDownloadsByVersion.cshtml index f8aa65d191..57c6c3c867 100644 --- a/src/NuGetGallery/Views/Statistics/PackageDownloadsByVersion.cshtml +++ b/src/NuGetGallery/Views/Statistics/PackageDownloadsByVersion.cshtml @@ -27,19 +27,24 @@ else @Scripts.Render("~/Scripts/statsdimensions.js") @Scripts.Render("~/Scripts/perpackagestatsgraphs.js") } diff --git a/src/NuGetGallery/Views/Statistics/PackageDownloadsDetail.cshtml b/src/NuGetGallery/Views/Statistics/PackageDownloadsDetail.cshtml index 4f53749cf0..3aef019d47 100644 --- a/src/NuGetGallery/Views/Statistics/PackageDownloadsDetail.cshtml +++ b/src/NuGetGallery/Views/Statistics/PackageDownloadsDetail.cshtml @@ -27,19 +27,25 @@ else @Scripts.Render("~/Scripts/statsdimensions.js") @Scripts.Render("~/Scripts/perpackagestatsgraphs.js") } diff --git a/src/NuGetGallery/Views/Statistics/_PivotTable.cshtml b/src/NuGetGallery/Views/Statistics/_PivotTable.cshtml index 328cf4bd3d..ec1e9b33af 100644 --- a/src/NuGetGallery/Views/Statistics/_PivotTable.cshtml +++ b/src/NuGetGallery/Views/Statistics/_PivotTable.cshtml @@ -8,7 +8,7 @@
    @foreach (var item in Model.Dimensions) - { + {
    @@ -88,5 +88,6 @@ } +
    diff --git a/src/NuGetGallery/Views/Users/Account.cshtml b/src/NuGetGallery/Views/Users/Account.cshtml index 9d1649ec2f..3732a86374 100644 --- a/src/NuGetGallery/Views/Users/Account.cshtml +++ b/src/NuGetGallery/Views/Users/Account.cshtml @@ -1,89 +1,9 @@ @model AccountViewModel -@helper AccordianBar( - string groupName, - string title, - string subtitle = null, - bool enabled = true, - string formModelStatePrefix = null, - Func actions = null, - Func content = null -) -{ - Func titleTemplate = null; - if (!String.IsNullOrEmpty(title)) - { - titleTemplate = new Func(@@title); - } - - Func subtitleTemplate = null; - if (!String.IsNullOrEmpty(subtitle)) - { - subtitleTemplate = new Func(@@subtitle); - } - - @AccordianBar(groupName, - titleTemplate, - subtitleTemplate, - enabled, - formModelStatePrefix, - actions, - content) -} - -@helper AccordianBar( - string groupName, - Func title, - Func subtitle = null, - bool enabled = true, - string formModelStatePrefix = null, - Func actions = null, - Func content = null -) -{ - @* Calculate Accordian Index *@ - string dataKey = "___AccordianCounter_" + groupName; - int lastId = (int)(HttpContext.Current.Items[dataKey] ?? 0); - int id = lastId + 1; - HttpContext.Current.Items[dataKey] = id; - string name = groupName + "-" + id.ToString(); - - var hlp = new AccordianHelper(name, formModelStatePrefix, this); -
  • - @if (actions != null) - { -
    - @actions(hlp) -
    - } - - @title(hlp) - - @if (subtitle != null) - { - - @subtitle(hlp) - - } - @if (content != null) - { -
    - @content(hlp) -
    - } -
  • -} - @{ ViewBag.Title = "Edit Profile"; var credGroups = Model.Credentials.GroupBy(c => c.Kind).ToDictionary(g => g.Key, g => g.ToList()); - CredentialViewModel apiKey = null; - if (credGroups.ContainsKey(CredentialKind.Token)) - { - apiKey = credGroups[CredentialKind.Token].Single(); - } - var hasPassword = credGroups.ContainsKey(CredentialKind.Password); var loginCredentials = hasPassword ? 1 : 0; @@ -97,12 +17,12 @@

    My NuGet.org Account

    -@if (!String.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress) && !String.IsNullOrEmpty(CurrentUser.EmailAddress)) +@if (!string.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress) && !string.IsNullOrEmpty(CurrentUser.EmailAddress)) {

    You recently registered a new email address: @CurrentUser.UnconfirmedEmailAddress
    - @if (!String.IsNullOrEmpty(CurrentUser.EmailAddress)) + @if (!string.IsNullOrEmpty(CurrentUser.EmailAddress)) { We will continue sending notification emails to your old verified email address (@CurrentUser.EmailAddress) until you verify your new email address. @@ -116,6 +36,10 @@

    } + + What do you think about NuGet.org?

    +
    + @if (!CurrentUser.Confirmed) {

    @@ -153,150 +77,156 @@ -

      +
        @************************** Email Address **************************@ - @AccordianBar( - groupName: "editprofile", - title: (String.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress) ? "Email Address" : "Pending Email Address"), - subtitle: currentEmailAddress, - enabled: String.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress), - formModelStatePrefix: "ChangeEmail", + @ViewHelpers.AccordionBar( + groupName: "editprofile", + page: this, + title: (string.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress) ? "Email Address" : "Pending Email Address"), + subtitle: currentEmailAddress, + enabled: string.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress), + formModelStatePrefix: "ChangeEmail", actions: @@item.ExpandButton("Change", "Cancel") - @if (!String.IsNullOrEmpty(CurrentUser.EmailAddress) && !String.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress)) - { - using (Html.BeginForm("CancelChangeEmail", "Users", FormMethod.Post, new { @class = "form-inline" })) - { -
        - Reset to Confirmed Email Address - @Html.AntiForgeryToken() - -
        - } - } -
        , + @if (!String.IsNullOrEmpty(CurrentUser.EmailAddress) && !String.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress)) + { + using (Html.BeginForm("CancelChangeEmail", "Users", FormMethod.Post, new {@class = "form-inline"})) + { +
        + Reset to Confirmed Email Address + @Html.AntiForgeryToken() + +
        + } + } + , content: @ - @using (Html.BeginForm("ChangeEmail", "Users", FormMethod.Post, new { @class = "form-inline" })) - { -
        - Change Email Address - - @Html.AntiForgeryToken() - @Html.ValidationSummaryFor("ChangeEmail") - -
        - @Html.LabelFor(m => m.ChangeEmail.NewEmail) - @Html.TextBoxFor(m => m.ChangeEmail.NewEmail) - @Html.ValidationMessageFor(m => m.ChangeEmail.NewEmail) -
        - - @if (hasPassword) - { -
        - @Html.LabelFor(m => m.ChangeEmail.Password) - @Html.PasswordFor(m => m.ChangeEmail.Password) - @Html.ValidationMessageFor(m => m.ChangeEmail.Password) -
        - } - - Blue border on left means required. - - -
        - } -
        ) + @using (Html.BeginForm("ChangeEmail", "Users", FormMethod.Post, new {@class = "form-inline"})) + { +
        + Change Email Address + + @Html.AntiForgeryToken() + @Html.ValidationSummaryFor("ChangeEmail") + +
        + @Html.LabelFor(m => m.ChangeEmail.NewEmail) + @Html.TextBoxFor(m => m.ChangeEmail.NewEmail) + @Html.ValidationMessageFor(m => m.ChangeEmail.NewEmail) +
        + + @if (hasPassword) + { +
        + @Html.LabelFor(m => m.ChangeEmail.Password) + @Html.PasswordFor(m => m.ChangeEmail.Password) + @Html.ValidationMessageFor(m => m.ChangeEmail.Password) +
        + } + + Blue border on left means required. + + +
        + } + ) @************************** Email Notifications **************************@ - @AccordianBar( - groupName: "editprofile", - title: "Email Notifications", - enabled: true, + @ViewHelpers.AccordionBar( + groupName: "editprofile", + page: this, + title: "Email Notifications", + enabled: true, actions: @ - @(CurrentUser.EmailAllowed ? "Users can contact you" : "Users can not contact you") - | @(CurrentUser.NotifyPackagePushed ? "Receiving package push notifications" : "Not receiving package push notifications") - @item.ExpandButton("Change", "Cancel") - , + @(CurrentUser.EmailAllowed ? "Users can contact you" : "Users can not contact you") + | @(CurrentUser.NotifyPackagePushed ? "Receiving package push notifications" : "Not receiving package push notifications") + @item.ExpandButton("Change", "Cancel") + , content: @ - @using (Html.BeginRouteForm(routeName: RouteName.ChangeEmailSubscription, method: FormMethod.Post, htmlAttributes: new { @class = "form-inline" })) - { -
        - Update Email Notifications - @Html.AntiForgeryToken() -
        - - -

        - This subscription allows other registered users of the site to contact you - about packages that you own using the Contact Owners form, or to request - that you become an owner of their package. Unsubscribing means users cannot contact you for these reasons. -

        -
        -
        - - -

        - This subscription will send a notification whenever a package is pushed using your account. - We recommend to enable this subscription so that you can inspect whether a package was pushed intentional or not. - Unsubscribing means no notification will be sent on push. -

        -
        - -
        - } - -

        -
        Note: We will always send important account management and security notices. -

        -
        ) + @using (Html.BeginRouteForm(routeName: RouteName.ChangeEmailSubscription, method: FormMethod.Post, htmlAttributes: new {@class = "form-inline"})) + { +
        + Update Email Notifications + @Html.AntiForgeryToken() +
        + + +

        + This subscription allows other registered users of the site to contact you + about packages that you own using the Contact Owners form, or to request + that you become an owner of their package. Unsubscribing means users cannot contact you for these reasons. +

        +
        +
        + + +

        + This subscription will send a notification whenever a package is pushed using your account. + We recommend to enable this subscription so that you can inspect whether a package was pushed intentional or not. + Unsubscribing means no notification will be sent on push. +

        +
        + +
        + } + +

        +
        Note: We will always send important account management and security notices. +

        + ) @************************** Profile Picture **************************@ - @AccordianBar( - groupName: "editprofile", + @ViewHelpers.AccordionBar( + groupName: "editprofile", + page: this, title: @ - Profile Picture - @if(!String.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress)) { - @:(preview) - } - , + Profile Picture + @if (!String.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress)) + { + @:(preview) + } + , subtitle: @@ViewHelpers.GravatarImage(currentEmailAddress, CurrentUser.Username, 32), actions: @@item.ExpandLink("More Info", "Less Info"), content: @ - @if (!String.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress)) - { - This is a preview of how your profile picture will look once you have verified your - current email address. - } - We use your verified email address and - gravatar.com - to get your profile picture. - Go to gravatar.com - to change your profile picture. - ) + @if (!String.IsNullOrEmpty(CurrentUser.UnconfirmedEmailAddress)) + { + This is a preview of how your profile picture will look once you have verified your + current email address. + } + We use your verified email address and + gravatar.com + to get your profile picture. + Go to gravatar.com + to change your profile picture. + ) @************************** Curated Feeds **************************@ - @if (Model.CuratedFeeds.Any()) - { - foreach (var curatedFeed in Model.CuratedFeeds) - { - @AccordianBar( - groupName: "editprofile", - title: "Curated Feed", - subtitle: curatedFeed, - actions: @ - - Manage - - ) + @if (Model.CuratedFeeds.Any()) + { + foreach (var curatedFeed in Model.CuratedFeeds) + { + @ViewHelpers.AccordionBar( + groupName: "editprofile", + page: this, + title: "Curated Feed", + subtitle: curatedFeed, + actions: @ + + Manage + + ) } }
      +

      Credentials

      @if (hasPassword && credGroups.ContainsKey(CredentialKind.External) && credGroups[CredentialKind.External].Any()) { @@ -306,181 +236,109 @@

      } -
        - @************************** - External Credentials - **************************@ +
          + @************************** + External Credentials + **************************@ @if (credGroups.ContainsKey(CredentialKind.External)) { foreach (var cred in credGroups[CredentialKind.External]) { - @AccordianBar( + @ViewHelpers.AccordionBar( groupName: "editprofile", + page: this, title: cred.TypeCaption, subtitle: cred.Identity, - actions: @ - @item.ExpandLink("More Info", "Less Info") - @if (loginCredentials > 1) - { - using (Html.BeginForm("RemoveCredential", "Users", new { credentialType = cred.Type }, FormMethod.Post, new { @class = "form-inline" })) - { -
          - Remove @cred.TypeCaption Credential - @Html.AntiForgeryToken() - @Html.Hidden(cred.Type) - -
          - } - } -
          , - content: @

          - You can use this @cred.AuthUI.AccountNoun to sign in to @Config.Current.Brand -

          ) + actions: @ + @item.ExpandLink("More Info", "Less Info") + @if (loginCredentials > 1) + { + using (Html.BeginForm("RemoveCredential", "Users", new { credentialType = cred.Type, credentialKey = cred.Key }, FormMethod.Post, new { @class = "form-inline" })) + { +
          + Remove @cred.TypeCaption Credential + @Html.AntiForgeryToken() + @Html.Hidden(cred.Type) + +
          + } + } +
          , + content: @

          + You can use this @cred.AuthUI.AccountNoun to sign in to @Config.Current.Brand +

          ) } - } - - @************************** - Password - **************************@ - @AccordianBar( + } + + @************************** + Password + **************************@ + @ViewHelpers.AccordionBar( groupName: "editprofile", + page: this, title: (hasPassword ? "Password Login" : "Password Login Disabled"), enabled: hasPassword, formModelStatePrefix: "ChangePassword", - actions: @ - @if (hasPassword) - { - @item.ExpandButton("Change", "Cancel") - if (loginCredentials > 1) - { - using (Html.BeginForm("RemovePassword", "Users", FormMethod.Post, new { @class = "form-inline" })) - { -
          - Disable Password Login - @Html.AntiForgeryToken() - -
          - } - } - } - else - { - using (Html.BeginForm("ChangePassword", "Users", FormMethod.Post, new { @class = "form-inline" })) - { -
          - Enable Password Login - @Html.AntiForgeryToken() - -
          - } - } -
          , - content: @ - @using (Html.BeginForm("ChangePassword", "Users")) - { -
          - Change Password Form - - @Html.AntiForgeryToken() - @Html.ValidationSummaryFor("ChangePassword") -
          - @Html.LabelFor(m => m.ChangePassword.OldPassword) - @Html.PasswordFor(m => m.ChangePassword.OldPassword) - @Html.ValidationMessageFor(m => m.ChangePassword.OldPassword) -
          - -
          - @Html.LabelFor(m => m.ChangePassword.NewPassword) - @Html.PasswordFor(m => m.ChangePassword.NewPassword) - @Html.ValidationMessageFor(m => m.ChangePassword.NewPassword) -
          - - Blue border on left means required. - - -
          - } -
          ) - - @************************** - API Key - **************************@ - @AccordianBar( - groupName: "editprofile", - title: @ - API Key: - @if (!CurrentUser.Confirmed) - { - You need to - confirm your account before viewing your API Key - } - else if (apiKey == null) - { - You don't have an API key - } - else - { - @apiKey.Value - - } - , - actions: @ - @item.ExpandLink("More Info", "Less Info") - @if (CurrentUser.Confirmed) - { - using (Html.BeginForm("GenerateApiKey", "Users", FormMethod.Post, new { @class = "form-inline" })) - { -
          - @(apiKey == null ? "Generate an API Key" : "Generate a new API Key") - @Html.AntiForgeryToken() - -
          - } - } -
          , - content: @ - @if (!CurrentUser.Confirmed) - { -

          - Your API key provides you with a token that can identify you to the gallery. The - NuGet command-line utility allows you to - submit a NuGet package to the gallery using your API key to authenticate. - To get an API Key you will need to confirm your account. -

          - } - else if (apiKey == null) - { -

          - An API key provides you with a token that can identify you to the gallery. The - NuGet command-line utility allows you to - submit a NuGet package to the gallery using your API key to authenticate. -

          - } - else - { -

          - Your API key provides you with a token that identifies you to the gallery. - Keep this a secret. You can always regenerate your key at any time (invalidating - previous keys) if your token is accidentally revealed. The - NuGet command-line utility allows you to - submit a NuGet package to the gallery, and you would pass your token like this: -

          - -

          Example usage:

          -

          -

          -
          - nuget.exe setApiKey @apiKey.Value - - -
          -
          nuget.exe push MyPackage.1.0.nupkg -Source https://www.nuget.org/api/v2/package
          -
          -

          - } -
          ) -
        - + actions: @ + @if (hasPassword) + { + @item.ExpandButton("Change", "Cancel") + if (loginCredentials > 1) + { + using (Html.BeginForm("RemovePassword", "Users", FormMethod.Post, new { @class = "form-inline" })) + { +
        + Disable Password Login + @Html.AntiForgeryToken() + +
        + } + } + } + else + { + using (Html.BeginForm("ChangePassword", "Users", FormMethod.Post, new { @class = "form-inline" })) + { +
        + Enable Password Login + @Html.AntiForgeryToken() + +
        + } + } +
        , + content: @ + @using (Html.BeginForm("ChangePassword", "Users")) + { +
        + Change Password Form +

        + Using this form, you can change your password for @(Config.Current.Brand). +

        + @Html.AntiForgeryToken() + @Html.ValidationSummaryFor("ChangePassword") +
        + @Html.LabelFor(m => m.ChangePassword.OldPassword) + @Html.PasswordFor(m => m.ChangePassword.OldPassword) + @Html.ValidationMessageFor(m => m.ChangePassword.OldPassword) +
        +
        + @Html.LabelFor(m => m.ChangePassword.NewPassword) + @Html.PasswordFor(m => m.ChangePassword.NewPassword) + @Html.ValidationMessageFor(m => m.ChangePassword.NewPassword) +
        + Blue border on left means required. + +
        + } +
        ) + @************************** + API Key + **************************@ + + @Html.Partial("ApiKeys", Model) + +
      @section bottomScripts { -} \ No newline at end of file +} diff --git a/src/NuGetGallery/Views/Users/ApiKeys.cshtml b/src/NuGetGallery/Views/Users/ApiKeys.cshtml new file mode 100644 index 0000000000..e31ecd8a7f --- /dev/null +++ b/src/NuGetGallery/Views/Users/ApiKeys.cshtml @@ -0,0 +1,1370 @@ +@using NuGetGallery.Authentication +@model AccountViewModel + +@Scripts.Render("~/Scripts/jquery-1.11.0.min.js") +@Scripts.Render("~/Scripts/moment.min.js") + +@Html.AntiForgeryToken() + +@{ + var apiKeys = Model.Credentials.Where(c => c.Kind == CredentialKind.Token).ToList(); + + // Auto-expand when an API key has expired or api key deleted + var shouldExpandApiKeyAccordion = + apiKeys.Any(k => k.HasExpired) || + string.Equals(TempData["Message"], Strings.CredentialRemoved); + + // Sort the api keys by description, when a legacy api key appears first + apiKeys.Sort((k1, k2) => + { + if (k1.IsNonScopedV1ApiKey) + { + return -1; + } + if (k2.IsNonScopedV1ApiKey) + { + return 1; + } + + return k1.Description.CompareTo(k2.Description); + }); + + var newestApiKeyWithDuration = apiKeys.Where(x => x.ExpirationDuration != null).OrderByDescending(x => x.Created).FirstOrDefault(); + int defaultDurationDays = 365; + + if (newestApiKeyWithDuration != null) + { + defaultDurationDays = newestApiKeyWithDuration.ExpirationDuration.Value.Days; + } +} + +@ViewHelpers.AccordionBar( + groupName: "apikeys", + page: this, + title: @API Keys, + enabled: CurrentUser.Confirmed, + expanded: shouldExpandApiKeyAccordion, + actions: @ + @if (!CurrentUser.Confirmed) + { + + You need to + confirm your account before viewing your API Keys + + } + else + { + if (apiKeys.Count == 0) + { + You don't have any API keys yet + } + else if (apiKeys.Any(k => k.HasExpired)) + { + One of your API keys has expired + } + + @item.ExpandLink("Show details", "Hide details") + } + , + content: @ + +

      + An API key is a token that can identify you to @(Config.Current.Brand). The + NuGet command-line utility allows you to + submit a NuGet package to the gallery using your API key to authenticate. +

      + @if (!CurrentUser.Confirmed) + { +

      To get an API Key you will need to confirm your account.

      + } + @if (CurrentUser.Confirmed) + { +

      You don't have any API keys yet. Use the form below to generate an API key and start publishing packages to @(Config.Current.Brand).

      +

      Always keep your API keys a secret! If one of your keys is accidentally revealed, you can always generate a new one at any time. You can also remove existing API keys if necessary.

      +

      Manage API Keys

      + +
      +
        @AddApiKey(!apiKeys.Any())
      +
      +
      Keys
      +
      + + + + + + + + + @foreach (var apiKey in apiKeys) + { + + + + + + } + +
      + @if (apiKey.HasExpired) + { + + } + else if (apiKey.IsNonScopedV1ApiKey) + { + + } + else + { + + } + + @apiKey.Description
      + + Expires: + @if (!apiKey.Expires.HasValue) + { + Never + } + else if (!apiKey.HasExpired) + { + @apiKey.Expires.Value.ToNuGetShortDateString() + } + else + { + Expired + } +
      + @if (apiKey.Scopes.Any()) + { + var scopesList = apiKey.Scopes.Select(s =>s.AllowedAction).Distinct().OrderBy(s => s).ToList(); + var subjectsList = apiKey.Scopes.Select(s => s.Subject).Distinct().OrderBy(s => s).ToList(); + var globPattern = subjectsList.FirstOrDefault(x => x != null && x.Contains("*")); + + if (globPattern != null) + { + subjectsList.Remove(globPattern); + } + + + Scopes: + @string.Join(", ", scopesList.Take(3)) + @if (scopesList.Count > 3) + { + More... +
        + @foreach (var scope in scopesList) + { +
      • @scope
      • + } +
      + } +

      + + if (globPattern != null) + { + Glob pattern: @globPattern
      + } + + if (subjectsList.Any()) + { + const int itemsPerBatch = 10; + + + Packages: + @string.Join(", ", subjectsList.Take(3)) + @if (subjectsList.Count > 3) + { + More... +
        + @for (int i = 0; i < subjectsList.Count; i++) + { + var lastInBatch = i != 0 && i%itemsPerBatch == (itemsPerBatch - 1); +
      • + @subjectsList[i] + + @if (lastInBatch && i != subjectsList.Count - 1) + { + + } +
      • + } +
      + } +
      + } + } + else + { + Scopes: All
      + Packages: All + } +
      + @if (apiKey.IsNonScopedV1ApiKey) + { + + + } + + @if (!apiKey.IsNonScopedV1ApiKey) + { + + + using (Html.BeginForm("RegenerateCredential", "Users", FormMethod.Post, new {@class = "form-inline regenerate-api-key"})) + { + @Html.AntiForgeryToken() + @Html.Hidden("credentialType", apiKey.Type) + @Html.Hidden("credentialKey", apiKey.Key) + + } + } + + @using (Html.BeginForm("RemoveCredential", "Users", FormMethod.Post, new {@class = "form-inline remove-api-key"})) + { + @Html.AntiForgeryToken() + @Html.Hidden("credentialType", apiKey.Type) + @Html.Hidden("credentialKey", apiKey.Key) + + } +
      +
      + + + var siteRoot = Config.Current.RequireSSL + ? Config.Current.SiteRoot.TrimEnd('/').Replace("http://", "https://") + : Config.Current.SiteRoot.TrimEnd('/'); + + var exampleApiKey = "[your API key]"; + +
      +

      Example usage

      +

      + The following snippet demonstrates how you can use the NuGet + command-line utility to submit a NuGet package to the gallery: +

      +

      +

      +
      nuget.exe setApiKey @exampleApiKey -source @siteRoot
      +
      nuget.exe push MyPackage.1.0.nupkg -Source @siteRoot/api/v2/package
      +
      +

      +
      + } +
      +
      ) + + + +@helper AddApiKey(bool expanded) +{ + @ViewHelpers.AccordionBar( + groupName: "newapikey", + page: this, + title: @API Keys, + expanded: expanded, + actions: @ @item.ExpandButton("New API key", "Cancel") , + content: @ + @using (Html.BeginForm("GenerateApiKey", "Users", FormMethod.Post, new { @class = "form-inline", @style = "width:100%", @id = "generateKeyForm", @onkeypress= "return event.keyCode != 13;" })) + { +
      + @Html.AntiForgeryToken() +
      + + + + @if (Model.ExpirationInDaysForApiKeyV1 > 0) + { + + } + + + + @if (Model.ExpirationInDaysForApiKeyV1 > 0) + { + + } + +
      Key nameExpires in
      + + + Blue border on left means required. + + +
      +
      +
      +
      Select scopes

      +
      + +
      + + +
      + + +
      + + + +
      +
      +
      +
      Select packages

      + To select which packages to associate with a key use a glob pattern, select individual packages, or both. + + Example glob patterns. + + +
      + + + + + + + +
      Glob pattern
      + + +
      +
      +
      + + + + + + + +
      Available packages
      +
      + @if (Model.Packages != null && Model.Packages.Count > 0) + { + foreach (var package in Model.Packages) + { + + } + } + else + { + You have no packages + } +
      +
      +
      +
      +
      + + +
      +
      + } +
      ) +} + + + + + + + + diff --git a/src/NuGetGallery/Views/Users/ResetPassword.cshtml b/src/NuGetGallery/Views/Users/ResetPassword.cshtml index deb24a8359..ac2b4c20bd 100644 --- a/src/NuGetGallery/Views/Users/ResetPassword.cshtml +++ b/src/NuGetGallery/Views/Users/ResetPassword.cshtml @@ -1,35 +1,33 @@ @model PasswordResetViewModel @{ ViewBag.Title = ViewBag.ForgotPassword ? "Reset Password" : "Set Password"; -} - -@if(ViewBag.ForgotPassword) { -

      Forgot Password

      - -

      - We are sorry to hear you forgot your NuGet.org account password. Enter your email below and we will - send instructions to reset your password. -

      -} else { -

      Set Password

      - -

      - You can set a NuGet.org account password using the form below. +} + +

      @(ViewBag.Title)

      + +@if (ViewBag.ForgotPassword) +{ +

      + We are sorry to hear you forgot your NuGet.org account password. You can reset the NuGet.org account password using the form below.

      } - -@using (Html.BeginForm()) +else { -
      - @(ViewBag.ForgotPassword ? "Reset Password" : "Set Password") - - @Html.AntiForgeryToken() - @Html.ValidationSummary(true) - - @Html.EditorForModel() - - Blue border on left means required. - - -
      +

      + You can set a NuGet.org account password using the form below. +

      +} + +@using (Html.BeginForm()) +{ +
      + @(ViewBag.Title) + @Html.AntiForgeryToken() + @Html.ValidationSummary(true) + + @Html.EditorForModel() + + Blue border on left means required. + +
      } \ No newline at end of file diff --git a/src/NuGetGallery/Web.Debug.config b/src/NuGetGallery/Web.Debug.config index 2c6dd51a70..99329c622e 100644 --- a/src/NuGetGallery/Web.Debug.config +++ b/src/NuGetGallery/Web.Debug.config @@ -6,7 +6,7 @@ @@ -52,9 +52,9 @@ - - - + + + @@ -66,7 +66,7 @@ - + @@ -80,8 +80,15 @@ - + + + + + @@ -108,10 +115,12 @@ - + + + @@ -119,6 +128,7 @@ + @@ -132,6 +142,12 @@ + + + + + + @@ -144,13 +160,13 @@ - + - + @@ -161,7 +177,7 @@ - + @@ -237,7 +253,8 @@ --> - + + @@ -251,14 +268,13 @@ - + - - - + + + - @@ -300,14 +316,13 @@ - - - + + + - + - @@ -459,7 +474,7 @@ - + @@ -483,15 +498,15 @@ - + - + - + @@ -501,6 +516,14 @@ + + + + + + + + @@ -516,4 +539,11 @@ - \ No newline at end of file + + + + + + + + diff --git a/src/NuGetGallery/WebApi/PlainTextResult.cs b/src/NuGetGallery/WebApi/PlainTextResult.cs index ade468b35c..31f082b60e 100644 --- a/src/NuGetGallery/WebApi/PlainTextResult.cs +++ b/src/NuGetGallery/WebApi/PlainTextResult.cs @@ -1,6 +1,7 @@ // 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.Net; using System.Net.Http; using System.Text; using System.Threading; @@ -15,10 +16,18 @@ public class PlainTextResult private readonly HttpRequestMessage _request; public string Content { get; private set; } + public HttpStatusCode StatusCode { get; private set; } + + public PlainTextResult(string content, HttpRequestMessage request, HttpStatusCode statusCode):this(content, request) + { + StatusCode = statusCode; + } + public PlainTextResult(string content, HttpRequestMessage request) { _request = request; Content = content; + StatusCode = HttpStatusCode.OK; } public Task ExecuteAsync(CancellationToken cancellationToken) @@ -26,7 +35,8 @@ public Task ExecuteAsync(CancellationToken cancellationToke var response = new HttpResponseMessage() { Content = new StringContent(Content, Encoding.UTF8, "text/plain"), - RequestMessage = _request + RequestMessage = _request, + StatusCode = this.StatusCode }; return Task.FromResult(response); } diff --git a/src/NuGetGallery/WebApi/QueryResult.cs b/src/NuGetGallery/WebApi/QueryResult.cs index f007ddbb90..0cedee8c3e 100644 --- a/src/NuGetGallery/WebApi/QueryResult.cs +++ b/src/NuGetGallery/WebApi/QueryResult.cs @@ -176,11 +176,23 @@ public IHttpActionResult GetInnerResult() // Handle single result if (modelQueryResults != null) { - return NegotiatedContentResult(modelQueryResults.FirstOrDefault()); + var model = modelQueryResults.FirstOrDefault(); + if (model == null) + { + return NotFoundResult(); + } + + return NegotiatedContentResult(model); } else if (projectedQueryResults != null) { - return NegotiatedContentResult(projectedQueryResults.AsEnumerable().FirstOrDefault()); + var model = projectedQueryResults.AsEnumerable().FirstOrDefault(); + if (model == null) + { + return NotFoundResult(); + } + + return NegotiatedContentResult(model); } } } diff --git a/src/NuGetGallery/packages.config b/src/NuGetGallery/packages.config index 76bc3a7b57..46af8ce9d2 100644 --- a/src/NuGetGallery/packages.config +++ b/src/NuGetGallery/packages.config @@ -1,97 +1,106 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/README.md b/src/README.md deleted file mode 100644 index f5cef813d0..0000000000 --- a/src/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Source Code -The Main Source Code for the NuGet Gallery frontend, backend and operations worker - -## Projects - -* galops - NuGet Gallery Operations tools -* NuGetGallery.Backend - NuGet Gallery Backend Worker -* NuGetGallery.Backend.Cloud - Cloud Service Project for deploying NuGetGallery.Backend to an Azure Worker Role -* NuGetGallery.Frontend - NuGet Gallery Frontend Website -* NuGetGallery.Operations - Support Library shared by NuGet Operations tools and Backend. \ No newline at end of file diff --git a/src/galops/DebugHelper.cs b/src/galops/DebugHelper.cs deleted file mode 100644 index 337fb15800..0000000000 --- a/src/galops/DebugHelper.cs +++ /dev/null @@ -1,25 +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.Diagnostics; -using System.Linq; -using System.Threading; - -namespace NuGetGallery.Operations.Tools -{ - public static class DebugHelper - { - [Conditional("DEBUG")] - public static void WaitForDebugger(ref string[] args) - { - if (args.Length >= 1 && - (String.Equals("dbg", args[0], StringComparison.OrdinalIgnoreCase) || - String.Equals("debug", args[0], StringComparison.OrdinalIgnoreCase))) - { - args = args.Skip(1).ToArray(); - Console.WriteLine(Strings.WaitingForDebugger); - Debugger.Launch(); - } - } - } -} diff --git a/src/galops/ExceptionUtility.cs b/src/galops/ExceptionUtility.cs deleted file mode 100644 index 938f75f7dd..0000000000 --- a/src/galops/ExceptionUtility.cs +++ /dev/null @@ -1,32 +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.Reflection; - -namespace NuGet -{ - public static class ExceptionUtility - { - public static Exception Unwrap(Exception exception) - { - if (exception == null) - { - throw new ArgumentNullException(nameof(exception)); - } - - if (exception.InnerException == null) - { - return exception; - } - - // Always return the inner exception from a target invocation exception - if (exception is AggregateException || - exception is TargetInvocationException) - { - return exception.GetBaseException(); - } - - return exception; - } - } -} diff --git a/src/galops/GlobalSuppressions.cs b/src/galops/GlobalSuppressions.cs deleted file mode 100644 index 409489dbf5..0000000000 Binary files a/src/galops/GlobalSuppressions.cs and /dev/null differ diff --git a/src/galops/Program.cs b/src/galops/Program.cs deleted file mode 100644 index 0d91f044ce..0000000000 --- a/src/galops/Program.cs +++ /dev/null @@ -1,131 +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.Linq; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.ComponentModel.Composition.Hosting; -using NuBot.Infrastructure; -using NuGet; -using NLog.Config; -using NLog.Targets; -using NLog; -using System.Diagnostics.CodeAnalysis; - -namespace NuGetGallery.Operations.Tools -{ - [Export] - public class Program - { - private Logger _logger = LogManager.GetLogger("task.Program"); - - public HelpCommand HelpCommand { get; set; } - - [ImportMany] - public IEnumerable Commands { get; set; } - - [Import] - public ICommandManager Manager { get; set; } - - static int Main(string[] args) - { - DebugHelper.WaitForDebugger(ref args); - - // Configure Logging - ConfigureLogs(); - - // Compose - var catalog = new AggregateCatalog( - new AssemblyCatalog(typeof(Program).Assembly), - new AssemblyCatalog(typeof(HelpCommand).Assembly)); - var container = new CompositionContainer(catalog); - var p = container.GetExportedValue(); - - // Execute - return p.Invoke(args); - } - - public int Invoke(string[] args) - { - try - { - HelpCommand = new HelpCommand(Manager, "galops", "NuGet Gallery Operations", "https://github.com/NuGet/NuGetOperations/wiki/GalOps---Gallery-Operations-Commands"); - - // Add commands - foreach (ICommand cmd in Commands) - { - Manager.RegisterCommand(cmd); - } - - // Parse the command - var parser = new CommandLineParser(Manager); - ICommand command = parser.ParseCommandLine(args) ?? HelpCommand; - - // Fall back on help command if we failed to parse a valid command - if (!ArgumentCountValid(command)) - { - string commandName = command.CommandAttribute.CommandName; - Console.WriteLine(Strings.CommandInvalidArguments, commandName); - HelpCommand.ViewHelpForCommand(commandName); - } - else - { - command.Execute(); - } - } - catch (AggregateException exception) - { - string message; - Exception unwrappedEx = ExceptionUtility.Unwrap(exception); - if (unwrappedEx == exception) - { - // If the AggregateException contains more than one InnerException, it cannot be unwrapped. In which case, simply print out individual error messages - message = String.Join(Environment.NewLine, exception.InnerExceptions.Select(ex => ex.Message).Distinct(StringComparer.CurrentCulture)); - } - else - { - message = ExceptionUtility.Unwrap(exception).Message; - } - _logger.Error("{0}: {1}", unwrappedEx.GetType().Name, message); - _logger.Error(" Stack Trace: " + unwrappedEx.StackTrace); - return 1; - } - catch (Exception e) - { - var ex = ExceptionUtility.Unwrap(e); - _logger.Error("{0}: {1}", ex.GetType().Name, ex.Message); - _logger.Error(" Stack Trace: " + ex.StackTrace); - return 1; - } - return 0; - } - - private static void ConfigureLogs() - { - // Just a simple logging mechanism - var consoleTarget = new SnazzyConsoleTarget() - { - Layout = "${message}" - }; - var joblogTarget = new SnazzyConsoleTarget() - { - Layout = "[${date:format=yyyy-MM-dd} ${date:format=HH}:${date:format=mm}:${date:format=ss}] ${message}" - }; - - var config = new LoggingConfiguration(); - config.AddTarget("task", consoleTarget); - config.AddTarget("joblog", joblogTarget); - config.LoggingRules.Add(new LoggingRule("joblog.*", LogLevel.Trace, joblogTarget)); - config.LoggingRules.Add(new LoggingRule("task.*", LogLevel.Trace, consoleTarget)); - - LogManager.Configuration = config; - } - - public static bool ArgumentCountValid(ICommand command) - { - CommandAttribute attribute = command.CommandAttribute; - return command.Arguments.Count >= attribute.MinArgs && - command.Arguments.Count <= attribute.MaxArgs; - } - } -} diff --git a/src/galops/Properties/AssemblyInfo.cs b/src/galops/Properties/AssemblyInfo.cs deleted file mode 100644 index 8b89a4633f..0000000000 --- a/src/galops/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +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.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("galops")] -[assembly: AssemblyDescription("Command-line runner for NuGet Gallery Operations commands")] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("ce2f7d7f-caae-4399-b5f9-d32e0a0c525f")] diff --git a/src/galops/SnazzyConsoleTarget.cs b/src/galops/SnazzyConsoleTarget.cs deleted file mode 100644 index b49d0d301f..0000000000 --- a/src/galops/SnazzyConsoleTarget.cs +++ /dev/null @@ -1,123 +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 System.Diagnostics.CodeAnalysis; -using System.Linq; -using NLog; -using NLog.Targets; -using ColorPair = System.Tuple; - -namespace NuBot.Infrastructure -{ - [SuppressMessage("ReSharper", "LocalizableElement")] - public class SnazzyConsoleTarget : TargetWithLayout - { - private static readonly Dictionary ColorTable = new Dictionary() - { - { LogLevel.Debug, new ColorPair(ConsoleColor.Magenta, null) }, - { LogLevel.Error, new ColorPair(ConsoleColor.Red, null) }, - { LogLevel.Fatal, new ColorPair(ConsoleColor.White, ConsoleColor.Red) }, - { LogLevel.Info, new ColorPair(ConsoleColor.Green, null) }, - { LogLevel.Trace, new ColorPair(ConsoleColor.DarkGray, null) }, - { LogLevel.Warn, new ColorPair(ConsoleColor.Black, ConsoleColor.Yellow) } - }; - - private static readonly Dictionary LevelNames = new Dictionary() { - { LogLevel.Debug, "debug" }, - { LogLevel.Error, "error" }, - { LogLevel.Fatal, "fatal" }, - { LogLevel.Info, "info" }, - { LogLevel.Trace, "trace" }, - { LogLevel.Warn, "warn" }, - }; - - private static readonly int LevelLength = LevelNames.Values.Max(s => s.Length); - - protected override void Write(LogEventInfo logEvent) - { - var oldForeground = Console.ForegroundColor; - var oldBackground = Console.BackgroundColor; - - // Get us to the start of a line - if (Console.CursorLeft > 0) - { - Console.WriteLine(); - } - - // Get Color Pair colors - ColorPair pair; - if (!ColorTable.TryGetValue(logEvent.Level, out pair)) - { - pair = new ColorPair(Console.ForegroundColor, Console.BackgroundColor); - } - - // Get level string - string levelName; - if (!LevelNames.TryGetValue(logEvent.Level, out levelName)) - { - levelName = logEvent.Level.ToString(); - } - levelName = levelName.PadRight(LevelLength).Substring(0, LevelLength); - - // Break the message in to lines as necessary - var message = Layout.Render(logEvent); - var existingLines = message.Split(new string[] {Environment.NewLine}, StringSplitOptions.None); - var lines = new List(); - foreach (var existingLine in existingLines) - { - var prefix = levelName + ": "; - var fullMessage = prefix + existingLine; - var maxWidth = Console.BufferWidth - 2; - var currentLine = existingLine; - while (fullMessage.Length > maxWidth) - { - int end = maxWidth - prefix.Length; - int spaceIndex = currentLine.LastIndexOf(' ', Math.Min(end, message.Length - 1)); - if (spaceIndex < 10) - { - spaceIndex = end; - } - lines.Add(currentLine.Substring(0, spaceIndex).Trim()); - currentLine = currentLine.Substring(spaceIndex).Trim(); - fullMessage = prefix + currentLine; - } - lines.Add(currentLine); - } - - // Write lines - bool first = true; - foreach (var line in lines.Where(l => !String.IsNullOrWhiteSpace(l))) - { - if (first) - { - first = false; - } - else - { - Console.WriteLine(); - } - - // Write Level - Console.ForegroundColor = pair.Item1; - if (pair.Item2.HasValue) - { - Console.BackgroundColor = pair.Item2.Value; - } - Console.Write(levelName); - - // Write the message using the default foreground color, but the specified background color - // UNLESS: The background color has been changed. In which case the foreground color applies here too - var foreground = pair.Item2.HasValue - ? pair.Item1 - : oldForeground; - Console.ForegroundColor = foreground; - Console.Write(": " + line); - } - Console.WriteLine(); - - Console.ForegroundColor = oldForeground; - Console.BackgroundColor = oldBackground; - } - } -} diff --git a/src/galops/Strings.Designer.cs b/src/galops/Strings.Designer.cs deleted file mode 100644 index d0638a1e6a..0000000000 --- a/src/galops/Strings.Designer.cs +++ /dev/null @@ -1,81 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace NuGetGallery.Operations.Tools { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NuGetGallery.Operations.Tools.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to {0}: invalid arguments... - /// - internal static string CommandInvalidArguments { - get { - return ResourceManager.GetString("CommandInvalidArguments", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Waiting for Debugger.... - /// - internal static string WaitingForDebugger { - get { - return ResourceManager.GetString("WaitingForDebugger", resourceCulture); - } - } - } -} diff --git a/src/galops/Strings.resx b/src/galops/Strings.resx deleted file mode 100644 index e3fa4c6dc8..0000000000 --- a/src/galops/Strings.resx +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Waiting for Debugger... - - - {0}: invalid arguments.. - - \ No newline at end of file diff --git a/src/galops/app.config b/src/galops/app.config deleted file mode 100644 index 7323238a51..0000000000 --- a/src/galops/app.config +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/galops/galops.csproj b/src/galops/galops.csproj deleted file mode 100644 index 985eb21912..0000000000 --- a/src/galops/galops.csproj +++ /dev/null @@ -1,139 +0,0 @@ - - - - - Debug - x86 - 8.0.30703 - 2.0 - {F240D1BC-BBFB-4F22-9DF8-3FDE36BFD665} - Exe - Properties - NuGetGallery.Operations.Tools - galops - v4.6.1 - - - 512 - - - - - true - bin\Debug\ - DEBUG;TRACE - full - AnyCPU - prompt - ..\Backend.ruleset - true - MinimumRecommendedRules.ruleset - - - bin\Release\ - TRACE - true - pdbonly - AnyCPU - prompt - MinimumRecommendedRules.ruleset - false - - - - False - ..\..\packages\Microsoft.Data.Edm.5.6.5-beta\lib\net40\Microsoft.Data.Edm.dll - True - - - False - ..\..\packages\Microsoft.Data.OData.5.6.5-beta\lib\net40\Microsoft.Data.OData.dll - True - - - False - ..\..\packages\Microsoft.Data.Services.Client.5.6.5-beta\lib\net40\Microsoft.Data.Services.Client.dll - True - - - False - ..\..\packages\Microsoft.WindowsAzure.ConfigurationManager.3.1.0\lib\net40\Microsoft.WindowsAzure.Configuration.dll - True - - - False - C:\Program Files\Microsoft SDKs\Azure\.NET SDK\v2.7\ref\Microsoft.WindowsAzure.ServiceRuntime.dll - - - False - ..\..\packages\WindowsAzure.Storage.4.3.0\lib\net40\Microsoft.WindowsAzure.Storage.dll - True - - - ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - True - - - False - ..\..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll - True - - - - - - - False - ..\..\packages\System.Spatial.5.6.5-beta\lib\net40\System.Spatial.dll - True - - - - - - - - - - - - - - - - True - True - Strings.resx - - - - - {DBECF66B-8F2F-4B32-9143-E243BAFF12DF} - NuGetGallery.Operations - - - - - Designer - - - - - - - - - ResXFileCodeGenerator - Strings.Designer.cs - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - \ No newline at end of file diff --git a/src/galops/packages.config b/src/galops/packages.config deleted file mode 100644 index c210796ada..0000000000 --- a/src/galops/packages.config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/test.ps1 b/test.ps1 new file mode 100644 index 0000000000..6c99797d2b --- /dev/null +++ b/test.ps1 @@ -0,0 +1,65 @@ +[CmdletBinding(DefaultParameterSetName='RegularBuild')] +param ( + [ValidateSet("debug", "release")] + [string]$Configuration = 'debug', + [int]$BuildNumber +) + +# For TeamCity - If any issue occurs, this script fail the build. - By default, TeamCity returns an exit code of 0 for all powershell scripts, even if they fail +trap { + Write-Host "BUILD FAILED: $_" -ForegroundColor Red + Write-Host "ERROR DETAILS:" -ForegroundColor Red + Write-Host $_.Exception -ForegroundColor Red + Write-Host ("`r`n" * 3) + exit 1 +} + +. "$PSScriptRoot\build\common.ps1" + +Function Run-Tests { + [CmdletBinding()] + param() + + Trace-Log 'Running tests' + + $xUnitExe = (Join-Path $PSScriptRoot "packages\xunit.runner.console\tools\xunit.console.exe") + + $TestAssemblies = "tests\NuGetGallery.Core.Facts\bin\$Configuration\NuGetGallery.Core.Facts.dll", "tests\NuGetGallery.Facts\bin\$Configuration\NuGetGallery.Facts.dll" + + $TestCount = 0 + + foreach ($Test in $TestAssemblies) { + & $xUnitExe (Join-Path $PSScriptRoot $Test) -xml "Results.$TestCount.xml" + $TestCount++ + } +} + +Write-Host ("`r`n" * 3) +Trace-Log ('=' * 60) + +$startTime = [DateTime]::UtcNow +if (-not $BuildNumber) { + $BuildNumber = Get-BuildNumber +} +Trace-Log "Build #$BuildNumber started at $startTime" + +$BuildErrors = @() + +Invoke-BuildStep 'Running tests' { Run-Tests } ` + -ev +BuildErrors + +Trace-Log ('-' * 60) + +## Calculating Build time +$endTime = [DateTime]::UtcNow +Trace-Log "Build #$BuildNumber ended at $endTime" +Trace-Log "Time elapsed $(Format-ElapsedTime ($endTime - $startTime))" + +Trace-Log ('=' * 60) + +if ($BuildErrors) { + $ErrorLines = $BuildErrors | %{ ">>> $($_.Exception.Message)" } + Error-Log "Tests completed with $($BuildErrors.Count) error(s):`r`n$($ErrorLines -join "`r`n")" -Fatal +} + +Write-Host ("`r`n" * 3) diff --git a/tests/.nuget/packages.config b/tests/.nuget/packages.config index 0919176789..4bbca7b0cc 100644 --- a/tests/.nuget/packages.config +++ b/tests/.nuget/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/tests/NuGet.Config b/tests/NuGet.Config index 7656efd23e..bc5f024d10 100644 --- a/tests/NuGet.Config +++ b/tests/NuGet.Config @@ -9,7 +9,7 @@ - + diff --git a/tests/NuGetGallery.Core.Facts/Auditing/AggregateAuditingServiceTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/AggregateAuditingServiceTests.cs new file mode 100644 index 0000000000..9461e2721e --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/AggregateAuditingServiceTests.cs @@ -0,0 +1,80 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace NuGetGallery.Auditing +{ + public class AggregateAuditingServiceTests + { + [Fact] + public void Constructor_ThrowsForNull() + { + Assert.Throws(() => new AggregateAuditingService(services: null)); + } + + [Fact] + public async Task SaveAuditRecordAsync_ThrowsForNull() + { + var services = Enumerable.Empty(); + var aggregatedService = new AggregateAuditingService(services); + + await Assert.ThrowsAsync(() => aggregatedService.SaveAuditRecordAsync(record: null)); + } + + [Fact] + public async Task SaveAuditRecordAsync_AwaitsAllServices() + { + var services = CreateTestAuditingServices(); + var auditRecord = CreateAuditRecord(); + var aggregatedService = new AggregateAuditingService(services); + + await aggregatedService.SaveAuditRecordAsync(auditRecord); + + foreach (var service in services) + { + Assert.True(service.Awaited); + } + } + + private static AuditRecord CreateAuditRecord() + { + var packageRegistration = new PackageRegistration() + { + DownloadCount = 1, + Id = "a", + Key = 2 + }; + + return new PackageRegistrationAuditRecord(packageRegistration, AuditedPackageRegistrationAction.AddOwner, owner: "b"); + } + + private static IEnumerable CreateTestAuditingServices() + { + var services = new List(); + + for (var i = 0; i < 10; ++i) + { + services.Add(new TestAuditingService()); + } + + return services; + } + + private class TestAuditingService : AuditingService + { + internal bool Awaited { get; private set; } + + protected override Task SaveAuditRecordAsync(string auditData, string resourceType, string filePath, string action, DateTime timestamp) + { + Awaited = true; + + return Task.FromResult(0); + } + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/AuditActorTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/AuditActorTests.cs new file mode 100644 index 0000000000..eec8bb2e50 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/AuditActorTests.cs @@ -0,0 +1,351 @@ +// 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.Collections.Specialized; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Security.Principal; +using System.Threading.Tasks; +using System.Web; +using Moq; +using Xunit; + +namespace NuGetGallery.Auditing +{ + public class AuditActorTests + { + [Fact] + public void Constructor_WithoutOnBehalfOf_AcceptsNullValues() + { + var actor = new AuditActor( + machineName: null, + machineIP: null, + userName: null, + authenticationType: null, + timeStampUtc: DateTime.MinValue); + + Assert.Null(actor.MachineName); + Assert.Null(actor.MachineIP); + Assert.Null(actor.UserName); + Assert.Null(actor.AuthenticationType); + } + + [Fact] + public void Constructor_WithOnBehalfOf_AcceptsNullValues() + { + var actor = new AuditActor(machineName: null, + machineIP: null, + userName: null, + authenticationType: null, + timeStampUtc: DateTime.MinValue, + onBehalfOf: null); + + Assert.Null(actor.MachineName); + Assert.Null(actor.MachineIP); + Assert.Null(actor.UserName); + Assert.Null(actor.AuthenticationType); + Assert.Null(actor.OnBehalfOf); + } + + [Fact] + public void Constructor_WithoutOnBehalfOf_AcceptsEmptyStringValues() + { + var actor = new AuditActor( + machineName: "", + machineIP: "", + userName: "", + authenticationType: "", + timeStampUtc: DateTime.MinValue); + + Assert.Equal("", actor.MachineName); + Assert.Equal("", actor.MachineIP); + Assert.Equal("", actor.UserName); + Assert.Equal("", actor.AuthenticationType); + } + + [Fact] + public void Constructor_WithOnBehalfOf_AcceptsEmptyStringValues() + { + var actor = new AuditActor( + machineName: "", + machineIP: "", + userName: "", + authenticationType: "", + timeStampUtc: DateTime.MinValue, + onBehalfOf: null); + + Assert.Equal("", actor.MachineName); + Assert.Equal("", actor.MachineIP); + Assert.Equal("", actor.UserName); + Assert.Equal("", actor.AuthenticationType); + } + + [Fact] + public void Constructor_WithoutOnBehalfOf_SetsProperties() + { + var actor = new AuditActor( + machineName: "a", + machineIP: "b", + userName: "c", + authenticationType: "d", + timeStampUtc: DateTime.MinValue); + + Assert.Equal("a", actor.MachineName); + Assert.Equal("b", actor.MachineIP); + Assert.Equal("c", actor.UserName); + Assert.Equal("d", actor.AuthenticationType); + Assert.Equal(DateTime.MinValue, actor.TimestampUtc); + } + + [Fact] + public void Constructor_WithOnBehalfOf_SetsProperties() + { + var onBehalfOfActor = new AuditActor( + machineName: null, + machineIP: null, + userName: null, + authenticationType: null, + timeStampUtc: DateTime.MinValue); + var actor = new AuditActor( + machineName: "a", + machineIP: "b", + userName: "c", + authenticationType: "d", + timeStampUtc: DateTime.MinValue, + onBehalfOf: onBehalfOfActor); + + Assert.Equal("a", actor.MachineName); + Assert.Equal("b", actor.MachineIP); + Assert.Equal("c", actor.UserName); + Assert.Equal("d", actor.AuthenticationType); + Assert.Equal(DateTime.MinValue, actor.TimestampUtc); + Assert.Same(onBehalfOfActor, actor.OnBehalfOf); + } + + [Fact] + public async Task GetAspNetOnBehalfOfAsync_WithoutContext_ReturnsNullForNullHttpContext() + { + var actor = await AuditActor.GetAspNetOnBehalfOfAsync(); + + Assert.Null(actor); + } + + [Fact] + public async Task GetAspNetOnBehalfOfAsync_WithContext_ReturnsActor_WithHttpXForwardedForHeader() + { + var request = new Mock(); + var identity = new Mock(); + var user = new Mock(); + var context = new Mock(); + + request.SetupGet(x => x.ServerVariables) + .Returns(new NameValueCollection() { { "HTTP_X_FORWARDED_FOR", "a" } }); + identity.Setup(x => x.Name) + .Returns("b"); + identity.Setup(x => x.AuthenticationType) + .Returns("c"); + user.Setup(x => x.Identity) + .Returns(identity.Object); + context.Setup(x => x.Request) + .Returns(request.Object); + context.Setup(x => x.User) + .Returns(user.Object); + + var actor = await AuditActor.GetAspNetOnBehalfOfAsync(context.Object); + + Assert.NotNull(actor); + Assert.Equal("c", actor.AuthenticationType); + Assert.Equal("a", actor.MachineIP); + Assert.Null(actor.MachineName); + Assert.Null(actor.OnBehalfOf); + Assert.InRange(actor.TimestampUtc, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + Assert.Equal("b", actor.UserName); + } + + [Fact] + public async Task GetAspNetOnBehalfOfAsync_WithContext_ReturnsActor_WithRemoteAddrHeader() + { + var request = new Mock(); + var identity = new Mock(); + var user = new Mock(); + var context = new Mock(); + + request.SetupGet(x => x.ServerVariables) + .Returns(new NameValueCollection() { { "REMOTE_ADDR", "a" } }); + identity.Setup(x => x.Name) + .Returns("b"); + identity.Setup(x => x.AuthenticationType) + .Returns("c"); + user.Setup(x => x.Identity) + .Returns(identity.Object); + context.Setup(x => x.Request) + .Returns(request.Object); + context.Setup(x => x.User) + .Returns(user.Object); + + var actor = await AuditActor.GetAspNetOnBehalfOfAsync(context.Object); + + Assert.NotNull(actor); + Assert.Equal("c", actor.AuthenticationType); + Assert.Equal("a", actor.MachineIP); + Assert.Null(actor.MachineName); + Assert.Null(actor.OnBehalfOf); + Assert.InRange(actor.TimestampUtc, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + Assert.Equal("b", actor.UserName); + } + + [Fact] + public async Task GetAspNetOnBehalfOfAsync_WithContext_ReturnsActor_WithUserHostAddress() + { + var request = new Mock(); + var identity = new Mock(); + var user = new Mock(); + var context = new Mock(); + + request.SetupGet(x => x.ServerVariables) + .Returns(new NameValueCollection()); + request.SetupGet(x => x.UserHostAddress) + .Returns("a"); + identity.Setup(x => x.Name) + .Returns("b"); + identity.Setup(x => x.AuthenticationType) + .Returns("c"); + user.Setup(x => x.Identity) + .Returns(identity.Object); + context.Setup(x => x.Request) + .Returns(request.Object); + context.Setup(x => x.User) + .Returns(user.Object); + + var actor = await AuditActor.GetAspNetOnBehalfOfAsync(context.Object); + + Assert.NotNull(actor); + Assert.Equal("c", actor.AuthenticationType); + Assert.Equal("a", actor.MachineIP); + Assert.Null(actor.MachineName); + Assert.Null(actor.OnBehalfOf); + Assert.InRange(actor.TimestampUtc, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + Assert.Equal("b", actor.UserName); + } + + [Fact] + public async Task GetAspNetOnBehalfOfAsync_WithContext_ObfuscatesLastIpAddressOctet() + { + var request = new Mock(); + var identity = new Mock(); + var user = new Mock(); + var context = new Mock(); + + request.SetupGet(x => x.ServerVariables) + .Returns(new NameValueCollection() { { "HTTP_X_FORWARDED_FOR", "1.2.3.4" } }); + identity.Setup(x => x.Name) + .Returns("b"); + identity.Setup(x => x.AuthenticationType) + .Returns("c"); + user.Setup(x => x.Identity) + .Returns(identity.Object); + context.Setup(x => x.Request) + .Returns(request.Object); + context.Setup(x => x.User) + .Returns(user.Object); + + var actor = await AuditActor.GetAspNetOnBehalfOfAsync(context.Object); + + Assert.NotNull(actor); + Assert.Equal("c", actor.AuthenticationType); + Assert.Equal("1.2.3.0", actor.MachineIP); + Assert.Null(actor.MachineName); + Assert.Null(actor.OnBehalfOf); + Assert.InRange(actor.TimestampUtc, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + Assert.Equal("b", actor.UserName); + } + + [Fact] + public async Task GetAspNetOnBehalfOfAsync_WithContext_SupportsNullUser() + { + var request = new Mock(); + var context = new Mock(); + + request.SetupGet(x => x.ServerVariables) + .Returns(new NameValueCollection() { { "HTTP_X_FORWARDED_FOR", "1.2.3.4" } }); + context.Setup(x => x.Request) + .Returns(request.Object); + context.Setup(x => x.User) + .Returns((IPrincipal)null); + + var actor = await AuditActor.GetAspNetOnBehalfOfAsync(context.Object); + + Assert.NotNull(actor); + Assert.Null(actor.AuthenticationType); + Assert.Equal("1.2.3.0", actor.MachineIP); + Assert.Null(actor.MachineName); + Assert.Null(actor.OnBehalfOf); + Assert.InRange(actor.TimestampUtc, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + Assert.Null(actor.UserName); + } + + [Fact] + public async Task GetCurrentMachineActorAsync_WithoutOnBehalfOf() + { + var actor = await AuditActor.GetCurrentMachineActorAsync(); + var expectedIpAddress = await AuditActor.GetLocalIpAddressAsync(); + + Assert.NotNull(actor); + Assert.Equal(Environment.MachineName, actor.MachineName); + Assert.Equal(expectedIpAddress, actor.MachineIP); + Assert.Equal($@"{Environment.UserDomainName}\{Environment.UserName}", actor.UserName); + Assert.Equal("MachineUser", actor.AuthenticationType); + Assert.InRange(actor.TimestampUtc, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + Assert.Null(actor.OnBehalfOf); + } + + [Fact] + public async Task GetCurrentMachineActorAsync_WithOnBehalfOf_AcceptsNull() + { + var expectedResult = await AuditActor.GetCurrentMachineActorAsync(); + var actualResult = await AuditActor.GetCurrentMachineActorAsync(onBehalfOf: null); + + Assert.NotNull(expectedResult); + Assert.NotNull(actualResult); + Assert.Equal(expectedResult.MachineName, actualResult.MachineName); + Assert.Equal(expectedResult.MachineIP, actualResult.MachineIP); + Assert.Equal(expectedResult.UserName, actualResult.UserName); + Assert.Equal(expectedResult.AuthenticationType, actualResult.AuthenticationType); + Assert.InRange(actualResult.TimestampUtc, expectedResult.TimestampUtc, expectedResult.TimestampUtc.AddMinutes(1)); + Assert.Null(actualResult.OnBehalfOf); + } + + [Fact] + public async Task GetLocalIpAddressAsync_ReturnsAppropriateValueForLocalMachine() + { + string expectedIpAddress = null; + + if (NetworkInterface.GetIsNetworkAvailable()) + { + var entry = await Dns.GetHostEntryAsync(Dns.GetHostName()); + + if (entry != null) + { + expectedIpAddress = + TryGetAddress(entry.AddressList, AddressFamily.InterNetworkV6) ?? + TryGetAddress(entry.AddressList, AddressFamily.InterNetwork); + } + } + + var actualIpAddress = await AuditActor.GetLocalIpAddressAsync(); + + Assert.Equal(expectedIpAddress, actualIpAddress); + } + + private static string TryGetAddress(IEnumerable addresses, AddressFamily family) + { + return addresses.Where(address => address.AddressFamily == family) + .Select(address => address.ToString()) + .FirstOrDefault(); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/AuditEntryTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/AuditEntryTests.cs new file mode 100644 index 0000000000..b167d8a823 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/AuditEntryTests.cs @@ -0,0 +1,37 @@ +// 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 Moq; +using Xunit; + +namespace NuGetGallery.Auditing +{ + public class AuditEntryTests + { + [Fact] + public void Constructor_AcceptsNulls() + { + var entry = new AuditEntry(record: null, actor: null); + + Assert.Null(entry.Record); + Assert.Null(entry.Actor); + } + + [Fact] + public void Constructor_SetsProperties() + { + var record = new Mock(); + var actor = new AuditActor( + machineName: null, + machineIP: null, + userName: null, + authenticationType: null, + timeStampUtc: DateTime.MinValue); + var entry = new AuditEntry(record.Object, actor); + + Assert.Same(record.Object, entry.Record); + Assert.Same(actor, entry.Actor); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/AuditedAuthenticatedOperationActionTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/AuditedAuthenticatedOperationActionTests.cs new file mode 100644 index 0000000000..51cb5a548c --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/AuditedAuthenticatedOperationActionTests.cs @@ -0,0 +1,23 @@ +// 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 Xunit; + +namespace NuGetGallery.Auditing +{ + public class AuditedAuthenticatedOperationActionTests : EnumTests + { + [Fact] + public void Definition_HasNotChanged() + { + var expectedNames = new[] + { + "FailedLoginInvalidPassword", + "FailedLoginNoSuchUser", + "PackagePushAttemptByNonOwner" + }; + + Verify(typeof(AuditedAuthenticatedOperationAction), expectedNames); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageActionTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageActionTests.cs new file mode 100644 index 0000000000..761a87724d --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageActionTests.cs @@ -0,0 +1,27 @@ +// 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 Xunit; + +namespace NuGetGallery.Auditing +{ + public class AuditedPackageActionTests : EnumTests + { + [Fact] + public void Definition_HasNotChanged() + { + var expectedNames = new[] + { + "Create", + "Delete", + "Edit", + "List", + "SoftDelete", + "UndoEdit", + "Unlist" + }; + + Verify(typeof(AuditedPackageAction), expectedNames); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageRegistrationActionTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageRegistrationActionTests.cs new file mode 100644 index 0000000000..46389bb9d8 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/AuditedPackageRegistrationActionTests.cs @@ -0,0 +1,22 @@ +// 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 Xunit; + +namespace NuGetGallery.Auditing +{ + public class AuditedPackageRegistrationActionTests : EnumTests + { + [Fact] + public void Definition_HasNotChanged() + { + var expectedNames = new[] + { + "AddOwner", + "RemoveOwner" + }; + + Verify(typeof(AuditedPackageRegistrationAction), expectedNames); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/AuditedUserActionTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/AuditedUserActionTests.cs new file mode 100644 index 0000000000..a1f214baa0 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/AuditedUserActionTests.cs @@ -0,0 +1,30 @@ +// 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 Xunit; + +namespace NuGetGallery.Auditing +{ + public class AuditedUserActionTests : EnumTests + { + [Fact] + public void Definition_HasNotChanged() + { + var expectedNames = new [] + { + "AddCredential", + "CancelChangeEmail", + "ChangeEmail", + "ConfirmEmail", + "EditCredential", + "ExpireCredential", + "Login", + "Register", + "RemoveCredential", + "RequestPasswordReset" + }; + + Verify(typeof(AuditedUserAction), expectedNames); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/AuditingServiceTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/AuditingServiceTests.cs new file mode 100644 index 0000000000..5e2bbe2e57 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/AuditingServiceTests.cs @@ -0,0 +1,325 @@ +// 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.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NuGetGallery.Auditing.AuditedEntities; +using Xunit; + +namespace NuGetGallery.Auditing +{ + public class AuditingServiceTests + { + [Fact] + public async Task SaveAuditRecordAsync_UserAuditRecord() + { + var user = new User() + { + CreatedUtc = DateTime.Now, + Credentials = new List() + { + new Credential( + CredentialTypes.Password.V3, + value: "a", + expiration: new TimeSpan(days: 1, hours: 2, minutes:3, seconds: 4)) + }, + EmailAddress = "b", + Roles = new List() { new Role() { Key = 5, Name = "c" } }, + UnconfirmedEmailAddress = "d", + Username = "e" + }; + var auditRecord = new UserAuditRecord(user, AuditedUserAction.Login, user.Credentials.First()); + var service = new TestAuditingService(async (string auditData, string resourceType, string filePath, string action, DateTime timestamp) => + { + Assert.Equal("User", resourceType); + Assert.Equal("e", filePath); + Assert.Equal("login", action); + Assert.InRange(timestamp, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + + var jObject = JObject.Parse(auditData); + + var record = jObject["Record"]; + + Assert.Equal("e", record["Username"].Value()); + Assert.Equal("b", record["EmailAddress"].Value()); + Assert.Equal("d", record["UnconfirmedEmailAddress"].Value()); + Assert.Equal("c", record["Roles"].ToObject>().Single()); + + var credentials = record["Credentials"]; + var credential = credentials.AsEnumerable().Single(); + + Assert.Equal(0, credential["Key"].Value()); + Assert.Equal(CredentialTypes.Password.V3, credential["Type"].Value()); + Assert.Equal(JTokenType.Null, credential["Value"].Type); + Assert.Equal(JTokenType.Null, credential["Description"].Type); + Assert.False(credential["Scopes"].ToObject>().Any()); + Assert.Equal(JTokenType.Null, credential["Identity"].Type); + Assert.Equal(DateTime.MinValue, credential["Created"].Value()); + Assert.Equal(user.Credentials.First().Expires.Value, credential["Expires"].Value()); + Assert.Equal(JTokenType.Null, credential["LastUsed"].Type); + + var affectedCredential = record["AffectedCredential"].AsJEnumerable().Single(); + + Assert.Equal(0, affectedCredential["Key"].Value()); + Assert.Equal(CredentialTypes.Password.V3, affectedCredential["Type"].Value()); + Assert.Equal(JTokenType.Null, affectedCredential["Value"].Type); + Assert.Equal(JTokenType.Null, affectedCredential["Description"].Type); + Assert.Equal(0, affectedCredential["Scopes"].AsJEnumerable().Count()); + Assert.Equal(JTokenType.Null, affectedCredential["Identity"].Type); + Assert.Equal(DateTime.MinValue, affectedCredential["Created"].Value()); + Assert.Equal(user.Credentials.First().Expires.Value, affectedCredential["Expires"].Value()); + Assert.Equal(JTokenType.Null, affectedCredential["LastUsed"].Type); + + Assert.Equal(JTokenType.Null, record["AffectedEmailAddress"].Type); + Assert.Equal("Login", record["Action"].Value()); + + await VerifyActor(jObject); + + return null; + }); + + await service.SaveAuditRecordAsync(auditRecord); + } + + [Fact] + public async Task SaveAuditRecordAsync_PackageAuditRecord() + { + var package = new Package() + { + Copyright = "a", + Created = DateTime.Now, + Deleted = true, + Description = "b", + DownloadCount = 1, +#pragma warning disable 612 + ExternalPackageUrl = "c", +#pragma warning restore 612 + FlattenedAuthors = "d", + FlattenedDependencies = "e", + Hash = "f", + HashAlgorithm = "g", + HideLicenseReport = true, + IconUrl = "h", + IsLatest = true, + IsLatestStable = true, + IsPrerelease = true, + Key = 2, + Language = "i", + LastEdited = DateTime.Now.AddMinutes(1), + LastUpdated = DateTime.Now.AddMinutes(2), + LicenseNames = "j", + LicenseReportUrl = "k", + LicenseUrl = "l", + Listed = true, + MinClientVersion = "m", + NormalizedVersion = "n", + PackageFileSize = 3, + PackageRegistration = new PackageRegistration() { Id = "o" }, + PackageRegistrationKey = 4, + ProjectUrl = "p", + Published = DateTime.Now.AddMinutes(3), + ReleaseNotes = "q", + RequiresLicenseAcceptance = true, + Summary = "r", + Tags = "s", + Title = "t", + UserKey = 5, + Version = "u" + }; + var auditRecord = new PackageAuditRecord(package, AuditedPackageAction.Create, reason: "v"); + var service = new TestAuditingService(async (string auditData, string resourceType, string filePath, string action, DateTime timestamp) => + { + Assert.Equal("Package", resourceType); + Assert.Equal("o/u", filePath); + Assert.Equal("create", action); + Assert.InRange(timestamp, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + + var jObject = JObject.Parse(auditData); + + var record = jObject["Record"]; + + Assert.Equal("o", record["Id"].Value()); + Assert.Equal("u", record["Version"].Value()); + Assert.Equal("f", record["Hash"].Value()); + + var packageRecord = record["PackageRecord"]; + + Assert.Equal(4, packageRecord["PackageRegistrationKey"].Value()); + Assert.Equal("a", packageRecord["Copyright"].Value()); + Assert.Equal(package.Created.ToUniversalTime(), packageRecord["Created"].Value()); + Assert.Equal("b", packageRecord["Description"].Value()); + Assert.Equal("q", packageRecord["ReleaseNotes"].Value()); + Assert.Equal(1, packageRecord["DownloadCount"].Value()); + Assert.Equal(JTokenType.Null, packageRecord["ExternalPackageUrl"].Type); + Assert.Equal("g", packageRecord["HashAlgorithm"].Value()); + Assert.Equal("f", packageRecord["Hash"].Value()); + Assert.Equal("h", packageRecord["IconUrl"].Value()); + Assert.True(packageRecord["IsLatest"].Value()); + Assert.True(packageRecord["IsLatestStable"].Value()); + Assert.Equal(package.LastUpdated.ToUniversalTime(), packageRecord["LastUpdated"].Value()); + Assert.Equal(package.LastEdited.Value.ToUniversalTime(), packageRecord["LastEdited"].Value()); + Assert.Equal("l", packageRecord["LicenseUrl"].Value()); + Assert.True(packageRecord["HideLicenseReport"].Value()); + Assert.Equal("i", packageRecord["Language"].Value()); + Assert.Equal(package.Published.ToUniversalTime(), packageRecord["Published"].Value()); + Assert.Equal(3, packageRecord["PackageFileSize"].Value()); + Assert.Equal("p", packageRecord["ProjectUrl"].Value()); + Assert.True(packageRecord["RequiresLicenseAcceptance"].Value()); + Assert.Equal("r", packageRecord["Summary"].Value()); + Assert.Equal("s", packageRecord["Tags"].Value()); + Assert.Equal("t", packageRecord["Title"].Value()); + Assert.Equal("u", packageRecord["Version"].Value()); + Assert.Equal("n", packageRecord["NormalizedVersion"].Value()); + Assert.Equal("j", packageRecord["LicenseNames"].Value()); + Assert.Equal("k", packageRecord["LicenseReportUrl"].Value()); + Assert.True(packageRecord["Listed"].Value()); + Assert.True(packageRecord["IsPrerelease"].Value()); + Assert.Equal("d", packageRecord["FlattenedAuthors"].Value()); + Assert.Equal("e", packageRecord["FlattenedDependencies"].Value()); + Assert.Equal(2, packageRecord["Key"].Value()); + Assert.Equal("m", packageRecord["MinClientVersion"].Value()); + Assert.Equal(5, packageRecord["UserKey"].Value()); + Assert.True(packageRecord["Deleted"].Value()); + + var registrationRecord = record["RegistrationRecord"]; + + Assert.Equal("o", registrationRecord["Id"].Value()); + Assert.Equal(0, registrationRecord["DownloadCount"].Value()); + Assert.Equal(0, registrationRecord["Key"].Value()); + + Assert.Equal("v", record["Reason"].Value()); + Assert.Equal("Create", record["Action"].Value()); + + await VerifyActor(jObject); + + return null; + }); + + await service.SaveAuditRecordAsync(auditRecord); + } + + [Fact] + public async Task SaveAuditRecordAsync_PackageRegistrationAuditRecord() + { + var packageRegistration = new PackageRegistration() + { + DownloadCount = 1, + Id = "a", + Key = 2 + }; + var auditRecord = new PackageRegistrationAuditRecord(packageRegistration, AuditedPackageRegistrationAction.AddOwner, owner: "b"); + var service = new TestAuditingService(async (string auditData, string resourceType, string filePath, string action, DateTime timestamp) => + { + Assert.Equal("PackageRegistration", resourceType); + Assert.Equal("a", filePath); + Assert.Equal("addowner", action); + Assert.InRange(timestamp, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + + var jObject = JObject.Parse(auditData); + + var record = jObject["Record"]; + + Assert.Equal("a", record["Id"].Value()); + + var registrationRecord = record["RegistrationRecord"]; + + Assert.Equal("a", registrationRecord["Id"].Value()); + Assert.Equal(1, registrationRecord["DownloadCount"].Value()); + Assert.Equal(2, registrationRecord["Key"].Value()); + + Assert.Equal("b", record["Owner"].Value()); + Assert.Equal("AddOwner", record["Action"].Value()); + + await VerifyActor(jObject); + + return null; + }); + + await service.SaveAuditRecordAsync(auditRecord); + } + + [Fact] + public async Task SaveAuditRecordAsync_FailedAuthenticatedOperationAuditRecord() + { + var expiresIn = new TimeSpan(days: 1, hours: 2, minutes: 3, seconds: 4); + var auditRecord = new FailedAuthenticatedOperationAuditRecord( + usernameOrEmail: "a", + action: AuditedAuthenticatedOperationAction.PackagePushAttemptByNonOwner, + attemptedPackage: new AuditedPackageIdentifier("b", "c"), + attemptedCredential: new Credential(CredentialTypes.ApiKey.V2, value: "d", expiration: expiresIn)); + var service = new TestAuditingService(async (string auditData, string resourceType, string filePath, string action, DateTime timestamp) => + { + Assert.Equal("FailedAuthenticatedOperation", resourceType); + Assert.Equal("all", filePath); + Assert.Equal("packagepushattemptbynonowner", action); + Assert.InRange(timestamp, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + + var jObject = JObject.Parse(auditData); + + var record = jObject["Record"]; + + Assert.Equal("a", record["UsernameOrEmail"].Value()); + + var attemptedPackage = record["AttemptedPackage"]; + + Assert.Equal("b", attemptedPackage["Id"].Value()); + Assert.Equal("c", attemptedPackage["Version"].Value()); + + var attemptedCredential = record["AttemptedCredential"]; + + Assert.Equal(0, attemptedCredential["Key"].Value()); + Assert.Equal(CredentialTypes.ApiKey.V2, attemptedCredential["Type"].Value()); + + Assert.Equal(JTokenType.Null, attemptedCredential["Value"].Type); + Assert.Equal(JTokenType.Null, attemptedCredential["Description"].Type); + Assert.False(attemptedCredential["Scopes"].ToObject>().Any()); + Assert.Equal(JTokenType.Null, attemptedCredential["Identity"].Type); + Assert.Equal(DateTime.MinValue, attemptedCredential["Created"].Value()); + + var expiresUtc = DateTime.UtcNow.Add(expiresIn); + + Assert.InRange(attemptedCredential["Expires"].Value(), expiresUtc.AddMinutes(-1), expiresUtc.AddMinutes(1)); + Assert.Equal(JTokenType.Null, attemptedCredential["LastUsed"].Type); + + await VerifyActor(jObject); + + return null; + }); + + await service.SaveAuditRecordAsync(auditRecord); + } + + private static async Task VerifyActor(JObject jObject) + { + var actor = jObject["Actor"]; + + Assert.Equal(Environment.MachineName, actor["MachineName"].Value()); + + var expectedIpAddress = await AuditActor.GetLocalIpAddressAsync(); + + Assert.Equal(expectedIpAddress, actor["MachineIP"].Value()); + Assert.Equal($@"{Environment.UserDomainName}\{Environment.UserName}", actor["UserName"].Value()); + Assert.Equal("MachineUser", actor["AuthenticationType"].Value()); + Assert.InRange(actor["TimestampUtc"].Value(), DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + Assert.Equal(JTokenType.Null, actor["OnBehalfOf"].Type); + } + + private class TestAuditingService : AuditingService + { + private readonly Func> _saveDelegate; + + internal TestAuditingService(Func> saveDelegate) + { + _saveDelegate = saveDelegate; + } + + protected override Task SaveAuditRecordAsync(string auditData, string resourceType, string filePath, string action, DateTime timestamp) + { + return _saveDelegate(auditData, resourceType, filePath, action, timestamp); + } + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/CredentialAuditRecordTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/CredentialAuditRecordTests.cs new file mode 100644 index 0000000000..fe79bf4bb5 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/CredentialAuditRecordTests.cs @@ -0,0 +1,96 @@ +// 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 Xunit; + +namespace NuGetGallery.Auditing +{ + public class CredentialAuditRecordTests + { + [Fact] + public void Constructor_ThrowsForNullCredential() + { + Assert.Throws(() => new CredentialAuditRecord(credential: null, removed: true)); + } + + [Fact] + public void Constructor_ThrowsForRemovalWithNullType() + { + var credential = new Credential(); + + Assert.Throws(() => new CredentialAuditRecord(credential, removed: true)); + } + + [Fact] + public void Constructor_RemovalOfNonPasswordSetsValue() + { + var credential = new Credential(type: "a", value: "b"); + var record = new CredentialAuditRecord(credential, removed: true); + + Assert.Equal("b", record.Value); + } + + [Fact] + public void Constructor_RemovalOfPasswordDoesNotSetValue() + { + var credential = new Credential(type: CredentialTypes.Password.V3, value: "a"); + var record = new CredentialAuditRecord(credential, removed: true); + + Assert.Null(record.Value); + } + + [Fact] + public void Constructor_NonRemovalOfNonPasswordDoesNotSetsValue() + { + var credential = new Credential(type: "a", value: "b"); + var record = new CredentialAuditRecord(credential, removed: false); + + Assert.Null(record.Value); + } + + [Fact] + public void Constructor_NonRemovalOfPasswordDoesNotSetValue() + { + var credential = new Credential(type: CredentialTypes.Password.V3, value: "a"); + var record = new CredentialAuditRecord(credential, removed: false); + + Assert.Null(record.Value); + } + + [Fact] + public void Constructor_SetsProperties() + { + var created = DateTime.MinValue; + var expires = DateTime.MinValue.AddDays(1); + var lastUsed = DateTime.MinValue.AddDays(2); + var credential = new Credential() + { + Created = created, + Description = "a", + Expires = expires, + Identity = "b", + Key = 1, + LastUsed = lastUsed, + Scopes = new List() { new Scope(subject: "c", allowedAction: "d") }, + Type = "e", + Value = "f" + }; + var record = new CredentialAuditRecord(credential, removed: true); + + Assert.Equal(created, record.Created); + Assert.Equal("a", record.Description); + Assert.Equal(expires, record.Expires); + Assert.Equal("b", record.Identity); + Assert.Equal(1, record.Key); + Assert.Equal(lastUsed, record.LastUsed); + Assert.Equal(1, record.Scopes.Count); + var scope = record.Scopes[0]; + Assert.Equal("c", scope.Subject); + Assert.Equal("d", scope.AllowedAction); + Assert.Equal("e", record.Type); + Assert.Equal("f", record.Value); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/EnumTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/EnumTests.cs new file mode 100644 index 0000000000..82b9bb8047 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/EnumTests.cs @@ -0,0 +1,27 @@ +// 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.Linq; +using Xunit; + +namespace NuGetGallery.Auditing +{ + public abstract class EnumTests + { + protected void Verify(Type enumType, string[] expectedNames) + { + var actualNames = Enum.GetNames(enumType); + + Assert.Equal(expectedNames.Length, actualNames.Length); + + var actualNotInExpected = actualNames.Except(expectedNames); + var expectedNotInActual = expectedNames.Except(actualNames); + + var commonMessage = $"The {enumType.Name} enum definition has changed. Please evaluate this change against all {nameof(AuditingService)} implementations."; + + Assert.False(actualNotInExpected.Any(), $"{commonMessage} Unexpected members found: {string.Join(", ", actualNotInExpected)}"); + Assert.False(expectedNotInActual.Any(), $"{commonMessage} Expected members not found: {string.Join(", ", expectedNotInActual)}"); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/FailedAuthenticatedOperationAuditRecordTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/FailedAuthenticatedOperationAuditRecordTests.cs new file mode 100644 index 0000000000..f23c23435f --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/FailedAuthenticatedOperationAuditRecordTests.cs @@ -0,0 +1,70 @@ +// 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 NuGetGallery.Auditing.AuditedEntities; +using Xunit; + +namespace NuGetGallery.Auditing +{ + public class FailedAuthenticatedOperationAuditRecordTests + { + [Fact] + public void Constructor_AcceptsNulls() + { + var record = new FailedAuthenticatedOperationAuditRecord( + usernameOrEmail: null, + action: AuditedAuthenticatedOperationAction.FailedLoginNoSuchUser, + attemptedPackage: null, + attemptedCredential: null); + + Assert.Null(record.UsernameOrEmail); + Assert.Equal(AuditedAuthenticatedOperationAction.FailedLoginNoSuchUser, record.Action); + Assert.Null(record.AttemptedPackage); + Assert.Null(record.AttemptedCredential); + } + + [Fact] + public void Constructor_AcceptsEmptyStringUserNameOrEmail() + { + var record = new FailedAuthenticatedOperationAuditRecord( + usernameOrEmail: "", + action: AuditedAuthenticatedOperationAction.FailedLoginInvalidPassword, + attemptedPackage: null, + attemptedCredential: null); + + Assert.Equal("", record.UsernameOrEmail); + } + + [Fact] + public void Constructor_SetsProperties() + { + var identifier = new AuditedPackageIdentifier(id: "a", version: "1.0.0"); + var credential = new Credential(type: CredentialTypes.Password.V3, value: "b"); + var record = new FailedAuthenticatedOperationAuditRecord( + usernameOrEmail: "c", + action: AuditedAuthenticatedOperationAction.PackagePushAttemptByNonOwner, + attemptedPackage: identifier, + attemptedCredential: credential); + + Assert.Equal("c", record.UsernameOrEmail); + Assert.Same(identifier, record.AttemptedPackage); + Assert.NotNull(record.AttemptedCredential); + Assert.Equal(credential.Type, record.AttemptedCredential.Type); + Assert.Null(record.AttemptedCredential.Value); + Assert.Equal(AuditedAuthenticatedOperationAction.PackagePushAttemptByNonOwner, record.Action); + } + + [Fact] + public void GetPath() + { + var record = new FailedAuthenticatedOperationAuditRecord( + usernameOrEmail: null, + action: AuditedAuthenticatedOperationAction.FailedLoginNoSuchUser, + attemptedPackage: null, + attemptedCredential: null); + var actualResult = record.GetPath(); + + Assert.Equal("all", actualResult); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/FileSystemAuditingServiceTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/FileSystemAuditingServiceTests.cs new file mode 100644 index 0000000000..de2453c526 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/FileSystemAuditingServiceTests.cs @@ -0,0 +1,161 @@ +// 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.IO; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NuGetGallery.Utilities; +using Xunit; + +namespace NuGetGallery.Auditing +{ + public class FileSystemAuditingServiceTests + { + [Theory] + [InlineData(null)] + [InlineData("")] + public void Constructor_ThrowsForNullOrEmptyAuditingPath(string auditingPath) + { + Assert.Throws(() => + new FileSystemAuditingService(auditingPath, GetOnBehalfOf)); + } + + [Fact] + public void Constructor_ThrowsForNullGetOnBehalfOf() + { + Assert.Throws(() => + new FileSystemAuditingService(auditingPath: "a", getOnBehalfOf: null)); + } + + [Fact] + public async Task SaveAuditRecord_ThrowsForNull() + { + var service = new FileSystemAuditingService( + auditingPath: "a", + getOnBehalfOf: AuditActor.GetAspNetOnBehalfOfAsync); + + await Assert.ThrowsAsync(async () => + await service.SaveAuditRecordAsync(record: null)); + } + + [Fact] + public async Task SaveAuditRecord_ReturnsUriForAuditFile() + { + using (var testDirectory = TestDirectory.Create()) + { + var service = new FileSystemAuditingService( + auditingPath: testDirectory.FullPath, + getOnBehalfOf: GetOnBehalfOf); + var record = new PackageAuditRecord( + new Package() + { + Hash = "a", + PackageRegistration = new PackageRegistration() { Id = "b" }, + Version = "1.0.0" + }, + AuditedPackageAction.Create); + + await service.SaveAuditRecordAsync(record); + + var files = Directory.GetFiles(testDirectory.FullPath, "*", SearchOption.AllDirectories); + var actualFilePath = files.Single(); + var expectedFilePathPattern = new Regex(@"package\\b\\1.0.0\\[0-9a-f]{32}-create.audit.v1.json$"); + + Assert.True(expectedFilePathPattern.IsMatch(actualFilePath)); + } + } + + [Fact] + public async Task SaveAuditRecord_CreatesAuditFile() + { + using (var testDirectory = TestDirectory.Create()) + { + var service = new FileSystemAuditingService( + auditingPath: testDirectory.FullPath, + getOnBehalfOf: GetOnBehalfOf); + var record = new PackageAuditRecord( + new Package() + { + Hash = "a", + PackageRegistration = new PackageRegistration() { Id = "b" }, + Version = "1.0.0" + }, + AuditedPackageAction.Create); + + await service.SaveAuditRecordAsync(record); + + var files = Directory.GetFiles(testDirectory.FullPath, "*", SearchOption.AllDirectories); + var json = JObject.Parse(File.ReadAllText(files.Single())); + + Assert.NotNull(json["Record"]); + Assert.NotNull(json["Record"]["Id"]); + Assert.Equal("b", json["Record"]["Id"].Value()); + Assert.NotNull(json["Record"]["Version"]); + Assert.Equal("1.0.0", json["Record"]["Version"].Value()); + Assert.NotNull(json["Record"]["Hash"]); + Assert.Equal("a", json["Record"]["Hash"].Value()); + Assert.NotNull(json["Record"]["PackageRecord"]); + Assert.NotNull(json["Record"]["PackageRecord"]["Hash"]); + Assert.Equal("a", json["Record"]["PackageRecord"]["Hash"].Value()); + Assert.NotNull(json["Record"]["PackageRecord"]["Version"]); + Assert.Equal("1.0.0", json["Record"]["PackageRecord"]["Version"].Value()); + Assert.NotNull(json["Record"]["RegistrationRecord"]); + Assert.NotNull(json["Record"]["RegistrationRecord"]["Id"]); + Assert.Equal("b", json["Record"]["RegistrationRecord"]["Id"].Value()); + Assert.NotNull(json["Record"]["Action"]); + Assert.Equal("Create", json["Record"]["Action"].Value()); + Assert.NotNull(json["Actor"]); + Assert.NotNull(json["Actor"]["MachineName"]); + Assert.Equal(Environment.MachineName, json["Actor"]["MachineName"].Value()); + Assert.NotNull(json["Actor"]["MachineIP"]); + Assert.True(IsValidMachineIpValue(json["Actor"]["MachineIP"].Value())); + Assert.NotNull(json["Actor"]["UserName"]); + Assert.Equal($@"{Environment.UserDomainName}\{Environment.UserName}", json["Actor"]["UserName"].Value()); + Assert.NotNull(json["Actor"]["AuthenticationType"]); + Assert.Equal("MachineUser", json["Actor"]["AuthenticationType"].Value()); + Assert.NotNull(json["Actor"]["TimestampUtc"]); + Assert.InRange(DateTime.Parse(json["Actor"]["TimestampUtc"].Value()), DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1)); + Assert.NotNull(json["Actor"]["OnBehalfOf"]); + Assert.NotNull(json["Actor"]["OnBehalfOf"]["MachineName"]); + Assert.Equal("a", json["Actor"]["OnBehalfOf"]["MachineName"].Value()); + Assert.NotNull(json["Actor"]["OnBehalfOf"]["MachineIP"]); + Assert.Equal("b", json["Actor"]["OnBehalfOf"]["MachineIP"].Value()); + Assert.NotNull(json["Actor"]["OnBehalfOf"]["UserName"]); + Assert.Equal("c", json["Actor"]["OnBehalfOf"]["UserName"].Value()); + Assert.NotNull(json["Actor"]["OnBehalfOf"]["AuthenticationType"]); + Assert.Equal("d", json["Actor"]["OnBehalfOf"]["AuthenticationType"].Value()); + Assert.NotNull(json["Actor"]["OnBehalfOf"]["TimestampUtc"]); + Assert.Equal(DateTime.MinValue, DateTime.Parse(json["Actor"]["OnBehalfOf"]["TimestampUtc"].Value())); + Assert.Equal(JTokenType.Null, json["Actor"]["OnBehalfOf"]["OnBehalfOf"].Type); + } + } + + private static Task GetOnBehalfOf() + { + var actor = new AuditActor( + machineName: "a", + machineIP: "b", + userName: "c", + authenticationType: "d", + timeStampUtc: DateTime.MinValue); + + return Task.FromResult(actor); + } + + private static bool IsValidMachineIpValue(string ipAddress) + { + if (ipAddress == null) + { + return true; + } + + IPAddress value; + + return IPAddress.TryParse(ipAddress, out value); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/PackageAuditRecordTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/PackageAuditRecordTests.cs new file mode 100644 index 0000000000..4f3f907591 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/PackageAuditRecordTests.cs @@ -0,0 +1,53 @@ +// 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 Xunit; + +namespace NuGetGallery.Auditing +{ + public class PackageAuditRecordTests + { + [Fact] + public void Constructor_SetsProperties() + { + var record = new PackageAuditRecord( + new Package() + { + Hash = "a", + PackageRegistration = new PackageRegistration() { Id = "b" }, + Version = "1.0.0" + }, + AuditedPackageAction.Create, + reason: "c"); + + Assert.Equal("b", record.Id); + Assert.Equal("1.0.0", record.Version); + Assert.Equal("a", record.Hash); + Assert.NotNull(record.PackageRecord); + Assert.Equal("a", record.PackageRecord.Hash); + Assert.Equal("1.0.0", record.PackageRecord.Version); + Assert.NotNull(record.RegistrationRecord); + Assert.Equal("b", record.RegistrationRecord.Id); + Assert.Equal("c", record.Reason); + Assert.Equal(AuditedPackageAction.Create, record.Action); + } + + [Fact] + public void GetPath_ReturnsNormalizedPackageIdAndVersion() + { + var record = new PackageAuditRecord( + new Package() + { + Hash = "a", + PackageRegistration = new PackageRegistration() { Id = "B" }, + Version = "1.0.0+c" + }, + AuditedPackageAction.Create, + reason: "d"); + + var actualResult = record.GetPath(); + + Assert.Equal("b/1.0.0", actualResult); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/PackageRegistrationAuditRecordTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/PackageRegistrationAuditRecordTests.cs new file mode 100644 index 0000000000..d476133534 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/PackageRegistrationAuditRecordTests.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 Xunit; + +namespace NuGetGallery.Auditing +{ + public class PackageRegistrationAuditRecordTests + { + [Fact] + public void Constructor_SetsProperties() + { + var record = new PackageRegistrationAuditRecord( + new PackageRegistration() { Id = "a" }, + AuditedPackageRegistrationAction.AddOwner, + owner: "b"); + + Assert.Equal("a", record.Id); + Assert.NotNull(record.RegistrationRecord); + Assert.Equal("a", record.RegistrationRecord.Id); + Assert.Equal("b", record.Owner); + Assert.Equal(AuditedPackageRegistrationAction.AddOwner, record.Action); + } + + [Fact] + public void GetPath_ReturnsLowerCasedId() + { + var record = new PackageRegistrationAuditRecord( + new PackageRegistration() { Id = "A" }, + AuditedPackageRegistrationAction.AddOwner, + owner: "b"); + + var actualPath = record.GetPath(); + + Assert.Equal("a", actualPath); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/ScopeAuditRecordTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/ScopeAuditRecordTests.cs new file mode 100644 index 0000000000..b8bc977220 --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/ScopeAuditRecordTests.cs @@ -0,0 +1,22 @@ +// 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 Xunit; + +namespace NuGetGallery.Auditing +{ + public class ScopeAuditRecordTests + { + [Theory] + [InlineData(null, null)] + [InlineData("", "")] + [InlineData("a", "b")] + public void Constructor_SetsProperties(string subject, string allowedAction) + { + var entry = new ScopeAuditRecord(subject, allowedAction); + + Assert.Equal(subject, entry.Subject); + Assert.Equal(allowedAction, entry.AllowedAction); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/Auditing/UserAuditRecordTests.cs b/tests/NuGetGallery.Core.Facts/Auditing/UserAuditRecordTests.cs new file mode 100644 index 0000000000..fcf04ce7ea --- /dev/null +++ b/tests/NuGetGallery.Core.Facts/Auditing/UserAuditRecordTests.cs @@ -0,0 +1,65 @@ +// 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 Xunit; + +namespace NuGetGallery.Auditing +{ + public class UserAuditRecordTests + { + [Fact] + public void Constructor_WithoutAffected_ThrowsForNullUser() + { + Assert.Throws(() => new UserAuditRecord(user: null, action: AuditedUserAction.Login)); + } + + [Fact] + public void Constructor_WithAffected_SetsProperties() + { + var user = new User() + { + Username = "a", + EmailAddress = "b", + UnconfirmedEmailAddress = "c", + Roles = new List() { new Role() { Name = "d" } }, + Credentials = new List() + { + new Credential(type: CredentialTypes.Password.V3, value: "e"), + new Credential(type: "f", value: "g") + } + }; + + var record = new UserAuditRecord(user, AuditedUserAction.Login, new Credential(type: "h", value: "i")); + + Assert.Equal("a", record.Username); + Assert.Equal("b", record.EmailAddress); + Assert.Equal("c", record.UnconfirmedEmailAddress); + Assert.Equal(1, record.Roles.Length); + Assert.Equal("d", record.Roles[0]); + Assert.Equal(1, record.Credentials.Length); + Assert.Equal(CredentialTypes.Password.V3, record.Credentials[0].Type); + Assert.Null(record.Credentials[0].Value); + Assert.Equal(1, record.AffectedCredential.Length); + Assert.Equal("h", record.AffectedCredential[0].Type); + Assert.Null(record.AffectedCredential[0].Value); + } + + [Fact] + public void GetPath_ReturnsLowerCasedUserName() + { + var user = new User() + { + Username = "A", + Roles = new List(), + Credentials = new List() + }; + + var record = new UserAuditRecord(user, AuditedUserAction.Login); + var actualPath = record.GetPath(); + + Assert.Equal("a", actualPath); + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj b/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj index 5dd34859cd..ba13e0aa91 100644 --- a/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj +++ b/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj @@ -2,7 +2,6 @@ - Debug AnyCPU @@ -16,7 +15,6 @@ v4.6.1 512 - true @@ -30,7 +28,7 @@ 4 true false - ManagedMinimumRules.ruleset + Sdl7.0.ruleset pdbonly @@ -42,76 +40,57 @@ false + + ..\..\packages\Castle.Core.4.0.0\lib\net45\Castle.Core.dll + - False - ..\..\packages\entityframework.6.1.3\lib\net45\EntityFramework.dll - True + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll - False - ..\..\packages\entityframework.6.1.3\lib\net45\EntityFramework.SqlServer.dll - True + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll - False ..\..\packages\Microsoft.Data.Edm.5.6.5-beta\lib\net40\Microsoft.Data.Edm.dll - True - - ..\..\packages\Microsoft.Data.Services.Client.5.6.5-beta\lib\net40\Microsoft.Data.Services.Client.dll - True - False + ..\..\packages\Microsoft.Data.OData.5.6.5-beta\lib\net40\Microsoft.Data.OData.dll - True - False ..\..\packages\Microsoft.Web.Xdt.2.1.1\lib\net40\Microsoft.Web.XmlTransform.dll - True - False ..\..\packages\Microsoft.WindowsAzure.ConfigurationManager.3.1.0\lib\net40\Microsoft.WindowsAzure.Configuration.dll - True False ..\..\packages\WindowsAzure.Storage.2.1.0.3\lib\net40\Microsoft.WindowsAzure.Storage.dll - - False - ..\..\packages\NuGet.Common.3.5.0-beta-final\lib\net45\NuGet.Common.dll - True + + ..\..\packages\Moq.4.7.0\lib\net45\Moq.dll - - False - ..\..\packages\NuGet.Frameworks.3.5.0-beta-final\lib\net45\NuGet.Frameworks.dll - True + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + + + ..\..\packages\NuGet.Common.4.0.0\lib\net45\NuGet.Common.dll + + + ..\..\packages\NuGet.Frameworks.4.0.0\lib\net45\NuGet.Frameworks.dll - False ..\..\packages\NuGet.Logging.3.5.0-beta-1160\lib\net45\NuGet.Logging.dll - True - - False - ..\..\packages\NuGet.Packaging.3.5.0-beta-final\lib\net45\NuGet.Packaging.dll - True + + ..\..\packages\NuGet.Packaging.4.0.0\lib\net45\NuGet.Packaging.dll - - False - ..\..\packages\NuGet.Packaging.Core.3.5.0-beta-final\lib\net45\NuGet.Packaging.Core.dll - True + + ..\..\packages\NuGet.Packaging.Core.4.0.0\lib\net45\NuGet.Packaging.Core.dll - - False - ..\..\packages\NuGet.Packaging.Core.Types.3.5.0-beta-final\lib\net45\NuGet.Packaging.Core.Types.dll - True + + ..\..\packages\NuGet.Packaging.Core.Types.4.0.0\lib\net45\NuGet.Packaging.Core.Types.dll - - False - ..\..\packages\NuGet.Versioning.3.5.0-beta-final\lib\net45\NuGet.Versioning.dll - True + + ..\..\packages\NuGet.Versioning.4.0.0\lib\net45\NuGet.Versioning.dll @@ -122,31 +101,39 @@ - False ..\..\packages\System.Spatial.5.6.5-beta\lib\net40\System.Spatial.dll - True - False ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll - True - False ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll - True - False ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll - True + + + + + + + + + + + + + + + + @@ -154,6 +141,7 @@ + @@ -169,19 +157,15 @@ - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/App_Start/CachingSecretReaderFacts.cs b/tests/NuGetGallery.Facts/App_Start/CachingSecretReaderFacts.cs new file mode 100644 index 0000000000..b7311f959f --- /dev/null +++ b/tests/NuGetGallery.Facts/App_Start/CachingSecretReaderFacts.cs @@ -0,0 +1,34 @@ +// 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.Threading.Tasks; +using Moq; +using NuGet.Services.KeyVault; +using NuGetGallery.Configuration.SecretReader; +using NuGetGallery.Diagnostics; +using Xunit; + +namespace NuGetGallery.App_Start +{ + public class CachingSecretReaderFacts + { + [Fact] + public async Task WhenGetSecretIsCalledCacheIsUsed() + { + // Arrange + const string secret = "secret"; + var mockSecretReader = new Mock(); + mockSecretReader.Setup(x => x.GetSecretAsync(It.IsAny())).Returns(Task.FromResult(secret)); + + var cachingSecretReader = new CachingSecretReader(mockSecretReader.Object, new DiagnosticsService()); + + // Act + string value = await cachingSecretReader.GetSecretAsync("secretname"); + value = await cachingSecretReader.GetSecretAsync("secretname"); + + // Assert + mockSecretReader.Verify(x => x.GetSecretAsync(It.IsAny()), Times.Once); + Assert.Equal(secret, value); + } + } +} diff --git a/tests/NuGetGallery.Facts/App_Start/ConfigurationServiceFacts.cs b/tests/NuGetGallery.Facts/App_Start/ConfigurationServiceFacts.cs index 0bef96961b..a55b15d70b 100644 --- a/tests/NuGetGallery.Facts/App_Start/ConfigurationServiceFacts.cs +++ b/tests/NuGetGallery.Facts/App_Start/ConfigurationServiceFacts.cs @@ -1,39 +1,52 @@ // 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.Configuration; +using System.Threading.Tasks; using System.Web; using Moq; +using NuGet.Services.KeyVault; using NuGetGallery.Configuration; +using NuGetGallery.Configuration.SecretReader; using Xunit; namespace NuGetGallery.App_Start { public class ConfigurationServiceFacts { - public class TestableConfigurationService : ConfigurationService + public class TheGetSiteRootMethod { - public TestableConfigurationService() + private class TestableConfigurationService : ConfigurationService { - StubRequest = new Mock(); - StubConfiguredSiteRoot = "http://aSiteRoot/"; - Current = (StubConfiguration = new Mock()).Object; - StubConfiguration.Setup(c => c.SiteRoot).Returns(() => StubConfiguredSiteRoot); + public TestableConfigurationService() : base(new EmptySecretReaderFactory()) + { + StubConfiguredSiteRoot = "http://aSiteRoot/"; - StubRequest.Setup(stub => stub.IsLocal).Returns(false); - } + StubRequest = new Mock(); + StubRequest.Setup(stub => stub.IsLocal).Returns(false); + } - public Mock StubConfiguration { get; set; } - public string StubConfiguredSiteRoot { get; set; } - public Mock StubRequest { get; set; } + public string StubConfiguredSiteRoot { get; set; } + public Mock StubRequest { get; set; } - protected override HttpRequestBase GetCurrentRequest() - { - return StubRequest.Object; + protected override string GetAppSetting(string settingName) + { + var tempAppConfig = new AppConfiguration(); + + if (settingName == $"{SettingPrefix}{nameof(tempAppConfig.SiteRoot)}") + { + return StubConfiguredSiteRoot; + } + + return string.Empty; + } + + protected override HttpRequestBase GetCurrentRequest() + { + return StubRequest.Object; + } } - } - public class TheGetSiteRootMethod - { [Fact] public void WillGetTheConfiguredHttpSiteRoot() { @@ -99,5 +112,93 @@ public void WillCacheTheSiteRootLookup() configuration.StubRequest.Verify(stub => stub.IsLocal, Times.Once()); } } + + public class TheReadSettingMethod + { + private class TestableConfigurationService : ConfigurationService + { + public TestableConfigurationService(ISecretReaderFactory secretReaderFactory = null) + : base(secretReaderFactory ?? new EmptySecretReaderFactory()) + { + } + + public string ConnectionStringStub { get; set; } + + public string CloudSettingStub { get; set; } + + public string AppSettingStub { get; set; } + + protected override ConnectionStringSettings GetConnectionString(string settingName) + { + return new ConnectionStringSettings(ConnectionStringStub, ConnectionStringStub); + } + + protected override string GetCloudSetting(string settingName) + { + return CloudSettingStub; + } + + protected override string GetAppSetting(string settingName) + { + return AppSettingStub; + } + } + + [Fact] + public async Task WhenCloudSettingIsNullStringNullIsReturned() + { + // Arrange + var configurationService = new TestableConfigurationService(); + configurationService.CloudSettingStub = "null"; + configurationService.AppSettingStub = "bla"; + configurationService.ConnectionStringStub = "abc"; + + // Act + string result = await configurationService.ReadSetting("any"); + + // Assert + Assert. Null(result); + } + + [Fact] + public async Task WhenCloudSettingIsEmptyAppSettingIsReturned() + { + // Arrange + var configurationService = new TestableConfigurationService(); + configurationService.CloudSettingStub = null; + configurationService.AppSettingStub = string.Empty; + configurationService.ConnectionStringStub = "abc"; + + // Act + string result = await configurationService.ReadSetting("any"); + + // Assert + Assert.Equal(configurationService.ConnectionStringStub, result); + } + + [Fact] + public async Task WhenSettingIsNotEmptySecretInjectorIsRan() + { + // Arrange + var secretInjectorMock = new Mock(); + secretInjectorMock.Setup(x => x.InjectAsync(It.IsAny())) + .Returns(s => Task.FromResult(s + "parsed")); + + var secretReaderFactory = new Mock(); + secretReaderFactory.Setup(x => x.CreateSecretReader(It.IsAny())) + .Returns(new EmptySecretReader()); + secretReaderFactory.Setup(x => x.CreateSecretInjector(It.IsAny())) + .Returns(secretInjectorMock.Object); + + var configurationService = new TestableConfigurationService(secretReaderFactory.Object); + configurationService.CloudSettingStub = "somevalue"; + + // Act + string result = await configurationService.ReadSetting("any"); + + // Assert + Assert.Equal("somevalueparsed", result); + } + } } } \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/App_Start/RuntimeServiceProviderTests.cs b/tests/NuGetGallery.Facts/App_Start/RuntimeServiceProviderTests.cs new file mode 100644 index 0000000000..8daf21a46c --- /dev/null +++ b/tests/NuGetGallery.Facts/App_Start/RuntimeServiceProviderTests.cs @@ -0,0 +1,94 @@ +// 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.ComponentModel.Composition; +using System.IO; +using System.Reflection; +using Xunit; + +namespace NuGetGallery.App_Start +{ + public class RuntimeServiceProviderTests + { + private readonly string _baseDirectoryPath; + + public RuntimeServiceProviderTests() + { + var thisAssemblyDirectoryPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + _baseDirectoryPath = Path.GetDirectoryName(thisAssemblyDirectoryPath); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Create_ThrowsForNullOrEmptyString(string baseDirectoryPath) + { + Assert.Throws(() => RuntimeServiceProvider.Create(baseDirectoryPath)); + } + + [Fact] + public void Create_HandlesNonexistentDirectoryPath() + { + var baseDirectoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + var serviceProvider = RuntimeServiceProvider.Create(baseDirectoryPath); + + Assert.NotNull(serviceProvider); + } + + [Fact] + public void Dispose_IsIdempotent() + { + var serviceProvider = RuntimeServiceProvider.Create(_baseDirectoryPath); + + serviceProvider.Dispose(); + serviceProvider.Dispose(); + } + + [Fact] + public void GetExportedValues_ThrowsIfDisposed() + { + var serviceProvider = RuntimeServiceProvider.Create(_baseDirectoryPath); + + serviceProvider.Dispose(); + + Assert.Throws(() => serviceProvider.GetExportedValues()); + } + + [Fact] + public void GetExportedValues_ReturnsInstanceForExportedType() + { + using (var serviceProvider = RuntimeServiceProvider.Create(_baseDirectoryPath)) + { + var services = serviceProvider.GetExportedValues(); + + Assert.NotNull(services); + Assert.Single(services); + } + } + + [Fact] + public void GetExportedValues_ReturnsEmptyEnumerableForNonExportedType() + { + using (var serviceProvider = RuntimeServiceProvider.Create(_baseDirectoryPath)) + { + var services = serviceProvider.GetExportedValues(); + + Assert.NotNull(services); + Assert.Empty(services); + } + } + + [Export] + [PartCreationPolicy(CreationPolicy.NonShared)] + private class ExportedType + { + } + + private class NonExportedType + { + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Authentication/AuthenticationServiceFacts.cs b/tests/NuGetGallery.Facts/Authentication/AuthenticationServiceFacts.cs index 02f3e763e8..06c84c5ecd 100644 --- a/tests/NuGetGallery.Facts/Authentication/AuthenticationServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Authentication/AuthenticationServiceFacts.cs @@ -16,6 +16,7 @@ using NuGetGallery.Authentication.Providers.MicrosoftAccount; using NuGetGallery.Configuration; using NuGetGallery.Framework; +using NuGetGallery.Infrastructure.Authentication; using Xunit; namespace NuGetGallery.Authentication @@ -24,88 +25,145 @@ public class AuthenticationServiceFacts { public class TheAuthenticateMethod : TestContainer { + private Fakes _fakes; + private AuthenticationService _authenticationService; + private Mock _dateTimeProviderMock; + + public TheAuthenticateMethod() + { + _fakes = Get(); + _dateTimeProviderMock = GetMock(); + _authenticationService = Get(); + } + [Fact] - public async Task GivenNoUserWithName_ItReturnsNull() + public async Task GivenNoUserWithName_ItReturnsFailure() { - // Arrange - var service = Get(); + // Act + var result = await _authenticationService.Authenticate("notARealUser", "password"); + // Assert + Assert.Equal(PasswordAuthenticationResult.AuthenticationResult.BadCredentials, result.Result); + } + + [Fact] + public async Task WritesAuditRecordWhenGivenNoUserWithName() + { // Act - var result = await service.Authenticate("notARealUser", "password"); + await _authenticationService.Authenticate("notARealUser", "password"); // Assert - Assert.Null(result); + Assert.True(_authenticationService.Auditing.WroteRecord(ar => + ar.Action == AuditedAuthenticatedOperationAction.FailedLoginNoSuchUser && + ar.UsernameOrEmail == "notARealUser")); } [Fact] - public async Task GivenUserNameDoesNotMatchPassword_ItReturnsNull() + public async Task GivenUserNameDoesNotMatchPassword_ItReturnsFailure() { - // Arrange - var service = Get(); + // Act + var result = await _authenticationService.Authenticate(_fakes.User.Username, "bogus password!!"); + // Assert + Assert.Equal(PasswordAuthenticationResult.AuthenticationResult.BadCredentials, result.Result); + } + + [Fact] + public async Task WritesAuditRecordWhenGivenUserNameDoesNotMatchPassword() + { // Act - var result = await service.Authenticate(Fakes.User.Username, "bogus password!!"); + await _authenticationService.Authenticate(_fakes.User.Username, "bogus password!!"); // Assert - Assert.Null(result); + Assert.True(_authenticationService.Auditing.WroteRecord(ar => + ar.Action == AuditedAuthenticatedOperationAction.FailedLoginInvalidPassword && + ar.UsernameOrEmail == _fakes.User.Username)); } [Fact] public async Task GivenUserNameWithMatchingPasswordCredential_ItReturnsAuthenticatedUser() { // Arrange - var service = Get(); + var user = _fakes.User; // Act - var result = await service.Authenticate(Fakes.User.Username, Fakes.Password); + var result = await _authenticationService.Authenticate(user.Username, Fakes.Password); // Assert - var expectedCred = Fakes.User.Credentials.SingleOrDefault( - c => String.Equals(c.Type, CredentialTypes.Password.Pbkdf2, StringComparison.OrdinalIgnoreCase)); - Assert.NotNull(result); - Assert.Same(Fakes.User, result.User); - Assert.Same(expectedCred, result.CredentialUsed); + var expectedCred = user.Credentials.SingleOrDefault( + c => string.Equals(c.Type, CredentialBuilder.LatestPasswordType, StringComparison.OrdinalIgnoreCase)); + Assert.Equal(result.Result, PasswordAuthenticationResult.AuthenticationResult.Success); + Assert.Same(user, result.AuthenticatedUser.User); + Assert.Same(expectedCred, result.AuthenticatedUser.CredentialUsed); } - [Fact] - public async Task GivenUserNameWithMatchingSha1PasswordCredential_ItMigratesHashToPbkdf2() + public static IEnumerable + GivenUserNameWithMatchingOldPasswordCredential_ItMigratesHashToLatest_Input + { + get + { + return new[] + { + new object[] {new Func(f => f.Pbkdf2User)}, + new object[] {new Func(f => f.ShaUser)} + }; + } + } + + [Theory, MemberData("GivenUserNameWithMatchingOldPasswordCredential_ItMigratesHashToLatest_Input")] + public async Task GivenUserNameWithMatchingOldPasswordCredential_ItMigratesHashToLatest( + Func getUser) { // Arrange - var service = Get(); + var user = getUser(_fakes); // Act - var result = await service.Authenticate(Fakes.ShaUser.Username, Fakes.Password); + var result = await _authenticationService.Authenticate(user.Username, Fakes.Password); // Assert - var expectedCred = Fakes.User.Credentials.SingleOrDefault( - c => String.Equals(c.Type, CredentialTypes.Password.Pbkdf2, StringComparison.OrdinalIgnoreCase)); + var expectedCred = user.Credentials.SingleOrDefault( + c => string.Equals(c.Type, CredentialBuilder.LatestPasswordType, StringComparison.OrdinalIgnoreCase)); Assert.NotNull(expectedCred); - Assert.True(VerifyPasswordHash(expectedCred.Value, Constants.PBKDF2HashAlgorithmId, Fakes.Password)); + Assert.True(VerifyPasswordHash(expectedCred.Value, CredentialBuilder.LatestPasswordType, Fakes.Password)); } - [Fact] - public async Task GivenUserNameWithMatchingSha1PasswordCredential_ItWritesAuditRecordsOfMigration() + public static IEnumerable + GivenUserNameWithMatchingOldPasswordCredential_ItWritesAuditRecordsOfMigration_Input + { + get + { + return new[] + { + new object[] {new Func(f => f.Pbkdf2User)}, + new object[] {new Func(f => f.ShaUser)} + }; + } + } + + [Theory, MemberData("GivenUserNameWithMatchingOldPasswordCredential_ItWritesAuditRecordsOfMigration_Input")] + public async Task GivenUserNameWithMatchingOldPasswordCredential_ItWritesAuditRecordsOfMigration( + Func getUser) { // Arrange - var service = Get(); - var user = Fakes.CreateUser("testSha", CredentialBuilder.CreateSha1Password(Fakes.Password)); - service.Entities.Users.Add(user); + var user = getUser(_fakes); + var oldCredentialType = user.Credentials.First().Type; // Act - var result = await service.Authenticate(user.Username, Fakes.Password); + var result = await _authenticationService.Authenticate(user.Username, Fakes.Password); // Assert - Assert.True(service.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.RemovedCredential && + Assert.True(_authenticationService.Auditing.WroteRecord(ar => + ar.Action == AuditedUserAction.RemoveCredential && ar.Username == user.Username && ar.AffectedCredential.Length == 1 && - ar.AffectedCredential[0].Type == CredentialTypes.Password.Sha1 && + ar.AffectedCredential[0].Type == oldCredentialType && ar.AffectedCredential[0].Value == null)); - Assert.True(service.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.AddedCredential && + + Assert.True(_authenticationService.Auditing.WroteRecord(ar => + ar.Action == AuditedUserAction.AddCredential && ar.Username == user.Username && ar.AffectedCredential.Length == 1 && - ar.AffectedCredential[0].Type == CredentialTypes.Password.Pbkdf2 && + ar.AffectedCredential[0].Type == CredentialBuilder.LatestPasswordType && ar.AffectedCredential[0].Value == null)); } @@ -114,68 +172,191 @@ public async Task GivenUserNameWithMatchingSha1PasswordCredential_ItWritesAuditR // uses a new Salt and thus produces a value that cannot be looked up in the DB. Instead, // we must look up the user and then verify the salted password hash. [Fact] - public void GivenPasswordCredential_ItThrowsArgumentException() + public async Task GivenPasswordCredential_ItThrowsArgumentException() { // Arrange - var service = Get(); - var cred = CredentialBuilder.CreatePbkdf2Password("bogus"); + var cred = new CredentialBuilder().CreatePasswordCredential("bogus"); // Act - var ex = Assert.Throws(() => service.Authenticate(cred)); + var ex = await Assert.ThrowsAsync(async () => await _authenticationService.Authenticate(cred)); // Assert - Assert.Equal(Strings.PasswordCredentialsCannotBeUsedHere + Environment.NewLine + "Parameter name: credential", ex.Message); + Assert.Equal( + Strings.PasswordCredentialsCannotBeUsedHere + Environment.NewLine + "Parameter name: credential", + ex.Message); Assert.Equal("credential", ex.ParamName); } [Fact] - public void GivenInvalidApiKeyCredential_ItReturnsNull() + public async Task GivenInvalidApiKeyCredential_ItReturnsNull() { - // Arrange - var service = Get(); - // Act - var result = service.Authenticate(CredentialBuilder.CreateV1ApiKey()); + var result = await _authenticationService.Authenticate( + TestCredentialHelper.CreateV1ApiKey(Guid.NewGuid(), Fakes.ExpirationForApiKeyV1)); // Assert Assert.Null(result); } [Fact] - public void GivenMatchingApiKeyCredential_ItReturnsTheUserAndMatchingCredential() + public async Task WritesAuditRecordWhenGivenInvalidApiKeyCredential() + { + // Act + await _authenticationService.Authenticate(TestCredentialHelper.CreateV1ApiKey(Guid.NewGuid(), TimeSpan.Zero)); + + // Assert + Assert.True(_authenticationService.Auditing.WroteRecord(ar => + ar.Action == AuditedAuthenticatedOperationAction.FailedLoginNoSuchUser && + string.IsNullOrEmpty(ar.UsernameOrEmail))); + } + + [Theory] + [InlineData(CredentialTypes.ApiKey.V1)] + [InlineData(CredentialTypes.ApiKey.V2)] + [InlineData(CredentialTypes.ApiKey.VerifyV1)] + public async Task GivenMatchingApiKeyCredential_ItReturnsTheUserAndMatchingCredential(string apiKeyType) { // Arrange - var service = Get(); - var cred = Fakes.User.Credentials.Single( - c => String.Equals(c.Type, CredentialTypes.ApiKeyV1, StringComparison.OrdinalIgnoreCase)); + var cred = _fakes.User.Credentials.Single( + c => string.Equals(c.Type, apiKeyType, StringComparison.OrdinalIgnoreCase)); // Act // Create a new credential to verify that it's a value-based lookup! - var result = service.Authenticate(CredentialBuilder.CreateV1ApiKey(Guid.Parse(cred.Value))); + var result = await _authenticationService.Authenticate(cred.Value); // Assert Assert.NotNull(result); - Assert.Same(Fakes.User, result.User); + Assert.Same(_fakes.User, result.User); Assert.Same(cred, result.CredentialUsed); } + [Theory] + [InlineData(CredentialTypes.ApiKey.V1)] + [InlineData(CredentialTypes.ApiKey.V2)] + [InlineData(CredentialTypes.ApiKey.VerifyV1)] + public async Task GivenMatchingApiKeyCredential_ItWritesCredentialLastUsed(string apiKeyType) + { + // Arrange + var cred = _fakes.User.Credentials.Single( + c => string.Equals(c.Type, apiKeyType, StringComparison.OrdinalIgnoreCase)); + + var referenceTime = DateTime.UtcNow; + _dateTimeProviderMock.SetupGet(x => x.UtcNow).Returns(referenceTime); + + Assert.False(cred.LastUsed.HasValue); + + // Act + // Create a new credential to verify that it's a value-based lookup! + var result = + await + _authenticationService.Authenticate(cred.Value); + + // Assert + Assert.NotNull(result); + Assert.True(cred.LastUsed == referenceTime); + Assert.True(cred.LastUsed.HasValue); + } + [Fact] - public void GivenMultipleMatchingCredentials_ItThrows() + public async Task GivenMatchingCredential_ItWritesCredentialLastUsed() { // Arrange + var cred = _fakes.User.Credentials.Single(c => c.Type.Contains(CredentialTypes.ExternalPrefix)); + + var referenceTime = DateTime.UtcNow; + _dateTimeProviderMock.SetupGet(x => x.UtcNow).Returns(referenceTime); + + Assert.False(cred.LastUsed.HasValue); + + // Act + // Create a new credential to verify that it's a value-based lookup! + var result = await _authenticationService.Authenticate(TestCredentialHelper.CreateExternalCredential(cred.Value)); + + // Assert + Assert.NotNull(result); + Assert.True(cred.LastUsed == referenceTime); + Assert.True(cred.LastUsed.HasValue); + } + + [Theory] + [InlineData(CredentialTypes.ApiKey.V1)] + [InlineData(CredentialTypes.ApiKey.V2)] + [InlineData(CredentialTypes.ApiKey.VerifyV1)] + public async Task GivenExpiredMatchingApiKeyCredential_ItReturnsNull(string apiKeyType) + { + // Arrange + var cred = _fakes.User.Credentials.Single( + c => string.Equals(c.Type, apiKeyType, StringComparison.OrdinalIgnoreCase)); + + cred.Expires = DateTime.UtcNow.AddDays(-1); + + // Act + // Create a new credential to verify that it's a value-based lookup! + var result = await _authenticationService.Authenticate(cred.Value); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData(CredentialTypes.ApiKey.V1, true)] + [InlineData(CredentialTypes.ApiKey.V2, false)] + public async Task GivenMatchingApiKeyCredentialThatWasLastUsedTooLongAgo_ItReturnsNullAndExpiresTheApiKeyAndWritesAuditRecord(string apiKeyType, bool shouldExpire) + { + // Arrange + var config = GetMock(); + config.SetupGet(m => m.ExpirationInDaysForApiKeyV1).Returns(10); + + var cred = _fakes.User.Credentials.Single(c => string.Equals(c.Type, apiKeyType, StringComparison.OrdinalIgnoreCase)); + + // credential was last used < allowed last used + cred.LastUsed = DateTime.UtcNow.AddDays(-20); + var service = Get(); + + // Act + // Create a new credential to verify that it's a value-based lookup! + var result = await service.Authenticate(cred.Value); + + // Assert + + if (shouldExpire) + { + Assert.Null(result); + Assert.True(cred.HasExpired); + Assert.True(service.Auditing.WroteRecord(ar => + ar.Action == AuditedUserAction.ExpireCredential && + ar.Username == _fakes.User.Username)); + } + else + { + Assert.NotNull(result); + Assert.False(cred.HasExpired); + Assert.False(service.Auditing.WroteRecord(ar => + ar.Action == AuditedUserAction.ExpireCredential && + ar.Username == _fakes.User.Username)); + } + } + + [Fact] + public async Task GivenMultipleMatchingCredentials_ItThrows() + { + // Arrange var entities = Get(); - var cred = CredentialBuilder.CreateV1ApiKey(); + var cred = TestCredentialHelper.CreateV1ApiKey(Guid.NewGuid(), Fakes.ExpirationForApiKeyV1); cred.Key = 42; var creds = entities.Set(); creds.Add(cred); - creds.Add(CredentialBuilder.CreateV1ApiKey(Guid.Parse(cred.Value))); + creds.Add(TestCredentialHelper.CreateV1ApiKey(Guid.Parse(cred.Value), Fakes.ExpirationForApiKeyV1)); // Act - var ex = Assert.Throws(() => service.Authenticate(CredentialBuilder.CreateV1ApiKey(Guid.Parse(cred.Value)))); + var ex = await Assert.ThrowsAsync(async () => + await + _authenticationService.Authenticate(TestCredentialHelper.CreateV1ApiKey(Guid.Parse(cred.Value), + Fakes.ExpirationForApiKeyV1))); // Assert - Assert.Equal(String.Format( + Assert.Equal(string.Format( CultureInfo.CurrentCulture, Strings.MultipleMatchingCredentials, cred.Type, @@ -183,67 +364,180 @@ public void GivenMultipleMatchingCredentials_ItThrows() } [Fact] - public async Task GivenOnlyASHA1PasswordItAuthenticatesUserAndReplacesItWithAPBKDF2Password() + public async Task WhenUserLoginFailsAfterFailureUserRecordIsUpdatedWithFailureDetails() { - var user = Fakes.CreateUser("tempUser", CredentialBuilder.CreateSha1Password("thePassword")); - var service = Get(); - service.Entities.Users.Add(user); + // Arrange + var currentTime = DateTime.UtcNow; + _dateTimeProviderMock.SetupGet(x => x.UtcNow).Returns(currentTime); - var foundByUserName = await service.Authenticate("tempUser", "thePassword"); + _fakes.User.FailedLoginCount = 7; + _fakes.User.LastFailedLoginUtc = currentTime - TimeSpan.FromMinutes(1); - var cred = foundByUserName.User.Credentials.Single(); - Assert.Same(user, foundByUserName.User); - Assert.Equal(CredentialTypes.Password.Pbkdf2, cred.Type); - Assert.True(CryptographyService.ValidateSaltedHash(cred.Value, "thePassword", Constants.PBKDF2HashAlgorithmId)); - service.Entities.VerifyCommitChanges(); + // Act + await _authenticationService.Authenticate(_fakes.User.Username, "bogus password!!"); + + // Assert + Assert.Equal(currentTime, _fakes.User.LastFailedLoginUtc); + Assert.Equal(8, _fakes.User.FailedLoginCount); } [Fact] - public async Task GivenASHA1AndAPBKDF2PasswordItAuthenticatesUserAndRemovesTheSHA1Password() + public async Task WhenUserLoginFailsAfterSuccessUserRecordIsUpdatedWithFailureDetails() { - var user = Fakes.CreateUser("tempUser", - CredentialBuilder.CreateSha1Password("thePassword"), - CredentialBuilder.CreatePbkdf2Password("thePassword")); - var service = Get(); - service.Entities.Users.Add(user); + // Arrange + var currentTime = DateTime.UtcNow; + _dateTimeProviderMock.SetupGet(x => x.UtcNow).Returns(currentTime); - var foundByUserName = await service.Authenticate("tempUser", "thePassword"); + _fakes.User.FailedLoginCount = 0; + _fakes.User.LastFailedLoginUtc = null; - var cred = foundByUserName.User.Credentials.Single(); - Assert.Same(user, foundByUserName.User); - Assert.Equal(CredentialTypes.Password.Pbkdf2, cred.Type); - Assert.True(CryptographyService.ValidateSaltedHash(cred.Value, "thePassword", Constants.PBKDF2HashAlgorithmId)); - service.Entities.VerifyCommitChanges(); + // Act + await _authenticationService.Authenticate(_fakes.User.Username, "bogus password!!"); + + // Assert + Assert.Equal(currentTime, _fakes.User.LastFailedLoginUtc); + Assert.Equal(1, _fakes.User.FailedLoginCount); + } + + [Fact] + public async Task WhenUserLoginSucceedsAfterFailureFailureDetailsAreReset() + { + // Arrange + var user = _fakes.User; + user.FailedLoginCount = 8; + user.LastFailedLoginUtc = DateTime.UtcNow; + _dateTimeProviderMock.SetupGet(x => x.UtcNow).Returns(user.LastFailedLoginUtc.Value + TimeSpan.FromSeconds(10)); + + // Act + var result = await _authenticationService.Authenticate(user.Username, Fakes.Password); + + // Assert + Assert.Equal(PasswordAuthenticationResult.AuthenticationResult.Success, result.Result); + Assert.Same(user, result.AuthenticatedUser.User); + Assert.Equal(0, user.FailedLoginCount); + Assert.Null(user.LastFailedLoginUtc); + } + + [Fact] + public async Task WhenUserLoginSucceedsAfterSuccessFailureDetailsAreReset() + { + // Arrange + var user = _fakes.User; + user.FailedLoginCount = 0; + user.LastFailedLoginUtc = DateTime.UtcNow; + + // Act + var result = await _authenticationService.Authenticate(user.Username, Fakes.Password); + + // Assert + Assert.Equal(PasswordAuthenticationResult.AuthenticationResult.Success, result.Result); + Assert.Same(user, result.AuthenticatedUser.User); + Assert.Equal(0, user.FailedLoginCount); + Assert.Null(user.LastFailedLoginUtc); + } + + [Theory] + [MemberData("VerifyAccountLockoutTimeCalculation_Data")] + public async Task VerifyAccountLockoutTimeCalculation(int failureCount, DateTime? lastFailedLoginTime, DateTime currentTime, int expectedLockoutMinutesLeft) + { + // Arrange + var user = _fakes.User; + user.FailedLoginCount = failureCount; + user.LastFailedLoginUtc = lastFailedLoginTime; + + _dateTimeProviderMock.SetupGet(x => x.UtcNow).Returns(currentTime); + + // Act + var result = await _authenticationService.Authenticate(user.Username, Fakes.Password); + + // Assert + var expectedResult = expectedLockoutMinutesLeft == 0 + ? PasswordAuthenticationResult.AuthenticationResult.Success + : PasswordAuthenticationResult.AuthenticationResult.AccountLocked; + + Assert.Equal(expectedResult, result.Result); + Assert.Equal(expectedLockoutMinutesLeft, result.LockTimeRemainingMinutes); + } + + public static IEnumerable VerifyAccountLockoutTimeCalculation_Data + { + get + { + return new[] + { + // No failed logins + new object[] {0, null, DateTime.UtcNow, 0}, + // Small number of failed logins, no lock required + new object[] {1, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 0, 0, 1), 0}, + new object[] {5, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 0, 0, 1), 0}, + new object[] {9, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 0, 0, 1), 0}, + // Initial lockout period + new object[] {10, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 0, 0, 1), 1}, + new object[] {19, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 0, 0, 59), 1}, + // Exponentially increasing lockout period + new object[] {21, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 0, 0, 1), 10}, + new object[] {25, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 0, 9, 0), 1}, + new object[] {29, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 0, 5, 30), 5}, + new object[] {30, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 0, 0, 1), 100}, + // Lockout expired + new object[] {10, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 0, 10, 0), 0}, + new object[] {20, new DateTime(2016, 9, 30, 0, 0, 0), new DateTime(2016, 9, 30, 1, 40, 0), 0} + }; + } } } - public class TheCreateSessionMethod : TestContainer + public class TheCreateSessionAsyncMethod : TestContainer { [Fact] - public void GivenAUser_ItCreatesAnOwinAuthenticationTicketForTheUser() + public async Task GivenAUser_ItCreatesAnOwinAuthenticationTicketForTheUser() { // Arrange var service = Get(); + var fakes = Get(); var context = Fakes.CreateOwinContext(); - var passwordCred = Fakes.Admin.Credentials.SingleOrDefault( - c => String.Equals(c.Type, CredentialTypes.Password.Pbkdf2, StringComparison.OrdinalIgnoreCase)); + var passwordCred = fakes.Admin.Credentials.SingleOrDefault( + c => string.Equals(c.Type, CredentialTypes.Password.Pbkdf2, StringComparison.OrdinalIgnoreCase)); - var authUser = new AuthenticatedUser(Fakes.Admin, passwordCred); + var authUser = new AuthenticatedUser(fakes.Admin, passwordCred); // Act - service.CreateSession(context, authUser.User); + await service.CreateSessionAsync(context, authUser); // Assert var principal = context.Authentication.AuthenticationResponseGrant.Principal; var id = principal.Identity; Assert.NotNull(principal); Assert.NotNull(id); - Assert.Equal(Fakes.Admin.Username, id.Name); - Assert.Equal(Fakes.Admin.Username, principal.GetClaimOrDefault(ClaimTypes.NameIdentifier)); + Assert.Equal(fakes.Admin.Username, id.Name); + Assert.Equal(fakes.Admin.Username, principal.GetClaimOrDefault(ClaimTypes.NameIdentifier)); Assert.Equal(AuthenticationTypes.LocalUser, id.AuthenticationType); Assert.True(principal.IsInRole(Constants.AdminRoleName)); } + + [Fact] + public async Task WritesAnAuditRecord() + { + // Arrange + var service = Get(); + var fakes = Get(); + var context = Fakes.CreateOwinContext(); + + var credential = fakes.Admin.Credentials.SingleOrDefault( + c => string.Equals(c.Type, CredentialTypes.Password.Pbkdf2, StringComparison.OrdinalIgnoreCase)); + + var authenticatedUser = new AuthenticatedUser(fakes.Admin, credential); + + // Act + await service.CreateSessionAsync(context, authenticatedUser); + + // Assert + var authenticationService = Get(); + Assert.True(authenticationService.Auditing.WroteRecord(ar => + ar.Action == AuditedUserAction.Login && + ar.Username == fakes.Admin.Username)); + } } public class TheRegisterMethod : TestContainer @@ -251,22 +545,24 @@ public class TheRegisterMethod : TestContainer [Fact] public async Task GivenPlainTextPassword_ItSaltsHashesAndPassesThru() { + var fakes = Get(); + // Just tests that the obsolete version passes through to the new version string password = "thePassword"; var mock = GetMock(); // Mock out the new version, we only care that it is called with expected params mock.Setup(a => a.Register( - Fakes.User.Username, - Fakes.User.EmailAddress, + fakes.User.Username, + fakes.User.EmailAddress, It.Is(c => VerifyPasswordHash( c.Value, - Constants.PBKDF2HashAlgorithmId, + CredentialBuilder.LatestPasswordType, password)))) .CompletesWithNull() .Verifiable(); // Act - await mock.Object.Register(Fakes.User.Username, password, Fakes.User.EmailAddress); + await mock.Object.Register(fakes.User.Username, fakes.User.EmailAddress, new CredentialBuilder().CreatePasswordCredential(password)); // Assert mock.VerifyAll(); @@ -277,16 +573,17 @@ public async Task WillThrowIfTheUsernameIsAlreadyInUse() { // Arrange var auth = Get(); + var fakes = Get(); // Act var ex = await AssertEx.Throws(() => auth.Register( - Fakes.User.Username, + fakes.User.Username, "theEmailAddress", - CredentialBuilder.CreatePbkdf2Password("thePassword"))); + new CredentialBuilder().CreatePasswordCredential("thePassword"))); // Assert - Assert.Equal(String.Format(Strings.UsernameNotAvailable, Fakes.User.Username), ex.Message); + Assert.Equal(string.Format(Strings.UsernameNotAvailable, fakes.User.Username), ex.Message); } [Fact] @@ -294,16 +591,17 @@ public async Task WillThrowIfTheEmailAddressIsAlreadyInUse() { // Arrange var auth = Get(); + var fakes = Get(); // Act var ex = await AssertEx.Throws(() => auth.Register( "newUser", - Fakes.User.EmailAddress, - CredentialBuilder.CreatePbkdf2Password("thePassword"))); + fakes.User.EmailAddress, + new CredentialBuilder().CreatePasswordCredential("thePassword"))); // Assert - Assert.Equal(String.Format(Strings.EmailAddressBeingUsed, Fakes.User.EmailAddress), ex.Message); + Assert.Equal(string.Format(Strings.EmailAddressBeingUsed, fakes.User.EmailAddress), ex.Message); } [Fact] @@ -316,7 +614,7 @@ public async Task WillSaveTheNewUser() var authUser = await auth.Register( "newUser", "theEmailAddress", - CredentialBuilder.CreatePbkdf2Password("thePassword")); + new CredentialBuilder().CreatePasswordCredential("thePassword")); // Assert Assert.True(auth.Entities.Users.Contains(authUser.User)); @@ -336,7 +634,7 @@ public async Task WillSaveTheNewUserAsConfirmedWhenConfigured() var authUser = await auth.Register( "newUser", "theEmailAddress", - CredentialBuilder.CreatePbkdf2Password("thePassword")); + new CredentialBuilder().CreatePasswordCredential("thePassword")); // Assert Assert.True(auth.Entities.Users.Contains(authUser.User)); @@ -344,26 +642,6 @@ public async Task WillSaveTheNewUserAsConfirmedWhenConfigured() auth.Entities.VerifyCommitChanges(); } - [Fact] - public async Task SetsAnApiKey() - { - // Arrange - var auth = Get(); - - // Arrange - var authUser = await auth.Register( - "newUser", - "theEmailAddress", - CredentialBuilder.CreatePbkdf2Password("thePassword")); - - // Assert - Assert.True(auth.Entities.Users.Contains(authUser.User)); - auth.Entities.VerifyCommitChanges(); - - var apiKeyCred = authUser.User.Credentials.FirstOrDefault(c => c.Type == CredentialTypes.ApiKeyV1); - Assert.NotNull(apiKeyCred); - } - [Fact] public async Task SetsAConfirmationToken() { @@ -377,7 +655,7 @@ public async Task SetsAConfirmationToken() var authUser = await auth.Register( "newUser", "theEmailAddress", - CredentialBuilder.CreatePbkdf2Password("thePassword")); + new CredentialBuilder().CreatePasswordCredential("thePassword")); // Assert Assert.True(auth.Entities.Users.Contains(authUser.User)); @@ -397,7 +675,7 @@ public async Task SetsCreatedDate() var authUser = await auth.Register( "newUser", "theEmailAddress", - CredentialBuilder.CreatePbkdf2Password("thePassword")); + new CredentialBuilder().CreatePasswordCredential("thePassword")); // Assert Assert.True(auth.Entities.Users.Contains(authUser.User)); @@ -417,11 +695,11 @@ public async Task WritesAnAuditRecord() var authUser = await auth.Register( "newUser", "theEmailAddress", - CredentialBuilder.CreatePbkdf2Password("thePassword")); + new CredentialBuilder().CreatePasswordCredential("thePassword")); // Assert Assert.True(auth.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.Registered && + ar.Action == AuditedUserAction.Register && ar.Username == "newUser")); } } @@ -446,9 +724,10 @@ public async Task ThrowsExceptionIfNoUserWithProvidedUserName() public async Task AddsNewCredentialIfNoneWithSameTypeForUser() { // Arrange + var fakes = Get(); var existingCred = new Credential("foo", "bar"); var newCred = new Credential("baz", "boz"); - var user = Fakes.CreateUser("foo", existingCred); + var user = fakes.CreateUser("foo", existingCred); var service = Get(); service.Entities.Users.Add(user); @@ -464,10 +743,11 @@ public async Task AddsNewCredentialIfNoneWithSameTypeForUser() public async Task ReplacesExistingCredentialIfOneWithSameTypeExistsForUser() { // Arrange + var fakes = Get(); var frozenCred = new Credential("foo", "bar"); var existingCred = new Credential("baz", "bar"); var newCred = new Credential("baz", "boz"); - var user = Fakes.CreateUser("foo", existingCred, frozenCred); + var user = fakes.CreateUser("foo", existingCred, frozenCred); var service = Get(); service.Entities.Users.Add(user); @@ -484,9 +764,10 @@ public async Task ReplacesExistingCredentialIfOneWithSameTypeExistsForUser() public async Task WritesAuditRecordRemovingTheOldCredential() { // Arrange + var fakes = Get(); var existingCred = new Credential("baz", "bar"); var newCred = new Credential("baz", "boz"); - var user = Fakes.CreateUser("foo", existingCred); + var user = fakes.CreateUser("foo", existingCred); var service = Get(); service.Entities.Users.Add(user); @@ -495,21 +776,24 @@ public async Task WritesAuditRecordRemovingTheOldCredential() // Assert Assert.True(service.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.RemovedCredential && + ar.Action == AuditedUserAction.RemoveCredential && ar.Username == user.Username && ar.AffectedCredential.Length == 1 && ar.AffectedCredential[0].Type == existingCred.Type && ar.AffectedCredential[0].Identity == existingCred.Identity && - ar.AffectedCredential[0].Value == existingCred.Value)); + ar.AffectedCredential[0].Value == existingCred.Value && + ar.AffectedCredential[0].Created == existingCred.Created && + ar.AffectedCredential[0].Expires == existingCred.Expires)); } [Fact] public async Task WritesAuditRecordAddingTheNewCredential() { // Arrange + var fakes = Get(); var existingCred = new Credential("foo", "bar"); var newCred = new Credential("baz", "boz"); - var user = Fakes.CreateUser("foo", existingCred); + var user = fakes.CreateUser("foo", existingCred); var service = Get(); service.Entities.Users.Add(user); @@ -518,12 +802,14 @@ public async Task WritesAuditRecordAddingTheNewCredential() // Assert Assert.True(service.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.AddedCredential && + ar.Action == AuditedUserAction.AddCredential && ar.Username == user.Username && ar.AffectedCredential.Length == 1 && ar.AffectedCredential[0].Type == newCred.Type && ar.AffectedCredential[0].Identity == newCred.Identity && - ar.AffectedCredential[0].Value == null)); + ar.AffectedCredential[0].Value == null && + ar.AffectedCredential[0].Created == existingCred.Created && + ar.AffectedCredential[0].Expires == existingCred.Expires)); } } @@ -563,14 +849,14 @@ public async Task ThrowsExceptionIfUserNotConfirmed() public async Task ResetsPasswordCredential() { // Arrange - var oldCred = CredentialBuilder.CreatePbkdf2Password("thePassword"); + var oldCred = new CredentialBuilder().CreatePasswordCredential("thePassword"); var user = new User { Username = "user", EmailAddress = "confirmed@example.com", PasswordResetToken = "some-token", PasswordResetTokenExpirationDate = DateTime.UtcNow.AddDays(1), - Credentials = new List() { oldCred } + Credentials = new List { oldCred } }; var authService = Get(); @@ -583,22 +869,37 @@ public async Task ResetsPasswordCredential() Assert.NotNull(result); var newCred = user.Credentials.Single(); Assert.Same(result, newCred); - Assert.Equal(CredentialTypes.Password.Pbkdf2, newCred.Type); - Assert.True(VerifyPasswordHash(newCred.Value, Constants.PBKDF2HashAlgorithmId, "new-password")); + Assert.Equal(CredentialBuilder.LatestPasswordType, newCred.Type); + Assert.True(VerifyPasswordHash(newCred.Value, CredentialBuilder.LatestPasswordType, "new-password")); authService.Entities.VerifyCommitChanges(); + Assert.Equal(0, user.FailedLoginCount); + Assert.Null(user.LastFailedLoginUtc); } - [Fact] - public async Task ResetsPasswordMigratesPasswordHash() + + public static IEnumerable ResetsPasswordMigratesPasswordHash_Input + { + get + { + return new[] + { + new object[] {new Func(TestCredentialHelper.CreateSha1Password)}, + new object[] {new Func(TestCredentialHelper.CreatePbkdf2Password)} + }; + } + } + + [Theory, MemberData("ResetsPasswordMigratesPasswordHash_Input")] + public async Task ResetsPasswordMigratesPasswordHash(Func oldCredentialBuilder) { - var oldCred = CredentialBuilder.CreateSha1Password("thePassword"); + var oldCred = oldCredentialBuilder("thePassword"); var user = new User { Username = "user", EmailAddress = "confirmed@example.com", PasswordResetToken = "some-token", PasswordResetTokenExpirationDate = DateTime.UtcNow.AddDays(1), - Credentials = new List() { oldCred } + Credentials = new List { oldCred } }; var authService = Get(); @@ -610,8 +911,8 @@ public async Task ResetsPasswordMigratesPasswordHash() Assert.NotNull(result); var newCred = user.Credentials.Single(); Assert.Same(result, newCred); - Assert.Equal(CredentialTypes.Password.Pbkdf2, newCred.Type); - Assert.True(VerifyPasswordHash(newCred.Value, Constants.PBKDF2HashAlgorithmId, "new-password")); + Assert.Equal(CredentialBuilder.LatestPasswordType, newCred.Type); + Assert.True(VerifyPasswordHash(newCred.Value, CredentialBuilder.LatestPasswordType, "new-password")); authService.Entities.VerifyCommitChanges(); } @@ -619,14 +920,14 @@ public async Task ResetsPasswordMigratesPasswordHash() public async Task WritesAuditRecordWhenReplacingPasswordCredential() { // Arrange - var oldCred = CredentialBuilder.CreatePbkdf2Password("thePassword"); + var oldCred = TestCredentialHelper.CreatePbkdf2Password("thePassword"); var user = new User { Username = "user", EmailAddress = "confirmed@example.com", PasswordResetToken = "some-token", PasswordResetTokenExpirationDate = DateTime.UtcNow.AddDays(1), - Credentials = new List() { oldCred } + Credentials = new List { oldCred } }; var authService = Get(); @@ -637,17 +938,17 @@ public async Task WritesAuditRecordWhenReplacingPasswordCredential() // Assert Assert.True(authService.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.RemovedCredential && + ar.Action == AuditedUserAction.RemoveCredential && ar.Username == user.Username && ar.AffectedCredential.Length == 1 && ar.AffectedCredential[0].Type == CredentialTypes.Password.Pbkdf2 && ar.AffectedCredential[0].Identity == null && ar.AffectedCredential[0].Value == null)); Assert.True(authService.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.AddedCredential && + ar.Action == AuditedUserAction.AddCredential && ar.Username == user.Username && ar.AffectedCredential.Length == 1 && - ar.AffectedCredential[0].Type == CredentialTypes.Password.Pbkdf2 && + ar.AffectedCredential[0].Type == CredentialBuilder.LatestPasswordType && ar.AffectedCredential[0].Identity == null && ar.AffectedCredential[0].Value == null)); } @@ -770,7 +1071,7 @@ public async Task WritesAuditRecordWhenGeneratingNewToken() // Assert Assert.True(authService.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.RequestedPasswordReset && + ar.Action == AuditedUserAction.RequestPasswordReset && ar.Username == user.Username)); } } @@ -781,7 +1082,8 @@ public class TheChangePasswordMethod : TestContainer public async Task GivenInvalidOldPassword_ItReturnsFalseAndDoesNotChangePassword() { // Arrange - var user = Fakes.CreateUser("test", CredentialBuilder.CreatePbkdf2Password(Fakes.Password)); + var fakes = Get(); + var user = fakes.CreateUser("test", new CredentialBuilder().CreatePasswordCredential(Fakes.Password)); var authService = Get(); // Act @@ -795,7 +1097,8 @@ public async Task GivenInvalidOldPassword_ItReturnsFalseAndDoesNotChangePassword public async Task GivenValidOldPassword_ItReturnsTrueAndReplacesPasswordCredential() { // Arrange - var user = Fakes.CreateUser("test", CredentialBuilder.CreatePbkdf2Password(Fakes.Password)); + var fakes = Get(); + var user = fakes.CreateUser("test", new CredentialBuilder().CreatePasswordCredential(Fakes.Password)); var authService = Get(); // Act @@ -804,15 +1107,16 @@ public async Task GivenValidOldPassword_ItReturnsTrueAndReplacesPasswordCredenti // Assert Assert.True(result); - Credential _; - Assert.True(AuthenticationService.ValidatePasswordCredential(user.Credentials, "new-password!", out _)); + var credentialValidator = new CredentialValidator(); + Assert.True(credentialValidator.ValidatePasswordCredential(user.Credentials.First(), "new-password!")); } [Fact] public async Task GivenValidOldPassword_ItWritesAnAuditRecordOfTheChange() { // Arrange - var user = Fakes.CreateUser("test", CredentialBuilder.CreatePbkdf2Password(Fakes.Password)); + var fakes = Get(); + var user = fakes.CreateUser("test", new CredentialBuilder().CreatePasswordCredential(Fakes.Password)); var authService = Get(); // Act @@ -820,15 +1124,15 @@ public async Task GivenValidOldPassword_ItWritesAnAuditRecordOfTheChange() // Assert Assert.True(authService.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.RemovedCredential && + ar.Action == AuditedUserAction.RemoveCredential && ar.Username == user.Username && ar.AffectedCredential.Length == 1 && - ar.AffectedCredential[0].Type == CredentialTypes.Password.Pbkdf2)); + ar.AffectedCredential[0].Type == CredentialBuilder.LatestPasswordType)); Assert.True(authService.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.AddedCredential && + ar.Action == AuditedUserAction.AddCredential && ar.Username == user.Username && ar.AffectedCredential.Length == 1 && - ar.AffectedCredential[0].Type == CredentialTypes.Password.Pbkdf2)); + ar.AffectedCredential[0].Type == CredentialBuilder.LatestPasswordType)); } } @@ -883,8 +1187,11 @@ public class TheAddCredentialMethod : TestContainer public async Task AddsTheCredentialToTheDataStore() { // Arrange - var user = Fakes.CreateUser("test", CredentialBuilder.CreatePbkdf2Password(Fakes.Password)); - var cred = CredentialBuilder.CreateExternalCredential("flarg", "glarb", "blarb"); + var credentialBuilder = new CredentialBuilder(); + + var fakes = Get(); + var user = fakes.CreateUser("test", credentialBuilder.CreatePasswordCredential(Fakes.Password)); + var cred = credentialBuilder.CreateExternalCredential("flarg", "glarb", "blarb"); var authService = Get(); // Act @@ -899,8 +1206,11 @@ public async Task AddsTheCredentialToTheDataStore() public async Task WritesAuditRecordForTheNewCredential() { // Arrange - var user = Fakes.CreateUser("test", CredentialBuilder.CreatePbkdf2Password(Fakes.Password)); - var cred = CredentialBuilder.CreateExternalCredential("flarg", "glarb", "blarb"); + var credentialBuilder = new CredentialBuilder(); + + var fakes = Get(); + var user = fakes.CreateUser("test", credentialBuilder.CreatePasswordCredential(Fakes.Password)); + var cred = credentialBuilder.CreateExternalCredential("flarg", "glarb", "blarb"); var authService = Get(); // Act @@ -908,7 +1218,7 @@ public async Task WritesAuditRecordForTheNewCredential() // Assert Assert.True(authService.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.AddedCredential && + ar.Action == AuditedUserAction.AddCredential && ar.Username == user.Username && ar.AffectedCredential.Length == 1 && ar.AffectedCredential[0].Type == cred.Type && @@ -922,7 +1232,7 @@ public class TheDescribeCredentialMethod : TestContainer public void GivenAPasswordCredential_ItDescribesItCorrectly() { // Arrange - var cred = CredentialBuilder.CreatePbkdf2Password("wibblejab"); + var cred = new CredentialBuilder().CreatePasswordCredential("wibblejab"); var authService = Get(); // Act @@ -932,16 +1242,28 @@ public void GivenAPasswordCredential_ItDescribesItCorrectly() Assert.Equal(cred.Type, description.Type); Assert.Equal(Strings.CredentialType_Password, description.TypeCaption); Assert.Null(description.Identity); - Assert.True(String.IsNullOrEmpty(description.Value)); Assert.Equal(CredentialKind.Password, description.Kind); Assert.Null(description.AuthUI); } - [Fact] - public void GivenATokenCredential_ItDescribesItCorrectly() + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(true, true, true)] + [Theory] + public void GivenATokenCredential_LegacyApiKey_ItDescribesItCorrectly(bool hasExpired, bool hasBeenUsedInLastDays, bool expectedHasExpired) { // Arrange - var cred = CredentialBuilder.CreateV1ApiKey(Guid.NewGuid()); + const int expirationForApiKeyV1 = 365; + + var mockConfig = GetMock(); + mockConfig.SetupGet(x => x.ExpirationInDaysForApiKeyV1).Returns(expirationForApiKeyV1); + + var cred = TestCredentialHelper.CreateV1ApiKey(Guid.NewGuid(), Fakes.ExpirationForApiKeyV1); + cred.LastUsed = hasBeenUsedInLastDays + ? DateTime.UtcNow + : DateTime.UtcNow - TimeSpan.FromDays(expirationForApiKeyV1 + 1); + cred.Expires = hasExpired ? DateTime.UtcNow - TimeSpan.FromDays(1) : DateTime.UtcNow + TimeSpan.FromDays(1); + var authService = Get(); // Act @@ -951,16 +1273,61 @@ public void GivenATokenCredential_ItDescribesItCorrectly() Assert.Equal(cred.Type, description.Type); Assert.Equal(Strings.CredentialType_ApiKey, description.TypeCaption); Assert.Null(description.Identity); + Assert.Equal(cred.Created, description.Created); + Assert.Equal(cred.Expires, description.Expires); + Assert.Equal(CredentialKind.Token, description.Kind); + Assert.Null(description.AuthUI); Assert.Equal(cred.Value, description.Value); + Assert.Equal(Strings.NonScopedApiKeyDescription, description.Description); + Assert.Equal(expectedHasExpired, description.HasExpired); + } + + [InlineData(false)] + [InlineData(true)] + [Theory] + public void GivenATokenCredential_ScopedApiKey_ItDescribesItCorrectly(bool hasExpired) + { + // Arrange + var cred = new CredentialBuilder().CreateApiKey(Fakes.ExpirationForApiKeyV1); + cred.Description = "description"; + cred.Scopes = new[] { new Scope("123", NuGetScopes.PackagePushVersion), new Scope("123", NuGetScopes.PackageUnlist) }; + cred.Expires = hasExpired ? DateTime.UtcNow - TimeSpan.FromDays(1) : DateTime.UtcNow + TimeSpan.FromDays(1); + cred.ExpirationTicks = TimeSpan.TicksPerDay; + + var authService = Get(); + + // Act + var description = authService.DescribeCredential(cred); + + // Assert + Assert.Equal(cred.Type, description.Type); + Assert.Equal(Strings.CredentialType_ApiKey, description.TypeCaption); + Assert.Null(description.Identity); + Assert.Equal(cred.Created, description.Created); + Assert.Equal(cred.Expires, description.Expires); + Assert.Equal(cred.HasExpired, description.HasExpired); Assert.Equal(CredentialKind.Token, description.Kind); Assert.Null(description.AuthUI); + Assert.Equal(cred.Description, description.Description); + Assert.Equal(hasExpired, description.HasExpired); + + Assert.True(description.Scopes.Count == 2); + Assert.Equal(NuGetScopes.Describe(NuGetScopes.PackagePushVersion), description.Scopes[0].AllowedAction); + Assert.Equal("123", description.Scopes[0].Subject); + Assert.Equal(NuGetScopes.Describe(NuGetScopes.PackageUnlist), description.Scopes[1].AllowedAction); + Assert.Equal("123", description.Scopes[1].Subject); + Assert.Equal(cred.ExpirationTicks.Value, description.ExpirationDuration.Value.Ticks); } - [Fact] - public void GivenAnExternalCredential_ItDescribesItCorrectly() + [InlineData(false)] + [InlineData(true)] + [Theory] + public void GivenAnExternalCredential_ItDescribesItCorrectly(bool hasExpired) { // Arrange - var cred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "abc123", "Test User"); + var cred = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "abc123", "Test User"); + cred.Expires = hasExpired ? DateTime.UtcNow - TimeSpan.FromDays(1) : DateTime.UtcNow + TimeSpan.FromDays(1); + var msftAuther = new MicrosoftAccountAuthenticator(); var authService = Get(); @@ -971,10 +1338,10 @@ public void GivenAnExternalCredential_ItDescribesItCorrectly() Assert.Equal(cred.Type, description.Type); Assert.Equal(msftAuther.GetUI().Caption, description.TypeCaption); Assert.Equal(cred.Identity, description.Identity); - Assert.True(String.IsNullOrEmpty(description.Value)); Assert.Equal(CredentialKind.External, description.Kind); Assert.NotNull(description.AuthUI); Assert.Equal(msftAuther.GetUI().AccountNoun, description.AuthUI.AccountNoun); + Assert.Equal(hasExpired, description.HasExpired); } } @@ -984,8 +1351,11 @@ public class TheRemoveCredentialMethod : TestContainer public async Task RemovesTheCredentialFromTheDataStore() { // Arrange - var cred = CredentialBuilder.CreateExternalCredential("flarg", "glarb", "blarb"); - var user = Fakes.CreateUser("test", CredentialBuilder.CreatePbkdf2Password(Fakes.Password), cred); + var credentialBuilder = new CredentialBuilder(); + + var fakes = Get(); + var cred = credentialBuilder.CreateExternalCredential("flarg", "glarb", "blarb"); + var user = fakes.CreateUser("test", credentialBuilder.CreatePasswordCredential(Fakes.Password), cred); var authService = Get(); // Act @@ -1001,8 +1371,11 @@ public async Task RemovesTheCredentialFromTheDataStore() public async Task WritesAuditRecordForTheRemovedCredential() { // Arrange - var cred = CredentialBuilder.CreateExternalCredential("flarg", "glarb", "blarb"); - var user = Fakes.CreateUser("test", CredentialBuilder.CreatePbkdf2Password(Fakes.Password), cred); + var credentialBuilder = new CredentialBuilder(); + + var fakes = Get(); + var cred = credentialBuilder.CreateExternalCredential("flarg", "glarb", "blarb"); + var user = fakes.CreateUser("test", credentialBuilder.CreatePasswordCredential(Fakes.Password), cred); var authService = Get(); // Act @@ -1010,7 +1383,7 @@ public async Task WritesAuditRecordForTheRemovedCredential() // Assert Assert.True(authService.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.RemovedCredential && + ar.Action == AuditedUserAction.RemoveCredential && ar.Username == user.Username && ar.AffectedCredential.Length == 1 && ar.AffectedCredential[0].Type == cred.Type && @@ -1019,6 +1392,80 @@ public async Task WritesAuditRecordForTheRemovedCredential() } } + public class TheEditCredentialMethod : TestContainer + { + [Fact] + public async Task SavesChangesInTheDataStore() + { + // Arrange + var credentialBuilder = new CredentialBuilder(); + + var fakes = Get(); + var entities = Get(); + + var cred = credentialBuilder.CreateApiKey(null); + var user = fakes.CreateUser("test", credentialBuilder.CreatePasswordCredential(Fakes.Password), cred); + var authService = Get(); + + var credscopes = + Enumerable.Range(0, 5) + .Select( + i => new Scope { AllowedAction = NuGetScopes.PackagePush, Key = i, Subject = "package" + i }).ToList(); + + var newScopes = + Enumerable.Range(1, 2) + .Select( + i => new Scope { AllowedAction = NuGetScopes.PackageUnlist, Key = i*10, Subject = "otherpackage" + i }).ToList(); + + cred.Scopes = credscopes; + + foreach (var scope in credscopes) + { + entities.Scopes.Add(scope); + } + + // Add an unrelated scope to make sure it's not removed + entities.Scopes.Add(new Scope { AllowedAction = NuGetScopes.PackagePush, Key = 999, Subject = "package999" }); + + // Act + await authService.EditCredentialScopes(user, cred, newScopes); + + // Assert + Assert.Equal(1, authService.Entities.Scopes.Count()); + Assert.True(authService.Entities.Scopes.First().Key == 999); + + Assert.Equal(newScopes.Count, cred.Scopes.Count); + foreach (var newScope in newScopes) + { + Assert.NotNull(cred.Scopes.FirstOrDefault(x => x.Key == newScope.Key)); + } + + authService.Entities.VerifyCommitChanges(); + } + + [Fact] + public async Task WritesAuditRecordForTheEditedCredential() + { + // Arrange + var credentialBuilder = new CredentialBuilder(); + + var fakes = Get(); + var cred = credentialBuilder.CreateApiKey(null); + var user = fakes.CreateUser("test", credentialBuilder.CreatePasswordCredential(Fakes.Password), cred); + var authService = Get(); + + // Act + await authService.EditCredentialScopes(user, cred, new List()); + + // Assert + Assert.True(authService.Auditing.WroteRecord(ar => + ar.Action == AuditedUserAction.EditCredential && + ar.Username == user.Username && + ar.AffectedCredential.Length == 1 && + ar.AffectedCredential[0].Type == cred.Type)); + } + } + public class TheExtractExternalLoginCredentialsMethod : TestContainer { [Fact] @@ -1184,15 +1631,10 @@ public void Attach(IOwinContext context) public static bool VerifyPasswordHash(string hash, string algorithm, string password) { - bool canAuthenticate = CryptographyService.ValidateSaltedHash( - hash, - password, - algorithm); - - bool sanity = CryptographyService.ValidateSaltedHash( - hash, - "not_the_password", - algorithm); + var validator = CredentialValidator.Validators[algorithm]; + bool canAuthenticate = validator(password, new Credential { Value = hash }); + + bool sanity = validator("not_the_password", new Credential { Value = hash }); return canAuthenticate && !sanity; } diff --git a/tests/NuGetGallery.Facts/Authentication/AuthenticatorFacts.cs b/tests/NuGetGallery.Facts/Authentication/AuthenticatorFacts.cs index dd5f6dbb97..5e28334058 100644 --- a/tests/NuGetGallery.Facts/Authentication/AuthenticatorFacts.cs +++ b/tests/NuGetGallery.Facts/Authentication/AuthenticatorFacts.cs @@ -1,11 +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; -using System.Collections.Generic; + using System.Linq; -using System.Text; using System.Threading.Tasks; -using Moq; using NuGetGallery.Authentication.Providers; using NuGetGallery.Configuration; using NuGetGallery.Framework; @@ -46,57 +43,51 @@ public void DefaultsConfigurationToDisabled() public class TheStartupMethod : TestContainer { [Fact] - public void LoadsConfigFromConfigurationService() + public async Task LoadsConfigFromConfigurationService() { // Arrange var authConfig = new AuthenticatorConfiguration(); - GetMock() - .Setup(c => c.ResolveConfigObject(It.IsAny(), "Auth.ATest.")) - .Returns(authConfig); var auther = new ATestAuthenticator(); // Act - auther.Startup(Get(), Get()); + await auther.Startup(Get(), Get()); // Assert - Assert.Same(authConfig, auther.BaseConfig); + Assert.Equal(authConfig.Enabled, auther.BaseConfig.Enabled); + Assert.Equal(authConfig.AuthenticationType, auther.BaseConfig.AuthenticationType); } [Fact] - public void DoesNotAttachToOwinAppIfDisabled() + public async Task DoesNotAttachToOwinAppIfDisabled() { // Arrange - var authConfig = new AuthenticatorConfiguration() - { - Enabled = false - }; - GetMock() - .Setup(c => c.ResolveConfigObject(It.IsAny(), "Auth.ATest.")) - .Returns(authConfig); var auther = new ATestAuthenticator(); + var tempAuthConfig = new AuthenticatorConfiguration(); + + var mockConfiguration = (TestGalleryConfigurationService)Get(); + mockConfiguration.Settings[$"{Authenticator.AuthPrefix}{auther.Name}.{nameof(tempAuthConfig.Enabled)}"] = "false"; + // Act - auther.Startup(Get(), Get()); + await auther.Startup(Get(), Get()); // Assert Assert.Null(auther.AttachedTo); } [Fact] - public void AttachesToOwinAppIfEnabled() + public async Task AttachesToOwinAppIfEnabled() { // Arrange - var authConfig = new AuthenticatorConfiguration() - { - Enabled = true - }; - GetMock() - .Setup(c => c.ResolveConfigObject(It.IsAny(), "Auth.ATest.")) - .Returns(authConfig); var auther = new ATestAuthenticator(); + var tempAuthConfig = new AuthenticatorConfiguration(); + + var mockConfiguration = (TestGalleryConfigurationService)Get(); + mockConfiguration.Settings[$"{Authenticator.AuthPrefix}{auther.Name}.{nameof(tempAuthConfig.Enabled)}"] = "true"; + // Act - auther.Startup(Get(), Get()); + await auther.Startup(mockConfiguration, Get()); // Assert Assert.Same(Get(), auther.AttachedTo); @@ -132,7 +123,7 @@ public void ReturnsInstanceOfGenericParameter() private class ATestAuthenticator : Authenticator { public IAppBuilder AttachedTo { get; private set; } - protected override void AttachToOwinApp(ConfigurationService config, IAppBuilder app) + protected override void AttachToOwinApp(IGalleryConfigurationService config, IAppBuilder app) { AttachedTo = app; base.AttachToOwinApp(config, app); diff --git a/tests/NuGetGallery.Facts/Authentication/ForceSslWhenAuthenticatedMiddlewareFacts.cs b/tests/NuGetGallery.Facts/Authentication/ForceSslWhenAuthenticatedMiddlewareFacts.cs index f1c099f6c4..3cea1a119c 100644 --- a/tests/NuGetGallery.Facts/Authentication/ForceSslWhenAuthenticatedMiddlewareFacts.cs +++ b/tests/NuGetGallery.Facts/Authentication/ForceSslWhenAuthenticatedMiddlewareFacts.cs @@ -94,8 +94,10 @@ public async Task GivenNoForceSslCookieAndNonSslRequest_ItPassesThrough() next.Verify(n => n.Invoke(It.IsAny())); } - [Fact] - public async Task GivenNextMiddlewareGrantsAuth_ItDropsForceSslCookie() + [Theory] + [InlineData("http", false)] + [InlineData("https", true)] + public async Task GivenNextMiddlewareGrantsAuth_ItDropsForceSslCookie(string protocol, bool secure) { // Arrange var context = Fakes.CreateOwinContext(); @@ -110,14 +112,14 @@ public async Task GivenNextMiddlewareGrantsAuth_ItDropsForceSslCookie() return Task.FromResult(null); }); context.Request - .SetUrl("http://nuget.local/foo/bar/baz?qux=qooz"); + .SetUrl(protocol + "://nuget.local/foo/bar/baz?qux=qooz"); var middleware = new ForceSslWhenAuthenticatedMiddleware(next.Object, app, "ForceSSL", 443); // Act await middleware.Invoke(context); // Assert - OwinAssert.SetsCookie(context.Response, "ForceSSL", "true"); + OwinAssert.SetsCookie(context.Response, "ForceSSL", "true", secure); } [Fact] diff --git a/tests/NuGetGallery.Facts/Authentication/Providers/ApiKey/ApiKeyAuthenticationHandlerFacts.cs b/tests/NuGetGallery.Facts/Authentication/Providers/ApiKey/ApiKeyAuthenticationHandlerFacts.cs index 410aa2d810..3a90f284a9 100644 --- a/tests/NuGetGallery.Facts/Authentication/Providers/ApiKey/ApiKeyAuthenticationHandlerFacts.cs +++ b/tests/NuGetGallery.Facts/Authentication/Providers/ApiKey/ApiKeyAuthenticationHandlerFacts.cs @@ -8,6 +8,7 @@ using Microsoft.Owin.Security; using Moq; using NuGetGallery.Framework; +using NuGetGallery.Infrastructure.Authentication; using Xunit; namespace NuGetGallery.Authentication.Providers.ApiKey @@ -193,33 +194,35 @@ public async Task GivenNoUserMatchingApiKey_ItReturnsNull() public async Task GivenMatchingApiKey_ItReturnsTicketWithUserNameAndRoles() { // Arrange - Guid apiKey = Guid.NewGuid(); - var user = new User() { Username = "theUser", EmailAddress = "confirmed@example.com" }; - TestableApiKeyAuthenticationHandler handler = await TestableApiKeyAuthenticationHandler.CreateAsync(new ApiKeyAuthenticationOptions()); + var user = new User { Username = "theUser", EmailAddress = "confirmed@example.com" }; + var handler = await TestableApiKeyAuthenticationHandler.CreateAsync(new ApiKeyAuthenticationOptions()); + var apiKeyCredential = new CredentialBuilder().CreateApiKey(Fakes.ExpirationForApiKeyV1); + handler.OwinContext.Request.Headers.Set( Constants.ApiKeyHeaderName, - apiKey.ToString().ToLowerInvariant()); - handler.MockAuth.SetupAuth(CredentialBuilder.CreateV1ApiKey(apiKey), user); + apiKeyCredential.Value.ToLowerInvariant()); + handler.MockAuth.SetupAuth(apiKeyCredential, user); // Act var ticket = await handler.InvokeAuthenticateCoreAsync(); // Assert Assert.NotNull(ticket); - Assert.Equal(apiKey.ToString().ToLower(), ticket.Identity.GetClaimOrDefault(NuGetClaims.ApiKey)); + Assert.Equal(apiKeyCredential.Value.ToLower(), ticket.Identity.GetClaimOrDefault(NuGetClaims.ApiKey)); } [Fact] public async Task GivenMatchingApiKey_ItSetsUserInOwinEnvironment() { // Arrange - Guid apiKey = Guid.NewGuid(); - var user = new User() { Username = "theUser", EmailAddress = "confirmed@example.com" }; + var user = new User { Username = "theUser", EmailAddress = "confirmed@example.com" }; TestableApiKeyAuthenticationHandler handler = await TestableApiKeyAuthenticationHandler.CreateAsync(new ApiKeyAuthenticationOptions()); + var apiKeyCredential = new CredentialBuilder().CreateApiKey(Fakes.ExpirationForApiKeyV1); + handler.OwinContext.Request.Headers.Set( Constants.ApiKeyHeaderName, - apiKey.ToString().ToLowerInvariant()); - handler.MockAuth.SetupAuth(CredentialBuilder.CreateV1ApiKey(apiKey), user); + apiKeyCredential.Value.ToLowerInvariant()); + handler.MockAuth.SetupAuth(apiKeyCredential, user); // Act await handler.InvokeAuthenticateCoreAsync(); @@ -242,6 +245,7 @@ private TestableApiKeyAuthenticationHandler() { Logger = (MockLogger = new Mock()).Object; Auth = (MockAuth = new Mock()).Object; + CredentialBuilder = new CredentialBuilder(); } public static Task CreateAsync() diff --git a/tests/NuGetGallery.Facts/Authentication/TestCredentialHelper.cs b/tests/NuGetGallery.Facts/Authentication/TestCredentialHelper.cs new file mode 100644 index 0000000000..7eb2604db1 --- /dev/null +++ b/tests/NuGetGallery.Facts/Authentication/TestCredentialHelper.cs @@ -0,0 +1,54 @@ +// 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.Web.Helpers; +using NuGetGallery.Services.Authentication; + +namespace NuGetGallery.Authentication +{ + /// + /// Builds all kinds of supported credentials for test purposes. + /// + public class TestCredentialHelper + { + public static Credential CreatePbkdf2Password(string plaintextPassword) + { + return new Credential( + CredentialTypes.Password.Pbkdf2, + Crypto.HashPassword(plaintextPassword)); + } + + public static Credential CreateSha1Password(string plaintextPassword) + { + return new Credential( + CredentialTypes.Password.Sha1, + LegacyHasher.GenerateHash(plaintextPassword, Constants.Sha1HashAlgorithmId)); + } + + public static Credential CreateV1ApiKey(Guid apiKey, TimeSpan? expiration) + { + return CreateApiKey(CredentialTypes.ApiKey.V1, apiKey.ToString(), expiration); + } + + public static Credential CreateV2ApiKey(Guid apiKey, TimeSpan? expiration) + { + return CreateApiKey(CredentialTypes.ApiKey.V2, apiKey.ToString(), expiration); + } + + public static Credential CreateV2VerificationApiKey(Guid apiKey) + { + return CreateApiKey(CredentialTypes.ApiKey.VerifyV1, apiKey.ToString(), TimeSpan.FromDays(1)); + } + + public static Credential CreateExternalCredential(string value) + { + return new Credential { Type = CredentialTypes.ExternalPrefix + "MicrosoftAccount", Value = value }; + } + + internal static Credential CreateApiKey(string type, string apiKey, TimeSpan? expiration) + { + return new Credential(type, apiKey.ToLowerInvariant(), expiration: expiration); + } + } +} diff --git a/tests/NuGetGallery.Facts/Authentication/V3HasherTests.cs b/tests/NuGetGallery.Facts/Authentication/V3HasherTests.cs new file mode 100644 index 0000000000..c7700f7f25 --- /dev/null +++ b/tests/NuGetGallery.Facts/Authentication/V3HasherTests.cs @@ -0,0 +1,95 @@ +// 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.Diagnostics; +using NuGetGallery.Services.Authentication; +using Xunit; + +namespace NuGetGallery.Authentication +{ + public class V3HasherTests + { + [Fact] + public void WhenStringIsHashedItCanBeVerified() + { + // Arrange + string password = "arew234235wfsdq2321edfewt"; + + // Act + string hash = V3Hasher.GenerateHash(password); + + // Assert + Assert.True(V3Hasher.VerifyHash(hash, password)); + } + + [Fact] + public void WhenWrongStringIsVerifiedThenVerificationFails() + { + // Arrange + string password = "arew234235wfsdq2321edfewt"; + string hash = V3Hasher.GenerateHash(password); + + // Act + bool verify = V3Hasher.VerifyHash(hash, password+"1"); + + // Assert + Assert.False(verify); + } + + [Fact] + public void WhenHashIsInvalidVerificationFails() + { + // Arrange + string password = "arew234235wfsdq2321edfewt"; + string hash = V3Hasher.GenerateHash(password); + byte[] badHash = Convert.FromBase64String(hash); + badHash[0] = 0x0; + + // Act + // The first bit should be 0x01 in the algorithm we use. Make sure we fail if it's not. + bool verify = V3Hasher.VerifyHash(Convert.ToBase64String(badHash), password); + + // Assert + Assert.False(verify); + } + + [Fact(Skip = "This test is not deterministic. Lets not run it as part of CI")] + public void ProcessingTimesForSuccessfulAuthAndFailedAuthAreSimilar() + { + // Arrange + double allowedDiffPercent = 0.05; + int repetitions = 1000; + + string password = "arew234235wfsdq2321edfewt"; + string hash = V3Hasher.GenerateHash(password); + + // Act + var successStopWatch = new Stopwatch(); + var failureStopWatch = new Stopwatch(); + + successStopWatch.Start(); + + for (int i = 0; i < repetitions; i++) + { + V3Hasher.VerifyHash(hash, password); + } + + successStopWatch.Stop(); + + failureStopWatch.Start(); + + for (int i = 0; i < repetitions; i++) + { + V3Hasher.VerifyHash(hash, password + "1"); + } + + failureStopWatch.Stop(); + + double diffPercent = ((double)successStopWatch.ElapsedTicks - (double)failureStopWatch.ElapsedTicks)/ + (double)successStopWatch.ElapsedTicks; + + Assert.True(Math.Abs(diffPercent) < allowedDiffPercent); + } + } +} diff --git a/tests/NuGetGallery.Facts/AutocompleteServicePackageIdsQueryFacts.cs b/tests/NuGetGallery.Facts/AutocompleteServicePackageIdsQueryFacts.cs index 236332871b..30b0940fe5 100644 --- a/tests/NuGetGallery.Facts/AutocompleteServicePackageIdsQueryFacts.cs +++ b/tests/NuGetGallery.Facts/AutocompleteServicePackageIdsQueryFacts.cs @@ -23,27 +23,24 @@ private IAppConfiguration GetConfiguration() [Fact] public async Task ExecuteReturns30ResultsForEmptyQuery() { - var query = new AutocompleteServicePackageIdsQuery(GetConfiguration()); + var query = new AutoCompleteServicePackageIdsQuery(GetConfiguration()); var result = await query.Execute("", false); - Assert.NotEmpty(result); Assert.True(result.Count() == 30); } [Fact] public async Task ExecuteReturns30ResultsForNullQuery() { - var query = new AutocompleteServicePackageIdsQuery(GetConfiguration()); + var query = new AutoCompleteServicePackageIdsQuery(GetConfiguration()); var result = await query.Execute(null, false); - Assert.NotEmpty(result); Assert.True(result.Count() == 30); } [Fact] public async Task ExecuteReturnsResultsForSpecificQuery() { - var query = new AutocompleteServicePackageIdsQuery(GetConfiguration()); + var query = new AutoCompleteServicePackageIdsQuery(GetConfiguration()); var result = await query.Execute("jquery", false); - Assert.NotEmpty(result); Assert.Contains("jquery", result, StringComparer.OrdinalIgnoreCase); } } diff --git a/tests/NuGetGallery.Facts/AutocompleteServicePackageVersionsQueryFacts.cs b/tests/NuGetGallery.Facts/AutocompleteServicePackageVersionsQueryFacts.cs index f308609fcb..b5e41ba768 100644 --- a/tests/NuGetGallery.Facts/AutocompleteServicePackageVersionsQueryFacts.cs +++ b/tests/NuGetGallery.Facts/AutocompleteServicePackageVersionsQueryFacts.cs @@ -23,16 +23,15 @@ private IAppConfiguration GetConfiguration() [Fact] public async Task ExecuteThrowsForEmptyId() { - var query = new AutocompleteServicePackageVersionsQuery(GetConfiguration()); + var query = new AutoCompleteServicePackageVersionsQuery(GetConfiguration()); await Assert.ThrowsAsync(async () => await query.Execute("", false)); } [Fact] public async Task ExecuteReturnsResultsForSpecificQuery() { - var query = new AutocompleteServicePackageVersionsQuery(GetConfiguration()); + var query = new AutoCompleteServicePackageVersionsQuery(GetConfiguration()); var result = await query.Execute("newtonsoft.json", false); - Assert.NotEmpty(result); Assert.True(result.Any()); } } diff --git a/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs index 3369cd7945..ee6f312aa0 100644 --- a/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs @@ -6,6 +6,7 @@ using System.Collections.Specialized; using System.Data; using System.IO; +using System.Linq; using System.Net; using System.Threading.Tasks; using System.Web; @@ -14,9 +15,14 @@ using Moq; using Newtonsoft.Json.Linq; using NuGet.Packaging; +using NuGetGallery.Auditing; +using NuGetGallery.Authentication; +using NuGetGallery.Configuration; using NuGetGallery.Framework; +using NuGetGallery.Infrastructure.Authentication; using NuGetGallery.Packaging; using Xunit; +using System.Globalization; namespace NuGetGallery { @@ -32,12 +38,15 @@ class TestableApiController : ApiController public Mock MockIndexingService { get; private set; } public Mock MockAutoCuratePackage { get; private set; } public Mock MockMessageService { get; private set; } + public Mock MockConfigurationService { get; private set; } + public Mock MockTelemetryService { get; private set; } + public Mock MockAuthenticationService { get; private set; } private Stream PackageFromInputStream { get; set; } public TestableApiController(MockBehavior behavior = MockBehavior.Default) { - OwinContext = Fakes.CreateOwinContext(); + SetOwinContextOverride(Fakes.CreateOwinContext()); EntitiesContext = (MockEntitiesContext = new Mock()).Object; PackageService = (MockPackageService = new Mock(behavior)).Object; UserService = (MockUserService = new Mock(behavior)).Object; @@ -46,6 +55,9 @@ public TestableApiController(MockBehavior behavior = MockBehavior.Default) StatisticsService = (MockStatisticsService = new Mock()).Object; IndexingService = (MockIndexingService = new Mock()).Object; AutoCuratePackage = (MockAutoCuratePackage = new Mock()).Object; + AuthenticationService = (MockAuthenticationService = new Mock()).Object; + + CredentialBuilder = new CredentialBuilder(); MockPackageFileService = new Mock(MockBehavior.Strict); MockPackageFileService.Setup(p => p.SavePackageFileAsync(It.IsAny(), It.IsAny())) @@ -54,6 +66,28 @@ public TestableApiController(MockBehavior behavior = MockBehavior.Default) MessageService = (MockMessageService = new Mock()).Object; + MockConfigurationService = new Mock(); + MockConfigurationService.SetupGet(s => s.Features.TrackPackageDownloadCountInLocalDatabase) + .Returns(false); + ConfigurationService = MockConfigurationService.Object; + + AuditingService = new TestAuditingService(); + + MockTelemetryService = new Mock(); + TelemetryService = MockTelemetryService.Object; + + MockPackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())). + Returns((PackageArchiveReader nugetPackage, PackageStreamMetadata packageStreamMetadata, User user, bool commitChanges) => + { + var packageMetadata = PackageMetadata.FromNuspecReader(nugetPackage.GetNuspecReader()); + + var package = new Package(); + package.PackageRegistration = new PackageRegistration { Id = packageMetadata.Id }; + package.Version = packageMetadata.Version.ToString(); + + return Task.FromResult(package); + }); + TestUtility.SetupHttpContextMockForUrlGeneration(new Mock(), this); } @@ -107,6 +141,81 @@ public async Task CreatePackageWillSavePackageFileToFileStorage() controller.MockPackageFileService.Verify(); } + [Fact] + public async Task WillDeletePackageFileFromBlobStorageIfSavingDbChangesFails() + { + // Arrange + var user = new User() { EmailAddress = "confirmed@email.com" }; + var packageId = "theId"; + var packageVersion = "1.0.42"; + var packageRegistration = new PackageRegistration(); + packageRegistration.Id = packageId; + packageRegistration.Owners.Add(user); + var package = new Package(); + package.PackageRegistration = packageRegistration; + package.Version = "1.0.42"; + packageRegistration.Packages.Add(package); + + var controller = new TestableApiController(); + controller.SetCurrentUser(user); + controller.MockPackageFileService.Setup( + p => p.SavePackageFileAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask).Verifiable(); + controller.MockPackageFileService.Setup( + p => + p.DeletePackageFileAsync(packageId, + packageVersion)) + .Returns(Task.CompletedTask).Verifiable(); + controller.MockPackageService.Setup(p => p.FindPackageRegistrationById(It.IsAny())) + .Returns(packageRegistration); + controller.MockPackageService.Setup( + p => + p.CreatePackageAsync(It.IsAny(), It.IsAny(), + It.IsAny(), false)) + .Returns(Task.FromResult(package)); + controller.MockEntitiesContext.Setup(e => e.SaveChangesAsync()).Throws(); + + var nuGetPackage = TestPackage.CreateTestPackageStream(packageId, "1.0.42"); + controller.SetupPackageFromInputStream(nuGetPackage); + + // Act + await Assert.ThrowsAsync(async () => await controller.CreatePackagePut()); + + // Assert + controller.MockPackageFileService.Verify(); + } + + [Fact] + public async Task WritesAnAuditRecord() + { + // Arrange + var user = new User { EmailAddress = "confirmed@email.com" }; + var packageRegistration = new PackageRegistration(); + packageRegistration.Id = "theId"; + packageRegistration.Owners.Add(user); + var package = new Package(); + package.PackageRegistration = packageRegistration; + package.Version = "1.0.42"; + packageRegistration.Packages.Add(package); + + var controller = new TestableApiController(); + controller.SetCurrentUser(user); + controller.MockPackageService.Setup(p => p.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), false)) + .Returns(Task.FromResult(package)); + + var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); + controller.SetupPackageFromInputStream(nuGetPackage); + + // Act + await controller.CreatePackagePut(); + + // Assert + Assert.True(controller.AuditingService.WroteRecord(ar => + ar.Action == AuditedPackageAction.Create + && ar.Id == package.PackageRegistration.Id + && ar.Version == package.Version)); + } + [Fact] public async Task CreatePackageWillSendPackageAddedNotice() { @@ -138,7 +247,7 @@ public async Task CreatePackageWillSendPackageAddedNotice() } [Fact] - public async Task CreatePackageWillReturn400IfPackageIsInvalid() + public async Task CreatePackageWillReturn400IfFileIsNotANuGetPackage() { // Arrange var user = new User() { EmailAddress = "confirmed@email.com" }; @@ -162,6 +271,93 @@ public async Task CreatePackageWillReturn400IfPackageIsInvalid() ResultAssert.IsStatusCode(result, HttpStatusCode.BadRequest); } + private const string EnsureValidExceptionMessage = "naughty package"; + + [Theory] + [InlineData(typeof(InvalidPackageException), true)] + [InlineData(typeof(InvalidDataException), true)] + [InlineData(typeof(EntityException), true)] + [InlineData(typeof(Exception), false)] + public async Task CreatePackageReturns400IfEnsureValidThrowsExceptionMessage(Type exceptionType, bool expectExceptionMessageInResponse) + { + // Arrange + var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); + + var user = new User() { EmailAddress = "confirmed@email.com" }; + var controller = new TestableApiController(); + controller.SetCurrentUser(user); + controller.SetupPackageFromInputStream(nuGetPackage); + + var exception = + exceptionType.GetConstructor(new[] { typeof(string) }).Invoke(new[] { EnsureValidExceptionMessage }); + + controller.MockPackageService.Setup(p => p.EnsureValid(It.IsAny())) + .Throws(exception as Exception); + + // Act + ActionResult result = await controller.CreatePackagePut(); + + // Assert + ResultAssert.IsStatusCode(result, HttpStatusCode.BadRequest); + Assert.Equal(expectExceptionMessageInResponse ? EnsureValidExceptionMessage : Strings.FailedToReadUploadFile, (result as HttpStatusCodeWithBodyResult).StatusDescription); + } + + [Theory] + [InlineData("ILike*Asterisks")] + [InlineData("I_.Like.-Separators")] + [InlineData("-StartWithSeparator")] + [InlineData("EndWithSeparator.")] + [InlineData("EndsWithHyphen-")] + [InlineData("$id$")] + [InlineData("Contains#Invalid$Characters!@#$%^&*")] + [InlineData("Contains#Invalid$Characters!@#$%^&*EndsOnValidCharacter")] + public async Task CreatePackageReturns400IfPackageIdIsInvalid(string packageId) + { + // Arrange + var nuGetPackage = TestPackage.CreateTestPackageStream(packageId, "1.0.42"); + + var user = new User() { EmailAddress = "confirmed@email.com" }; + var controller = new TestableApiController(); + controller.SetCurrentUser(user); + controller.SetupPackageFromInputStream(nuGetPackage); + + // Act + ActionResult result = await controller.CreatePackagePut(); + + // Assert + ResultAssert.IsStatusCode(result, HttpStatusCode.BadRequest); + } + + [Fact] + public async Task WillWriteAnAuditRecordIfUserIsNotPackageOwner() + { + // Arrange + var user = new User { EmailAddress = "confirmed@email.com" }; + var packageRegistration = new PackageRegistration(); + packageRegistration.Id = "theId"; + var package = new Package(); + package.PackageRegistration = packageRegistration; + package.Version = "1.0.42"; + packageRegistration.Packages.Add(package); + + var controller = new TestableApiController(); + controller.SetCurrentUser(user); + controller.MockPackageService.Setup(p => p.FindPackageRegistrationById(It.IsAny())) + .Returns(packageRegistration); + + var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); + controller.SetupPackageFromInputStream(nuGetPackage); + + // Act + await controller.CreatePackagePut(); + + // Assert + Assert.True(controller.AuditingService.WroteRecord(ar => + ar.Action == AuditedAuthenticatedOperationAction.PackagePushAttemptByNonOwner + && ar.AttemptedPackage.Id == package.PackageRegistration.Id + && ar.AttemptedPackage.Version == package.Version)); + } + [Fact] public async Task WillReturnConflictIfAPackageWithTheIdAndSameNormalizedVersionAlreadyExists() { @@ -191,7 +387,66 @@ public async Task WillReturnConflictIfAPackageWithTheIdAndSameNormalizedVersionA } [Fact] - public void WillCreateAPackageFromTheNuGetPackage() + public async Task WillReturnConflictIfAPackageWithTheIdExistsBelongingToAnotherUser() + { + // Arrange + var user = new User { EmailAddress = "confirmed@email.com" }; + var packageId = "theId"; + var packageRegistration = new PackageRegistration(); + packageRegistration.Id = packageId; + var package = new Package(); + package.PackageRegistration = packageRegistration; + package.Version = "1.0.42"; + packageRegistration.Packages.Add(package); + + var controller = new TestableApiController(); + controller.SetCurrentUser(user); + controller.MockPackageService.Setup(p => p.FindPackageRegistrationById(It.IsAny())) + .Returns(packageRegistration); + + var nuGetPackage = TestPackage.CreateTestPackageStream(packageId, "1.0.42"); + controller.SetCurrentUser(new User()); + controller.SetupPackageFromInputStream(nuGetPackage); + + // Act + var result = await controller.CreatePackagePut(); + + // Assert + ResultAssert.IsStatusCode( + result, + HttpStatusCode.Conflict, + String.Format(Strings.PackageIdNotAvailable, packageId)); + } + + [Fact] + public async Task WillReturnConflictIfSavingPackageBlobFailsOnConflict() + { + // Arrange + var user = new User { EmailAddress = "confirmed@email.com" }; + var controller = new TestableApiController(); + controller.SetCurrentUser(user); + controller.MockPackageFileService.Setup( + x => x.SavePackageFileAsync(It.IsAny(), It.IsAny())) + .Throws(); + + var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); + controller.SetCurrentUser(new User()); + controller.SetupPackageFromInputStream(nuGetPackage); + + // Act + var result = await controller.CreatePackagePut(); + + // Assert + ResultAssert.IsStatusCode( + result, + HttpStatusCode.Conflict, + Strings.UploadPackage_IdVersionConflict); + + controller.MockEntitiesContext.VerifyCommitted(Times.Never()); + } + + [Fact] + public async Task WillCreateAPackageFromTheNuGetPackage() { var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); @@ -200,14 +455,14 @@ public void WillCreateAPackageFromTheNuGetPackage() controller.SetCurrentUser(user); controller.SetupPackageFromInputStream(nuGetPackage); - controller.CreatePackagePut(); + await controller.CreatePackagePut(); controller.MockPackageService.Verify(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), false)); controller.MockEntitiesContext.VerifyCommitted(); } [Fact] - public void WillCurateThePackage() + public async Task WillCurateThePackage() { var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); @@ -216,14 +471,14 @@ public void WillCurateThePackage() controller.SetCurrentUser(user); controller.SetupPackageFromInputStream(nuGetPackage); - controller.CreatePackagePut(); + await controller.CreatePackagePut(); controller.MockAutoCuratePackage.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny(), false)); controller.MockEntitiesContext.VerifyCommitted(); } [Fact] - public void WillCurateThePackageViaApi() + public async Task WillCurateThePackageViaApi() { var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); @@ -232,14 +487,14 @@ public void WillCurateThePackageViaApi() controller.SetCurrentUser(user); controller.SetupPackageFromInputStream(nuGetPackage); - controller.CreatePackagePost(); + await controller.CreatePackagePost(); controller.MockAutoCuratePackage.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny(), false)); controller.MockEntitiesContext.VerifyCommitted(); } [Fact] - public void WillCreateAPackageWithTheUserMatchingTheApiKey() + public async Task WillCreateAPackageWithTheUserMatchingTheApiKey() { var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); @@ -248,11 +503,136 @@ public void WillCreateAPackageWithTheUserMatchingTheApiKey() controller.SetCurrentUser(user); controller.SetupPackageFromInputStream(nuGetPackage); - controller.CreatePackagePut(); + await controller.CreatePackagePut(); controller.MockPackageService.Verify(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), user, false)); controller.MockEntitiesContext.VerifyCommitted(); } + + [InlineData("[{\"a\":\"package:push\", \"s\":\"theId\"}]", true)] + [InlineData("[{\"a\":\"package:push\", \"s\":\"*\"}]", true)] + [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"theId\"}]", false)] + [InlineData("[{\"a\":\"package:push\", \"s\":\"cbd\"}]", false)] + [Theory] + public async Task WillVerifyScopesForNewPackageId(string apiKeyScopes, bool isPushAllowed) + { + // Arrange + var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); + + var user = new User {EmailAddress = "confirmed@email.com", Username = "username"}; + + var package = new Package(); + package.PackageRegistration = new PackageRegistration(); + package.Version = "1.0.42"; + + var controller = new TestableApiController(); + controller.SetCurrentUser(user, apiKeyScopes); + controller.SetupPackageFromInputStream(nuGetPackage); + controller.MockPackageService.Setup( + x => + x.CreatePackageAsync(It.IsAny(), It.IsAny(), user, + It.IsAny())).Returns(Task.FromResult(package)); + + // Act + var result = await controller.CreatePackagePut(); + + // Assert + if (isPushAllowed) + { + controller.MockPackageService.Verify( + x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), user, false)); + controller.MockEntitiesContext.VerifyCommitted(); + } + else + { + controller.MockPackageService.Verify( + x => x.CreatePackageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + + ResultAssert.IsStatusCode( + result, + HttpStatusCode.Unauthorized, + Strings.ApiKeyNotAuthorized); + } + } + + [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"differentid\"}]", false)] + [InlineData("[{\"a\":\"package:push\", \"s\":\"theId\"}]", true)] + [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"theId\"}]", true)] + [Theory] + public async Task WillVerifyScopesForExistingPackageId(string apiKeyScopes, bool isPushAllowed) + { + // Arrange + const string packageId = "theId"; + + var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); + + var user = new User { EmailAddress = "confirmed@email.com", Username = "username", Key = 1 }; + + var controller = new TestableApiController(); + controller.SetCurrentUser(user, apiKeyScopes); + controller.SetupPackageFromInputStream(nuGetPackage); + + var packageRegistration = new PackageRegistration(); + packageRegistration.Id = packageId; + packageRegistration.Owners.Add(user); + + var package = new Package(); + package.PackageRegistration = packageRegistration; + package.Version = "1.0.42"; + + controller.MockPackageService.Setup(x => x.FindPackageRegistrationById(packageId)) + .Returns(packageRegistration); + controller.MockPackageService.Setup( + x => + x.CreatePackageAsync(It.IsAny(), It.IsAny(), user, + It.IsAny())).Returns(Task.FromResult(package)); + + // Act + var result = await controller.CreatePackagePut(); + + // Assert + if (isPushAllowed) + { + controller.MockPackageService.Verify( + x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), user, false)); + controller.MockEntitiesContext.VerifyCommitted(); + } + else + { + controller.MockPackageService.Verify( + x => x.CreatePackageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + + ResultAssert.IsStatusCode( + result, + HttpStatusCode.Unauthorized, + Strings.ApiKeyNotAuthorized); + } + } + + [Fact] + public async Task WillSendPackagePushEvent() + { + var nuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.42"); + + var user = new User() { EmailAddress = "confirmed@email.com" }; + var controller = new TestableApiController(); + controller.SetCurrentUser(user); + controller.SetupPackageFromInputStream(nuGetPackage); + + await controller.CreatePackagePut(); + + controller.MockTelemetryService.Verify(x => x.TrackPackagePushEvent(It.IsAny(), user, controller.OwinContext.Request.User.Identity), Times.Once); + } } public class TheDeletePackageAction @@ -290,11 +670,46 @@ public async Task WillNotDeleteThePackageIfApiKeyDoesNotBelongToAnOwner() Assert.IsType(result); var statusCodeResult = (HttpStatusCodeWithBodyResult)result; - Assert.Equal(String.Format(Strings.ApiKeyNotAuthorized, "delete"), statusCodeResult.StatusDescription); + Assert.Equal(string.Format(Strings.ApiKeyNotAuthorized, "delete"), statusCodeResult.StatusDescription); controller.MockPackageService.Verify(x => x.MarkPackageUnlistedAsync(package, true), Times.Never()); } + [InlineData("[{\"a\":\"all\", \"s\":\"*\"}]", true)] + [InlineData("[{\"a\":\"package:unlist\", \"s\":\"theId\"}]", true)] + [InlineData("[{\"a\":\"package:push\", \"s\":\"theId\"}]", false)] + [Theory] + public async Task WillVerifyApiKeyScopeBeforeDelete(string apiKeyScope, bool isDeleteAllowed) + { + var owner = new User {Key = 1, Username = "owner"}; + var package = new Package + { + PackageRegistration = new PackageRegistration {Owners = new[] {owner}} + }; + + var controller = new TestableApiController(); + controller.SetCurrentUser(owner, apiKeyScope); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)) + .Returns(package); + + var result = await controller.DeletePackage("theId", "1.0.42"); + + if (!isDeleteAllowed) + { + Assert.IsType(result); + var statusCodeResult = (HttpStatusCodeWithBodyResult) result; + Assert.Equal(string.Format(Strings.ApiKeyNotAuthorized, "delete"), + statusCodeResult.StatusDescription); + + controller.MockPackageService.Verify(x => x.MarkPackageUnlistedAsync(package, true), Times.Never()); + } + else + { + controller.MockPackageService.Verify(x => x.MarkPackageUnlistedAsync(package, true)); + controller.MockIndexingService.Verify(i => i.UpdatePackage(package)); + } + } + [Fact] public async Task WillUnlistThePackageIfApiKeyBelongsToAnOwner() { @@ -478,7 +893,7 @@ public async Task GetPackageReturns503IfNoVersionIsProvidedAndDatabaseUnavailabl var package = new Package(); var actionResult = new EmptyResult(); var controller = new TestableApiController(MockBehavior.Strict); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("Baz", "", false)).Throws(new DataException("Oh noes, database broked!")); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("Baz", "", false)).Throws(new DataException("Oh noes, database broken!")); controller.MockPackageFileService.Setup(s => s.CreateDownloadPackageActionResultAsync(HttpRequestUrl, packageId, package.NormalizedVersion)) .Returns(Task.FromResult(actionResult)) .Verifiable(); @@ -579,79 +994,293 @@ public async Task WillListThePackageIfUserIsAnOwner() } } - public class TheVerifyPackageKeyAction : TestContainer + public class PackageVerificationKeyContainer : TestContainer { - [Fact] - public void VerifyPackageKeyReturnsEmptyResultIfApiKeyExistsButIdAndVersionAreEmpty() + internal TestableApiController SetupController(string keyType, string scopes, Package package, bool isOwner = true) { - // Arrange + var credential = new Credential(keyType, string.Empty, TimeSpan.FromDays(1)); + if (!string.IsNullOrWhiteSpace(scopes)) + { + credential.Scopes.AddRange(Newtonsoft.Json.JsonConvert.DeserializeObject>(scopes)); + } + + var user = Get().CreateUser("testuser"); + user.Credentials.Add(credential); + + if (package != null && isOwner) + { + package.PackageRegistration.Owners.Add(user); + } + var controller = new TestableApiController(); - controller.SetCurrentUser(new User()); + + controller.MockAuthenticationService + .Setup(s => s.AddCredential(It.IsAny(), It.IsAny())) + .Callback((u, c) => u.Credentials.Add(c)) + .Returns(Task.CompletedTask); + + controller.MockAuthenticationService + .Setup(s => s.RemoveCredential(It.IsAny(), It.IsAny())) + .Callback((u, c) => u.Credentials.Remove(c)) + .Returns(Task.CompletedTask); + + var id = package?.PackageRegistration?.Id ?? "foo"; + var version = package?.Version ?? "1.0.0"; + controller.MockPackageService.Setup(s => s.FindPackageByIdAndVersion(id, version, true)).Returns(package); + + controller.SetCurrentUser(user, scopes); + + return controller; + } + } + + public class TheCreatePackageVerificationKeyAction : PackageVerificationKeyContainer + { + [Theory] + [InlineData("")] + [InlineData("[{\"a\":\"package:push\", \"s\":\"foo\"}]")] + [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"foo\"}]")] + public async void CreatePackageKeyReturnsPackageVerificationKey(string scope) + { + // Arrange + var controller = SetupController(CredentialTypes.ApiKey.V2, scope, package: null); // Act - var result = controller.VerifyPackageKey(null, null); + var jsonResult = await controller.CreatePackageVerificationKeyAsync("foo", "1.0.0") as JsonResult; // Assert - ResultAssert.IsEmpty(result); + dynamic json = jsonResult?.Data; + Assert.NotNull(json); + + Guid key; + Assert.True(Guid.TryParse(json.Key, out key)); + + DateTime expires; + Assert.True(DateTime.TryParse(json.Expires, out expires)); + + controller.MockAuthenticationService.Verify(s => s.AddCredential(It.IsAny(), It.IsAny()), Times.Once); + + controller.MockTelemetryService.Verify(x => x.TrackCreatePackageVerificationKeyEvent("foo", "1.0.0", + It.IsAny(), controller.OwinContext.Request.User.Identity), Times.Once); } + } - [Fact] - public void VerifyPackageKeyReturns404IfPackageDoesNotExist() + public class TheVerifyPackageKeyAction : PackageVerificationKeyContainer + { + [Theory] + [InlineData("")] + [InlineData("[{\"a\":\"package:push\", \"s\":\"foo\"}]")] + [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"foo\"}]")] + public async void VerifyPackageKeyReturns404IfPackageDoesNotExist_ApiKeyV2(string scope) { // Arrange - var user = new User { EmailAddress = "confirmed@email.com" }; - GetMock() - .Setup(s => s.FindPackageByIdAndVersion("foo", "1.0.0", true)) - .ReturnsNull(); - var controller = GetController(); - controller.SetCurrentUser(user); + var controller = SetupController(CredentialTypes.ApiKey.V2, scope, package: null); + + // Act + var result = await controller.VerifyPackageKeyAsync("foo", "1.0.0"); + + // Assert + ResultAssert.IsStatusCode( + result, + HttpStatusCode.NotFound, + String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, "foo", "1.0.0")); + + controller.MockAuthenticationService.Verify(s => s.RemoveCredential(It.IsAny(), It.IsAny()), Times.Never); + + controller.MockTelemetryService.Verify(x => x.TrackVerifyPackageKeyEvent("foo", "1.0.0", + It.IsAny(), controller.OwinContext.Request.User.Identity, 404), Times.Once); + } + + [Theory] + [InlineData("[{\"a\":\"package:verify\", \"s\":\"foo\"}]")] + public async void VerifyPackageKeyReturns404IfPackageDoesNotExist_ApiKeyVerifyV1(string scope) + { + // Arrange + var controller = SetupController(CredentialTypes.ApiKey.VerifyV1, scope, package: null); // Act - var result = controller.VerifyPackageKey("foo", "1.0.0"); + var result = await controller.VerifyPackageKeyAsync("foo", "1.0.0"); // Assert ResultAssert.IsStatusCode( result, HttpStatusCode.NotFound, - "A package with id 'foo' and version '1.0.0' does not exist."); + String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, "foo", "1.0.0")); + + controller.MockAuthenticationService.Verify(s => s.RemoveCredential(It.IsAny(), It.IsAny()), Times.Once); + + controller.MockTelemetryService.Verify(x => x.TrackVerifyPackageKeyEvent("foo", "1.0.0", + It.IsAny(), controller.OwinContext.Request.User.Identity, 404), Times.Once); } - [Fact] - public void VerifyPackageKeyReturns403IfUserIsNotAnOwner() + [Theory] + [InlineData("")] + [InlineData("[{\"a\":\"package:push\", \"s\":\"foo\"}]")] + [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"foo\"}]")] + public async void VerifyPackageKeyReturns403IfUserIsNotAnOwner_ApiKeyV2(string scope) { // Arrange - var controller = new TestableApiController(); - var nonOwner = new User(); - controller.SetCurrentUser(nonOwner); - controller.MockPackageService.Setup(s => s.FindPackageByIdAndVersion("foo", "1.0.0", true)).Returns( - new Package { PackageRegistration = new PackageRegistration() }); + var package = new Package + { + PackageRegistration = new PackageRegistration() { Id = "foo" }, + Version = "1.0.0" + }; + var controller = SetupController(CredentialTypes.ApiKey.V2, scope, package, isOwner: false); // Act - var result = controller.VerifyPackageKey("foo", "1.0.0"); + var result = await controller.VerifyPackageKeyAsync("foo", "1.0.0"); // Assert ResultAssert.IsStatusCode( result, HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); + + controller.MockAuthenticationService.Verify(s => s.RemoveCredential(It.IsAny(), It.IsAny()), Times.Never); + + controller.MockTelemetryService.Verify(x => x.TrackVerifyPackageKeyEvent("foo", "1.0.0", + It.IsAny(), controller.OwinContext.Request.User.Identity, 403), Times.Once); } - [Fact] - public void VerifyPackageKeyReturns200IfUserIsAnOwner() + [Theory] + [InlineData("[{\"a\":\"package:verify\", \"s\":\"foo\"}]")] + public async void VerifyPackageKeyReturns403IfUserIsNotAnOwner_ApiKeyVerifyV1(string scope) { // Arrange - var user = new User(); - var package = new Package { PackageRegistration = new PackageRegistration() }; - package.PackageRegistration.Owners.Add(user); - var controller = new TestableApiController(); - controller.SetCurrentUser(user); - controller.MockPackageService.Setup(s => s.FindPackageByIdAndVersion("foo", "1.0.0", true)).Returns(package); + var package = new Package + { + PackageRegistration = new PackageRegistration() { Id = "foo" }, + Version = "1.0.0" + }; + var controller = SetupController(CredentialTypes.ApiKey.VerifyV1, scope, package, isOwner: false); + + // Act + var result = await controller.VerifyPackageKeyAsync("foo", "1.0.0"); + + // Assert + ResultAssert.IsStatusCode( + result, + HttpStatusCode.Forbidden, + Strings.ApiKeyNotAuthorized); + + controller.MockAuthenticationService.Verify(s => s.RemoveCredential(It.IsAny(), It.IsAny()), Times.Once); + + controller.MockTelemetryService.Verify(x => x.TrackVerifyPackageKeyEvent("foo", "1.0.0", + It.IsAny(), controller.OwinContext.Request.User.Identity, 403), Times.Once); + } + + [Theory] + // action mismatch + [InlineData("[{\"a\":\"package:unlist\", \"s\":\"foo\"}]")] + [InlineData("[{\"a\":\"package:verify\", \"s\":\"foo\"}]")] + // subject mismatch + [InlineData("[{\"a\":\"package:push\", \"s\":\"notfoo\"}]")] + [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"notfoo\"}]")] + public async void VerifyPackageKeyReturns403IfScopeDoesNotMatch_ApiKeyV2(string scope) + { + // Arrange + var package = new Package + { + PackageRegistration = new PackageRegistration() { Id = "foo" }, + Version = "1.0.0" + }; + var controller = SetupController(CredentialTypes.ApiKey.V2, scope, package); + + // Act + var result = await controller.VerifyPackageKeyAsync("foo", "1.0.0"); + + // Assert + ResultAssert.IsStatusCode( + result, + HttpStatusCode.Forbidden, + Strings.ApiKeyNotAuthorized); + + controller.MockAuthenticationService.Verify(s => s.RemoveCredential(It.IsAny(), It.IsAny()), Times.Never); + + controller.MockTelemetryService.Verify(x => x.TrackVerifyPackageKeyEvent("foo", "1.0.0", + It.IsAny(), controller.OwinContext.Request.User.Identity, 403), Times.Once); + } + + [Theory] + // action mismatch + [InlineData("[{\"a\":\"package:push\", \"s\":\"foo\"}]")] + [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"foo\"}]")] + [InlineData("[{\"a\":\"package:unlist\", \"s\":\"foo\"}]")] + // subject mismatch + [InlineData("[{\"a\":\"package:verify\", \"s\":\"notfoo\"}]")] + public async void VerifyPackageKeyReturns403IfScopeDoesNotMatch_ApiKeyVerifyV1(string scope) + { + // Arrange + var package = new Package + { + PackageRegistration = new PackageRegistration() { Id = "foo" }, + Version = "1.0.0" + }; + var controller = SetupController(CredentialTypes.ApiKey.VerifyV1, scope, package); // Act - var result = controller.VerifyPackageKey("foo", "1.0.0"); + var result = await controller.VerifyPackageKeyAsync("foo", "1.0.0"); + + // Assert + ResultAssert.IsStatusCode( + result, + HttpStatusCode.Forbidden, + Strings.ApiKeyNotAuthorized); + + controller.MockAuthenticationService.Verify(s => s.RemoveCredential(It.IsAny(), It.IsAny()), Times.Once); + + controller.MockTelemetryService.Verify(x => x.TrackVerifyPackageKeyEvent("foo", "1.0.0", + It.IsAny(), controller.OwinContext.Request.User.Identity, 403), Times.Once); + } + + [Theory] + [InlineData("")] + [InlineData("[{\"a\":\"package:push\", \"s\":\"foo\"}]")] + [InlineData("[{\"a\":\"package:pushversion\", \"s\":\"foo\"}]")] + public async void VerifyPackageKeyReturns200_ApiKeyV2(string scope) + { + // Arrange + var package = new Package + { + PackageRegistration = new PackageRegistration() { Id = "foo" }, + Version = "1.0.0" + }; + var controller = SetupController(CredentialTypes.ApiKey.V2, scope, package); + + // Act + var result = await controller.VerifyPackageKeyAsync("foo", "1.0.0"); + + // Assert + ResultAssert.IsEmpty(result); + + controller.MockAuthenticationService.Verify(s => s.RemoveCredential(It.IsAny(), It.IsAny()), Times.Never); + + controller.MockTelemetryService.Verify(x => x.TrackVerifyPackageKeyEvent("foo", "1.0.0", + It.IsAny(), controller.OwinContext.Request.User.Identity, 200), Times.Once); + } + + [Theory] + [InlineData("[{\"a\":\"package:verify\", \"s\":\"foo\"}]")] + public async void VerifyPackageKeyReturns200_ApiKeyVerifyV1(string scope) + { + // Arrange + var package = new Package + { + PackageRegistration = new PackageRegistration() { Id = "foo" }, + Version = "1.0.0" + }; + var controller = SetupController(CredentialTypes.ApiKey.VerifyV1, scope, package); + + // Act + var result = await controller.VerifyPackageKeyAsync("foo", "1.0.0"); // Assert ResultAssert.IsEmpty(result); + + controller.MockAuthenticationService.Verify(s => s.RemoveCredential(It.IsAny(), It.IsAny()), Times.Once); + + controller.MockTelemetryService.Verify(x => x.TrackVerifyPackageKeyEvent("foo", "1.0.0", + It.IsAny(), controller.OwinContext.Request.User.Identity, 200), Times.Once); } } diff --git a/tests/NuGetGallery.Facts/Controllers/AppControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/AppControllerFacts.cs index e8d5e1e073..4ed73119d8 100644 --- a/tests/NuGetGallery.Facts/Controllers/AppControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/AppControllerFacts.cs @@ -15,7 +15,7 @@ public void GivenNoActiveUserPrincipal_ItReturnsNull() { // Arrange var ctrl = new TestableAppController(); - ctrl.OwinContext = Fakes.CreateOwinContext(); + ctrl.SetOwinContextOverride(Fakes.CreateOwinContext()); // Act var user = ctrl.InvokeGetCurrentUser(); diff --git a/tests/NuGetGallery.Facts/Controllers/AuthenticationControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/AuthenticationControllerFacts.cs index 77dc4a6170..919043e798 100644 --- a/tests/NuGetGallery.Facts/Controllers/AuthenticationControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/AuthenticationControllerFacts.cs @@ -15,6 +15,7 @@ using NuGetGallery.Authentication.Providers.AzureActiveDirectory; using NuGetGallery.Configuration; using NuGetGallery.Authentication.Providers.MicrosoftAccount; +using NuGetGallery.Infrastructure.Authentication; using Xunit; namespace NuGetGallery.Controllers @@ -28,7 +29,8 @@ public void GivenUserAlreadyAuthenticated_ItRedirectsToReturnUrl() { // Arrange var controller = GetController(); - controller.SetCurrentUser(Fakes.User); + var fakes = Get(); + controller.SetCurrentUser(fakes.User); // Act var result = controller.LogOn("/foo/bar/baz"); @@ -99,7 +101,8 @@ public async Task GivenUserAlreadyAuthenticated_ItRedirectsToReturnUrl() { // Arrange var controller = GetController(); - controller.SetCurrentUser(Fakes.User); + var fakes = Get(); + controller.SetCurrentUser(fakes.User); // Act var result = await controller.SignIn(new LogOnViewModel(), "/foo/bar/baz", linkingAccount: false); @@ -128,7 +131,7 @@ public async Task WillInvalidateModelStateAndShowTheViewWithErrorsWhenTheUsernam { GetMock() .Setup(x => x.Authenticate(It.IsAny(), It.IsAny())) - .CompletesWithNull(); + .CompletesWith(new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.BadCredentials)); var controller = GetController(); var result = await controller.SignIn( @@ -146,13 +149,18 @@ public async Task CanLogTheUserOnWithUserName() // Arrange var authUser = new AuthenticatedUser( new User("theUsername") { EmailAddress = "confirmed@example.com" }, - new Credential() { Type = "Foo" }); + new Credential { Type = "Foo" }); + var authResult = + new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.Success, authUser); + GetMock() .Setup(x => x.Authenticate(authUser.User.Username, "thePassword")) - .CompletesWith(authUser); + .CompletesWith(authResult); + var controller = GetController(); GetMock() - .Setup(a => a.CreateSession(controller.OwinContext, authUser.User)) + .Setup(a => a.CreateSessionAsync(controller.OwinContext, authUser)) + .Returns(Task.FromResult(0)) .Verifiable(); // Act @@ -175,12 +183,16 @@ public async Task CanLogTheUserOnWithEmailAddress() var authUser = new AuthenticatedUser( new User("theUsername") { EmailAddress = "confirmed@example.com" }, new Credential() { Type = "Foo" }); + var authResult = + new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.Success, authUser); + GetMock() .Setup(x => x.Authenticate("confirmed@example.com", "thePassword")) - .CompletesWith(authUser); + .CompletesWith(authResult); var controller = GetController(); GetMock() - .Setup(a => a.CreateSession(controller.OwinContext, authUser.User)) + .Setup(a => a.CreateSessionAsync(controller.OwinContext, authUser)) + .Returns(Task.FromResult(0)) .Verifiable(); // Act @@ -202,13 +214,16 @@ public async Task WillLogTheUserOnWithUsernameEvenWithoutConfirmedEmailAddress() // Arrange var authUser = new AuthenticatedUser( new User("theUsername") { UnconfirmedEmailAddress = "unconfirmed@example.com" }, - new Credential() { Type = "Foo" }); + new Credential { Type = "Foo" }); + var authResult = + new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.Success, authUser); GetMock() .Setup(x => x.Authenticate("confirmed@example.com", "thePassword")) - .CompletesWith(authUser); + .CompletesWith(authResult); var controller = GetController(); GetMock() - .Setup(a => a.CreateSession(controller.OwinContext, authUser.User)) + .Setup(a => a.CreateSessionAsync(controller.OwinContext, authUser)) + .Returns(Task.FromResult(0)) .Verifiable(); // Act @@ -230,11 +245,14 @@ public async Task GivenExpiredExternalAuth_ItRedirectsBackToLogOnWithExternalAut // Arrange var authUser = new AuthenticatedUser( new User("theUsername") { EmailAddress = "confirmed@example.com" }, - new Credential() { Type = "Foo" }); + new Credential { Type = "Foo" }); + + var authResult = + new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.Success, authUser); GetMock() .Setup(x => x.Authenticate(authUser.User.Username, "thePassword")) - .CompletesWith(authUser); + .CompletesWith(authResult); var controller = GetController(); @@ -253,7 +271,7 @@ public async Task GivenExpiredExternalAuth_ItRedirectsBackToLogOnWithExternalAut // Assert VerifyExternalLinkExpiredResult(controller, result); GetMock() - .Verify(x => x.CreateSession(It.IsAny(), It.IsAny()), Times.Never()); + .Verify(x => x.CreateSessionAsync(It.IsAny(), It.IsAny()), Times.Never()); } [Fact] @@ -262,12 +280,14 @@ public async Task GivenValidExternalAuth_ItLinksCredentialSendsEmailAndLogsIn() // Arrange var authUser = new AuthenticatedUser( new User("theUsername") { EmailAddress = "confirmed@example.com" }, - new Credential() { Type = "Foo" }); - var externalCred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); + new Credential { Type = "Foo" }); + var externalCred = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); + var authResult = + new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.Success, authUser); GetMock() .Setup(x => x.Authenticate(authUser.User.Username, "thePassword")) - .CompletesWith(authUser); + .CompletesWith(authResult); GetMock() .Setup(x => x.AddCredential(authUser.User, externalCred)) .Completes() @@ -279,11 +299,12 @@ public async Task GivenValidExternalAuth_ItLinksCredentialSendsEmailAndLogsIn() var controller = GetController(); GetMock() - .Setup(x => x.CreateSession(controller.OwinContext, authUser.User)) + .Setup(x => x.CreateSessionAsync(controller.OwinContext, It.IsAny())) + .Returns(Task.FromResult(0)) .Verifiable(); GetMock() .Setup(x => x.ReadExternalLoginCredential(controller.OwinContext)) - .CompletesWith(new AuthenticateExternalLoginResult() + .CompletesWith(new AuthenticateExternalLoginResult { ExternalIdentity = new ClaimsIdentity(), Credential = externalCred @@ -310,14 +331,14 @@ public async Task GivenAdminLogsInWithValidExternalAuth_ItChallengesWhenNotUsing { var enforcedProvider = "AzureActiveDirectory"; - var config = Get(); - config.Current = new AppConfiguration() + var mockConfig = GetMock(); + mockConfig.Setup(x => x.Current).Returns(new AppConfiguration { ConfirmEmailAddresses = false, EnforcedAuthProviderForAdmin = enforcedProvider - }; + }); - var externalCred = CredentialBuilder.CreateExternalCredential(providerUsedForLogin, "blorg", "Bloog"); + var externalCred = new CredentialBuilder().CreateExternalCredential(providerUsedForLogin, "blorg", "Bloog"); var authUser = new AuthenticatedUser( new User("theUsername") @@ -329,10 +350,13 @@ public async Task GivenAdminLogsInWithValidExternalAuth_ItChallengesWhenNotUsing } }, externalCred); - + + var authResult = + new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.Success, authUser); + GetMock() .Setup(x => x.Authenticate(authUser.User.Username, "thePassword")) - .CompletesWith(authUser); + .CompletesWith(authResult); GetMock() .Setup(x => x.AddCredential(authUser.User, externalCred)) @@ -356,7 +380,8 @@ public async Task GivenAdminLogsInWithValidExternalAuth_ItChallengesWhenNotUsing else { GetMock() - .Setup(x => x.CreateSession(controller.OwinContext, authUser.User)) + .Setup(x => x.CreateSessionAsync(controller.OwinContext, It.IsAny())) + .Returns(Task.FromResult(0)) .Verifiable(); } @@ -447,7 +472,8 @@ public async Task WillCreateAndLogInTheUserWhenNotLinking() var controller = GetController(); GetMock() - .Setup(x => x.CreateSession(controller.OwinContext, authUser.User)) + .Setup(x => x.CreateSessionAsync(controller.OwinContext, authUser)) + .Returns(Task.FromResult(0)) .Verifiable(); // Act @@ -484,11 +510,10 @@ public async Task WillNotSendConfirmationEmailWhenConfirmEmailAddressesIsOff() EmailConfirmationToken = "t0k3n" }, new Credential()); - var config = Get(); - config.Current = new AppConfiguration() - { - ConfirmEmailAddresses = false - }; + + var mockConfig = GetMock(); + mockConfig.Setup(x => x.Current).Returns(new AppConfiguration { ConfirmEmailAddresses = false }); + GetMock() .Setup(x => x.Register("theUsername", "unconfirmed@example.com", It.IsAny())) .CompletesWith(authUser); @@ -496,7 +521,8 @@ public async Task WillNotSendConfirmationEmailWhenConfirmEmailAddressesIsOff() var controller = GetController(); GetMock() - .Setup(x => x.CreateSession(controller.OwinContext, authUser.User)) + .Setup(x => x.CreateSessionAsync(controller.OwinContext, authUser)) + .Returns(Task.FromResult(0)) .Verifiable(); // Act @@ -528,7 +554,7 @@ public async Task GivenExpiredExternalAuth_ItRedirectsBackToLogOnWithExternalAut var controller = GetController(); GetMock() - .Setup(x => x.CreateSession(controller.OwinContext, authUser.User)) + .Setup(x => x.CreateSessionAsync(controller.OwinContext, authUser)) .Verifiable(); GetMock() .Setup(x => x.ReadExternalLoginCredential(controller.OwinContext)) @@ -548,7 +574,7 @@ public async Task GivenExpiredExternalAuth_ItRedirectsBackToLogOnWithExternalAut // Assert VerifyExternalLinkExpiredResult(controller, result); GetMock() - .Verify(x => x.CreateSession(It.IsAny(), It.IsAny()), Times.Never()); + .Verify(x => x.CreateSessionAsync(It.IsAny(), It.IsAny()), Times.Never()); GetMock() .Verify(x => x.Register("theUsername", "theEmailAddress", It.IsAny()), Times.Never()); } @@ -564,7 +590,7 @@ public async Task GivenValidExternalAuth_ItCreatesAccountAndLinksCredential() EmailConfirmationToken = "t0k3n" }, new Credential()); - var externalCred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); + var externalCred = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); GetMock() .Setup(x => x.Register("theUsername", "theEmailAddress", externalCred)) @@ -573,8 +599,10 @@ public async Task GivenValidExternalAuth_ItCreatesAccountAndLinksCredential() var controller = GetController(); GetMock() - .Setup(x => x.CreateSession(controller.OwinContext, authUser.User)) + .Setup(x => x.CreateSessionAsync(controller.OwinContext, authUser)) + .Returns(Task.FromResult(0)) .Verifiable(); + GetMock() .Setup(x => x.ReadExternalLoginCredential(controller.OwinContext)) .CompletesWith(new AuthenticateExternalLoginResult() @@ -617,14 +645,14 @@ public async Task GivenAdminLogsInWithExternalIdentity_ItChallengesWhenNotUsingR // Arrange var enforcedProvider = "AzureActiveDirectory"; - var config = Get(); - config.Current = new AppConfiguration() + var mockConfig = GetMock(); + mockConfig.Setup(x => x.Current).Returns(new AppConfiguration { ConfirmEmailAddresses = false, EnforcedAuthProviderForAdmin = enforcedProvider - }; + }); - var externalCred = CredentialBuilder.CreateExternalCredential(providerUsedForLogin, "blorg", "Bloog"); + var externalCred = new CredentialBuilder().CreateExternalCredential(providerUsedForLogin, "blorg", "Bloog"); var authUser = new AuthenticatedUser( new User("theUsername") @@ -655,7 +683,8 @@ public async Task GivenAdminLogsInWithExternalIdentity_ItChallengesWhenNotUsingR else { GetMock() - .Setup(x => x.CreateSession(controller.OwinContext, authUser.User)) + .Setup(x => x.CreateSessionAsync(controller.OwinContext, authUser)) + .Returns(Task.FromResult(0)) .Verifiable(); } @@ -701,7 +730,7 @@ public void WillChallengeTheUserUsingTheGivenProviderAndReturnUrl() var controller = GetController(); // Act - var result = controller.Authenticate("/theReturnUrl", "MicrosoftAccount"); + var result = controller.ChallengeAuthentication("/theReturnUrl", "MicrosoftAccount"); // Assert ResultAssert.IsChallengeResult(result, "MicrosoftAccount", "/users/account/authenticate/return?ReturnUrl=%2FtheReturnUrl"); @@ -731,11 +760,16 @@ public async Task GivenExpiredExternalAuth_ItRedirectsBackToLogOnWithExternalAut public async Task GivenAssociatedLocalUser_ItCreatesASessionAndSafeRedirectsToReturnUrl() { // Arrange + var fakes = Get(); + + var mockConfig = GetMock(); + mockConfig.Setup(x => x.Current).Returns(new AppConfiguration()); + GetMock(); // Force a mock to be created var controller = GetController(); - var cred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); + var cred = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); var authUser = new AuthenticatedUser( - Fakes.CreateUser("test", cred), + fakes.CreateUser("test", cred), cred); GetMock() .Setup(x => x.AuthenticateExternalLogin(controller.OwinContext)) @@ -751,7 +785,7 @@ public async Task GivenAssociatedLocalUser_ItCreatesASessionAndSafeRedirectsToRe // Assert ResultAssert.IsSafeRedirectTo(result, "theReturnUrl"); GetMock() - .Verify(x => x.CreateSession(controller.OwinContext, authUser.User)); + .Verify(x => x.CreateSessionAsync(controller.OwinContext, authUser)); } [Theory] @@ -762,18 +796,19 @@ public async Task GivenAssociatedLocalAdminUser_ItChallengesWhenNotUsingRequired // Arrange var enforcedProvider = "AzureActiveDirectory"; - var config = Get(); - config.Current = new AppConfiguration() + var mockConfig = GetMock(); + mockConfig.Setup(x => x.Current).Returns(new AppConfiguration { ConfirmEmailAddresses = false, EnforcedAuthProviderForAdmin = enforcedProvider - }; + }); + var fakes = Get(); GetMock(); // Force a mock to be created var controller = GetController(); - var cred = CredentialBuilder.CreateExternalCredential(providerUsedForLogin, "blorg", "Bloog"); + var cred = new CredentialBuilder().CreateExternalCredential(providerUsedForLogin, "blorg", "Bloog"); var authUser = new AuthenticatedUser( - Fakes.CreateUser("test", cred), + fakes.CreateUser("test", cred), cred); authUser.User.Roles.Add(new Role { Name = Constants.AdminRoleName }); @@ -796,7 +831,8 @@ public async Task GivenAssociatedLocalAdminUser_ItChallengesWhenNotUsingRequired else { GetMock() - .Setup(x => x.CreateSession(controller.OwinContext, authUser.User)) + .Setup(x => x.CreateSessionAsync(controller.OwinContext, authUser)) + .Returns(Task.FromResult(0)) .Verifiable(); } @@ -819,7 +855,7 @@ public async Task GivenAssociatedLocalAdminUser_ItChallengesWhenNotUsingRequired public async Task GivenNoLinkAndNoClaimData_ItDisplaysLogOnViewWithNoPrefilledData() { // Arrange - var cred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); + var cred = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); var msAuther = new MicrosoftAccountAuthenticator(); var msaUI = msAuther.GetUI(); @@ -884,7 +920,7 @@ public async Task GivenNoLinkAndNameClaim_ItDisplaysLogOnViewWithExternalAccount public async Task GivenNoLinkAndEmailClaim_ItDisplaysLogOnViewWithEmailPrefilled() { // Arrange - var cred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); + var cred = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); var msAuther = new MicrosoftAccountAuthenticator(); var msaUI = msAuther.GetUI(); @@ -918,12 +954,13 @@ public async Task GivenNoLinkAndEmailClaim_ItDisplaysLogOnViewWithEmailPrefilled public async Task GivenNoLinkButEmailMatchingLocalUser_ItDisplaysLogOnViewPresetForSignIn() { // Arrange + var fakes = Get(); var existingUser = new User("existingUser") { EmailAddress = "existing@example.com" }; - var cred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); + var cred = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "blorg", "Bloog"); var msAuther = new MicrosoftAccountAuthenticator(); var msaUI = msAuther.GetUI(); var authUser = new AuthenticatedUser( - Fakes.CreateUser("test", cred), + fakes.CreateUser("test", cred), cred); GetMock(); // Force a mock to be created diff --git a/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs index 27db2aa481..c864d6f106 100644 --- a/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs @@ -17,13 +17,17 @@ public class CuratedFeedsControllerFacts { public class TestableCuratedFeedsController : CuratedFeedsController { + public Fakes Fakes { get; } + public TestableCuratedFeedsController() { + Fakes = new Fakes(); + StubCuratedFeed = new CuratedFeed { Key = 0, Name = "aName", Managers = new HashSet(new[] { Fakes.User }) }; StubCuratedFeedService = new Mock(); - OwinContext = Fakes.CreateOwinContext(); + SetOwinContextOverride(Fakes.CreateOwinContext()); StubCuratedFeedService .Setup(stub => stub.GetFeedByName(It.IsAny(), It.IsAny())) @@ -73,7 +77,7 @@ public void WillReturn404IfTheCuratedFeedDoesNotExist() public void WillReturn403IfTheCurrentUsersIsNotAManagerOfTheCuratedFeed() { var controller = new TestableCuratedFeedsController(); - controller.SetCurrentUser(Fakes.Owner); + controller.SetCurrentUser(controller.Fakes.Owner); var result = controller.CuratedFeed("aName") as HttpStatusCodeResult; @@ -100,7 +104,7 @@ public void WillPassTheCuratedFeedManagersToTheView() var viewModel = (controller.CuratedFeed("aName") as ViewResult).Model as CuratedFeedViewModel; Assert.NotNull(viewModel); - Assert.Equal(Fakes.User.Username, viewModel.Managers.First()); + Assert.Equal(controller.Fakes.User.Username, viewModel.Managers.First()); } [Fact] diff --git a/tests/NuGetGallery.Facts/Controllers/CuratedPackagesControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/CuratedPackagesControllerFacts.cs index 3e6ba69c8c..1c52e609d7 100644 --- a/tests/NuGetGallery.Facts/Controllers/CuratedPackagesControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/CuratedPackagesControllerFacts.cs @@ -16,13 +16,17 @@ public class CuratedPackagesControllerFacts { public class TestableCuratedPackagesController : CuratedPackagesController { + public Fakes Fakes { get; } + public TestableCuratedPackagesController() { + Fakes = new Fakes(); + StubCuratedFeed = new CuratedFeed { Key = 0, Name = "aFeedName", Managers = new HashSet(new[] { Fakes.User }) }; StubPackageRegistration = new PackageRegistration { Key = 0, Id = "anId" }; - OwinContext = Fakes.CreateOwinContext(); + SetOwinContextOverride(Fakes.CreateOwinContext()); EntitiesContext = new FakeEntitiesContext(); EntitiesContext.CuratedFeeds.Add(StubCuratedFeed); @@ -52,7 +56,7 @@ public class TheDeleteCuratedPackageAction public async Task WillReturn404IfTheCuratedFeedDoesNotExist() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); var result = await controller.DeleteCuratedPackage("aStrangeCuratedFeedName", "anId"); @@ -63,7 +67,7 @@ public async Task WillReturn404IfTheCuratedFeedDoesNotExist() public async Task WillReturn404IfTheCuratedPackageDoesNotExist() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); controller.StubCuratedFeed.Packages = new[] { new CuratedPackage { PackageRegistration = new PackageRegistration() } }; var result = await controller.DeleteCuratedPackage("aFeedName", "aStrangeCuratedPackageId"); @@ -75,7 +79,7 @@ public async Task WillReturn404IfTheCuratedPackageDoesNotExist() public async Task WillReturn403IfTheUserNotAManager() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.Owner); + controller.SetCurrentUser(controller.Fakes.Owner); controller.StubCuratedFeed.Packages.Add( new CuratedPackage @@ -96,7 +100,7 @@ public async Task WillReturn403IfTheUserNotAManager() public async Task WillDeleteTheCuratedPackageWhenRequestIsValid() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); controller.StubCuratedFeed.Packages.Add( new CuratedPackage @@ -120,7 +124,7 @@ public async Task WillDeleteTheCuratedPackageWhenRequestIsValid() public async Task WillReturn204AfterDeletingTheCuratedPackage() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); controller.StubCuratedFeed.Packages.Add( new CuratedPackage @@ -144,7 +148,7 @@ public class TheGetCreateCuratedPackageFormAction public void WillReturn404IfTheCuratedFeedDoesNotExist() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); var result = controller.GetCreateCuratedPackageForm("aWrongFeedName"); @@ -155,7 +159,7 @@ public void WillReturn404IfTheCuratedFeedDoesNotExist() public void WillReturn403IfTheCurrentUsersIsNotAManagerOfTheCuratedFeed() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.Owner); + controller.SetCurrentUser(controller.Fakes.Owner); var result = controller.GetCreateCuratedPackageForm("aFeedName") as HttpStatusCodeResult; @@ -167,7 +171,7 @@ public void WillReturn403IfTheCurrentUsersIsNotAManagerOfTheCuratedFeed() public void WillPushTheCuratedFeedNameIntoTheViewBag() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); controller.StubCuratedFeed.Name = "theCuratedFeedName"; var result = controller.GetCreateCuratedPackageForm("theCuratedFeedName") as ViewResult; @@ -183,7 +187,7 @@ public class ThePatchCuratedPackageAction public async Task WillReturn404IfTheCuratedFeedDoesNotExist() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); var result = await controller.PatchCuratedPackage("aWrongFeedName", "anId", new ModifyCuratedPackageRequest()); @@ -195,7 +199,7 @@ public async Task WillReturn404IfTheCuratedFeedDoesNotExist() public async Task WillReturn404IfTheCuratedPackageDoesNotExist() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); var result = await controller.PatchCuratedPackage("aFeedName", "aWrongId", new ModifyCuratedPackageRequest()); @@ -206,7 +210,7 @@ public async Task WillReturn404IfTheCuratedPackageDoesNotExist() public async Task WillReturn403IfNotAFeedManager() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.Owner); + controller.SetCurrentUser(controller.Fakes.Owner); controller.StubCuratedFeed.Packages.Add( new CuratedPackage { @@ -227,7 +231,7 @@ public async Task WillReturn403IfNotAFeedManager() public async Task WillReturn400IfTheModelStateIsInvalid() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); controller.StubCuratedFeed.Packages.Add( new CuratedPackage { @@ -251,7 +255,7 @@ public async Task WillReturn400IfTheModelStateIsInvalid() public async Task WillModifyTheCuratedPackageWhenRequestIsValid() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); controller.StubCuratedFeed.Packages.Add( new CuratedPackage { @@ -278,7 +282,7 @@ public async Task WillModifyTheCuratedPackageWhenRequestIsValid() public async Task WillReturn204AfterModifyingTheCuratedPackage() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); controller.StubCuratedFeed.Packages.Add( new CuratedPackage { @@ -303,7 +307,7 @@ public class ThePostCuratedPackagesAction public async Task WillReturn404IfTheCuratedFeedDoesNotExist() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); var result = await controller.PostCuratedPackages( "aWrongFeedName", @@ -316,7 +320,7 @@ public async Task WillReturn404IfTheCuratedFeedDoesNotExist() public async Task WillReturn403IfTheCurrentUsersIsNotAManagerOfTheCuratedFeed() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.Owner); + controller.SetCurrentUser(controller.Fakes.Owner); var result = await controller.PostCuratedPackages( "aFeedName", @@ -331,7 +335,7 @@ public async Task WillReturn403IfTheCurrentUsersIsNotAManagerOfTheCuratedFeed() public async Task WillPushTheCuratedFeedNameIntoTheViewBagAndShowTheCreateCuratedPackageFormWithErrorsWhenModelStateIsInvalid() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); controller.StubCuratedFeed.Name = "theCuratedFeedName"; controller.ModelState.AddModelError("", "anError"); @@ -347,7 +351,7 @@ public async Task WillPushTheCuratedFeedNameIntoTheViewBagAndShowTheCreateCurate public async Task WillPushTheCuratedFeedNameIntoTheViewBagAndShowTheCreateCuratedPackageFormWithErrorsWhenThePackageIdDoesNotExist() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); var result = await controller.PostCuratedPackages("aFeedName", new CreateCuratedPackageRequest { PackageId = "aWrongId" }) as ViewResult; @@ -362,7 +366,7 @@ public async Task WillPushTheCuratedFeedNameIntoTheViewBagAndShowTheCreateCurate public async Task WillCreateTheCuratedPackage() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); await controller.PostCuratedPackages( "aFeedName", @@ -383,7 +387,7 @@ await controller.PostCuratedPackages( public async Task WillRedirectToTheCuratedFeedRouteAfterCreatingTheCuratedPackage() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); var result = await controller.PostCuratedPackages( "aFeedName", new CreateCuratedPackageRequest { PackageId = "anId" }) @@ -397,7 +401,7 @@ public async Task WillRedirectToTheCuratedFeedRouteAfterCreatingTheCuratedPackag public async Task WillShowAnErrorWhenThePackageHasAlreadyBeenCurated() { var controller = new TestableCuratedPackagesController(); - controller.SetCurrentUser(Fakes.User); + controller.SetCurrentUser(controller.Fakes.User); controller.StubCuratedFeed.Packages.Add( new CuratedPackage { CuratedFeed = controller.StubCuratedFeed, diff --git a/tests/NuGetGallery.Facts/Controllers/JsonApiControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/JsonApiControllerFacts.cs index c5fe600e5f..794587a7ca 100644 --- a/tests/NuGetGallery.Facts/Controllers/JsonApiControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/JsonApiControllerFacts.cs @@ -18,7 +18,7 @@ public async Task ReturnsFailureWhenPackageNotFound() { var controller = GetController(); - JsonResult result = await controller.AddPackageOwner("foo", "steve"); + JsonResult result = await controller.AddPackageOwner("foo", "steve", "message"); dynamic data = result.Data; Assert.False(data.success); @@ -33,7 +33,7 @@ public async Task DoesNotAllowNonPackageOwnerToAddPackageOwner() .Setup(svc => svc.FindPackageRegistrationById("foo")) .Returns(new PackageRegistration()); - JsonResult result = await controller.AddPackageOwner("foo", "steve"); + JsonResult result = await controller.AddPackageOwner("foo", "steve", "message"); dynamic data = result.Data; Assert.False(data.success); @@ -43,12 +43,13 @@ public async Task DoesNotAllowNonPackageOwnerToAddPackageOwner() [Fact] public async Task ReturnsFailureWhenRequestedNewOwnerDoesNotExist() { + var fakes = Get(); var controller = GetController(); GetMock() .Setup(c => c.User) - .Returns(Fakes.Owner.ToPrincipal()); + .Returns(Fakes.ToPrincipal(fakes.Owner)); - JsonResult result = await controller.AddPackageOwner(Fakes.Package.Id, "notARealUser"); + JsonResult result = await controller.AddPackageOwner(fakes.Package.Id, "notARealUser", "message"); dynamic data = result.Data; Assert.False(data.success); @@ -58,34 +59,37 @@ public async Task ReturnsFailureWhenRequestedNewOwnerDoesNotExist() [Fact] public async Task CreatesPackageOwnerRequestSendsEmailAndReturnsPendingState() { + var fakes = Get(); + var controller = GetController(); var httpContextMock = GetMock(); httpContextMock .Setup(c => c.User) - .Returns(Fakes.Owner.ToPrincipal()) + .Returns(Fakes.ToPrincipal(fakes.Owner)) .Verifiable(); var packageServiceMock = GetMock(); packageServiceMock - .Setup(p => p.CreatePackageOwnerRequestAsync(Fakes.Package, Fakes.Owner, Fakes.User)) + .Setup(p => p.CreatePackageOwnerRequestAsync(fakes.Package, fakes.Owner, fakes.User)) .Returns(Task.FromResult(new PackageOwnerRequest { ConfirmationCode = "confirmation-code" })) .Verifiable(); var messageServiceMock = GetMock(); messageServiceMock .Setup(m => m.SendPackageOwnerRequest( - Fakes.Owner, - Fakes.User, - Fakes.Package, - "https://nuget.local/packages/FakePackage/owners/testUser/confirm/confirmation-code")) + fakes.Owner, + fakes.User, + fakes.Package, + "https://nuget.local/packages/FakePackage/owners/testUser/confirm/confirmation-code", + "Hello World! Html Encoded <3")) .Verifiable(); - JsonResult result = await controller.AddPackageOwner(Fakes.Package.Id, Fakes.User.Username); + JsonResult result = await controller.AddPackageOwner(fakes.Package.Id, fakes.User.Username, "Hello World! Html Encoded <3"); dynamic data = result.Data; Assert.True(data.success); - Assert.Equal(Fakes.User.Username, data.name); + Assert.Equal(fakes.User.Username, data.name); Assert.True(data.pending); httpContextMock.Verify(); diff --git a/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs index e84b24afc2..a639f9cf83 100644 --- a/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs @@ -20,6 +20,7 @@ using NuGetGallery.Helpers; using NuGetGallery.Packaging; using NuGetGallery.Areas.Admin; +using NuGetGallery.Auditing; using Xunit; namespace NuGetGallery @@ -42,7 +43,9 @@ private static PackagesController CreateController( Mock indexingService = null, Mock cacheService = null, Mock packageDeleteService = null, - Mock supportRequestService = null) + Mock supportRequestService = null, + IAuditingService auditingService = null, + Mock telemetryService = null) { packageService = packageService ?? new Mock(); if (uploadFileService == null) @@ -75,6 +78,10 @@ private static PackagesController CreateController( supportRequestService = supportRequestService ?? new Mock(); + auditingService = auditingService ?? new TestAuditingService(); + + telemetryService = telemetryService ?? new Mock(); + var controller = new Mock( packageService.Object, uploadFileService.Object, @@ -88,10 +95,12 @@ private static PackagesController CreateController( cacheService.Object, editPackageService.Object, packageDeleteService.Object, - supportRequestService.Object); + supportRequestService.Object, + auditingService, + telemetryService.Object); controller.CallBase = true; - controller.Object.OwinContext = Fakes.CreateOwinContext(); + controller.Object.SetOwinContextOverride(Fakes.CreateOwinContext()); httpContext = httpContext ?? new Mock(); TestUtility.SetupHttpContextMockForUrlGeneration(httpContext, controller.Object); @@ -401,7 +410,7 @@ public async Task WithNonExistentPackageIdReturnsHttpNotFound() public async Task WithIdentityNotMatchingUserInRequestReturnsViewWithMessage() { var controller = CreateController(); - controller.SetCurrentUser("userA"); + controller.SetCurrentUser(new User("userA")); var result = await controller.ConfirmOwner("foo", "userB", "token"); var model = ResultAssert.IsView(result); @@ -543,7 +552,7 @@ public async Task UpdatesUnlistedIfSelected() var indexingService = new Mock(); var controller = CreateController(packageService: packageService, indexingService: indexingService); - controller.SetCurrentUser("Frodo"); + controller.SetCurrentUser(new User("Frodo")); controller.Url = new UrlHelper(new RequestContext(), new RouteCollection()); // Act @@ -579,7 +588,7 @@ public async Task UpdatesUnlistedIfNotSelected() var indexingService = new Mock(); var controller = CreateController(packageService: packageService, indexingService: indexingService); - controller.SetCurrentUser("Frodo"); + controller.SetCurrentUser(new User("Frodo")); controller.Url = new UrlHelper(new RequestContext(), new RouteCollection()); // Act @@ -604,7 +613,7 @@ public async Task TrimsSearchTerm() var controller = CreateController(searchService: searchService); controller.SetCurrentUser(TestUtility.FakeUser); - var result = (await controller.ListPackages(" test ")) as ViewResult; + var result = (await controller.ListPackages(new PackageListSearchViewModel() { Q = " test "})) as ViewResult; var model = result.Model as PackageListViewModel; Assert.Equal("test", model.SearchTerm); @@ -828,18 +837,20 @@ public class TheUploadFileActionForGetRequests [Fact] public async Task WillRedirectToVerifyPackageActionWhenThereIsAlreadyAnUploadInProgress() { - var fakeFileStream = new MemoryStream(); - var fakeUploadFileService = new Mock(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - var controller = CreateController( - uploadFileService: fakeUploadFileService); - controller.SetCurrentUser(TestUtility.FakeUser); + using (var fakeFileStream = new MemoryStream()) + { + var fakeUploadFileService = new Mock(); + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)) + .Returns(Task.FromResult(fakeFileStream)); + var controller = CreateController( + uploadFileService: fakeUploadFileService); + controller.SetCurrentUser(TestUtility.FakeUser); - var result = await controller.UploadPackage() as RedirectToRouteResult; + var result = await controller.UploadPackage() as RedirectToRouteResult; - Assert.NotNull(result); - Assert.Equal(RouteName.VerifyPackage, result.RouteName); - fakeFileStream.Dispose(); + Assert.NotNull(result); + Assert.Equal(RouteName.VerifyPackage, result.RouteName); + } } [Fact] @@ -862,18 +873,19 @@ public class TheUploadFileActionForPostRequests [Fact] public async Task WillReturn409WhenThereIsAlreadyAnUploadInProgress() { - var fakeFileStream = new MemoryStream(); - var fakeUploadFileService = new Mock(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - var controller = CreateController( - uploadFileService: fakeUploadFileService); - controller.SetCurrentUser(TestUtility.FakeUser); + using (var fakeFileStream = new MemoryStream()) + { + var fakeUploadFileService = new Mock(); + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + var controller = CreateController( + uploadFileService: fakeUploadFileService); + controller.SetCurrentUser(TestUtility.FakeUser); - var result = await controller.UploadPackage(null) as HttpStatusCodeResult; + var result = await controller.UploadPackage(null) as HttpStatusCodeResult; - Assert.NotNull(result); - Assert.Equal(409, result.StatusCode); - fakeFileStream.Dispose(); + Assert.NotNull(result); + Assert.Equal(409, result.StatusCode); + } } [Fact] @@ -903,9 +915,9 @@ public async Task WillShowViewWithErrorsIfFileIsNotANuGetPackage() Assert.False(controller.ModelState.IsValid); Assert.Equal(Strings.UploadFileMustBeNuGetPackage, controller.ModelState[String.Empty].Errors[0].ErrorMessage); } - + [Fact] - public async Task WillShowViewWithErrorsIfNuGetPackageIsInvalid() + public async Task WillShowViewWithErrorsIfEnsureValidThrowsException() { var fakeUploadedFile = new Mock(); fakeUploadedFile.Setup(x => x.FileName).Returns("theFile.nupkg"); @@ -924,6 +936,60 @@ public async Task WillShowViewWithErrorsIfNuGetPackageIsInvalid() Assert.Equal(Strings.FailedToReadUploadFile, controller.ModelState[String.Empty].Errors[0].ErrorMessage); } + private const string EnsureValidExceptionMessage = "naughty package"; + + [Theory] + [InlineData(typeof(InvalidPackageException), true)] + [InlineData(typeof(InvalidDataException), true)] + [InlineData(typeof(EntityException), true)] + [InlineData(typeof(Exception), false)] + public async Task WillShowViewWithErrorsIfEnsureValidThrowsExceptionMessage(Type exceptionType, bool expectExceptionMessageInResponse) + { + var fakeUploadedFile = new Mock(); + fakeUploadedFile.Setup(x => x.FileName).Returns("theFile.nupkg"); + var fakeFileStream = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + fakeUploadedFile.Setup(x => x.InputStream).Returns(fakeFileStream); + + var readPackageException = + exceptionType.GetConstructor(new[] {typeof(string)}).Invoke(new[] { EnsureValidExceptionMessage }); + + var controller = CreateController( + readPackageException: readPackageException as Exception); + controller.SetCurrentUser(TestUtility.FakeUser); + + var result = await controller.UploadPackage(fakeUploadedFile.Object) as ViewResult; + + Assert.NotNull(result); + Assert.False(controller.ModelState.IsValid); + Assert.Equal(expectExceptionMessageInResponse ? EnsureValidExceptionMessage : Strings.FailedToReadUploadFile, controller.ModelState[String.Empty].Errors[0].ErrorMessage); + } + + [Theory] + [InlineData("ILike*Asterisks")] + [InlineData("I_.Like.-Separators")] + [InlineData("-StartWithSeparator")] + [InlineData("EndWithSeparator.")] + [InlineData("EndsWithHyphen-")] + [InlineData("$id$")] + [InlineData("Contains#Invalid$Characters!@#$%^&*")] + [InlineData("Contains#Invalid$Characters!@#$%^&*EndsOnValidCharacter")] + public async Task WillShowViewWithErrorsIfPackageIdIsInvalid(string packageId) + { + // Arrange + var fakeUploadedFile = new Mock(); + fakeUploadedFile.Setup(x => x.FileName).Returns(packageId + ".nupkg"); + var fakeFileStream = TestPackage.CreateTestPackageStream(packageId, "1.0.0"); + fakeUploadedFile.Setup(x => x.InputStream).Returns(fakeFileStream); + + var controller = CreateController(fakeNuGetPackage: TestPackage.CreateTestPackageStream(packageId, "1.0.0")); + controller.SetCurrentUser(TestUtility.FakeUser); + + var result = await controller.UploadPackage(fakeUploadedFile.Object) as ViewResult; + + Assert.NotNull(result); + Assert.False(controller.ModelState.IsValid); + } + [Fact] public async Task WillShowTheViewWithErrorsWhenThePackageIdIsAlreadyBeingUsed() { @@ -1318,24 +1384,25 @@ public async Task WillRedirectToUploadPageWhenThereIsNoUploadInProgress() public async Task WillCreateThePackage() { var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); - var fakePackageService = new Mock(); - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns( - Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage); - controller.SetCurrentUser(TestUtility.FakeUser); - - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); - - fakePackageService.Verify(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), TestUtility.FakeUser, false)); - fakeFileStream.Dispose(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackageService = new Mock(); + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns( + Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage); + controller.SetCurrentUser(TestUtility.FakeUser); + + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); + + fakePackageService.Verify(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), TestUtility.FakeUser, false)); + } } [Fact] @@ -1343,32 +1410,113 @@ public async Task WillSavePackageToFileStorage() { // Arrange var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); - var fakePackageService = new Mock(); - var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(fakePackage)); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - - var fakePackageFileService = new Mock(); - fakePackageFileService.Setup(x => x.SavePackageFileAsync(fakePackage, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); - - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage, - packageFileService: fakePackageFileService); - controller.SetCurrentUser(TestUtility.FakeUser); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackageService = new Mock(); + var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(fakePackage)); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var fakePackageFileService = new Mock(); + fakePackageFileService.Setup(x => x.SavePackageFileAsync(fakePackage, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage, + packageFileService: fakePackageFileService); + controller.SetCurrentUser(TestUtility.FakeUser); + + // Act + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); + + // Assert + fakePackageService.Verify(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), TestUtility.FakeUser, false)); + fakePackageFileService.Verify(); + } + } - // Act - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); + [Fact] + public async Task WillDeletePackageFileFromBlobStorageIfSavingDbChangesFails() + { + // Arrange + var packageId = "theId"; + var packageVersion = "1.0.0"; + var fakeUploadFileService = new Mock(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackageService = new Mock(); + var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = packageId }, Version = packageVersion }; + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(fakePackage)); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream(packageId, packageVersion); + + var fakePackageFileService = new Mock(); + fakePackageFileService.Setup(x => x.SavePackageFileAsync(fakePackage, It.IsAny())).Returns(Task.CompletedTask).Verifiable(); + fakePackageFileService.Setup(x => x.DeletePackageFileAsync(packageId, packageVersion)).Returns(Task.CompletedTask).Verifiable(); + + var fakeEntitiesContext = new Mock(); + fakeEntitiesContext.Setup(e => e.SaveChangesAsync()).Throws(); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage, + packageFileService: fakePackageFileService, + entitiesContext: fakeEntitiesContext); + controller.SetCurrentUser(TestUtility.FakeUser); + + // Act + await Assert.ThrowsAsync(async () => await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null })); + + // Assert + fakePackageService.Verify(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), TestUtility.FakeUser, false)); + fakePackageFileService.Verify(); + } + } - // Assert - fakePackageService.Verify(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), TestUtility.FakeUser, false)); - fakePackageFileService.Verify(); - fakeFileStream.Dispose(); + [Fact] + public async Task WillShowViewWithMessageIfSavingPackageBlobFails() + { + // Arrange + var fakeUploadFileService = new Mock(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackageService = new Mock(); + var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(fakePackage)); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var fakePackageFileService = new Mock(); + fakePackageFileService.Setup(x => x.SavePackageFileAsync(fakePackage, It.IsAny())) + .Throws(); + + var fakeEntitiesContext = new Mock(); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage, + packageFileService: fakePackageFileService, + entitiesContext: fakeEntitiesContext); + controller.SetCurrentUser(TestUtility.FakeUser); + + // Act + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); + + // Assert + fakePackageService.Verify(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), TestUtility.FakeUser, false)); + Assert.Equal(Strings.UploadPackage_IdVersionConflict, controller.TempData["Message"]); + fakeEntitiesContext.VerifyCommitted(Times.Never()); + } } [Fact] @@ -1376,34 +1524,35 @@ public async Task WillUpdateIndexingService() { // Arrange var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); - var fakePackageService = new Mock(); - var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(fakePackage)); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - var fakePackageFileService = new Mock(); - fakePackageFileService.Setup(x => x.SavePackageFileAsync(fakePackage, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); - - var fakeIndexingService = new Mock(MockBehavior.Strict); - fakeIndexingService.Setup(f => f.UpdateIndex()).Verifiable(); - - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage, - packageFileService: fakePackageFileService, - indexingService: fakeIndexingService); - controller.SetCurrentUser(TestUtility.FakeUser); - - // Act - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); - - // Assert - fakeIndexingService.Verify(); - fakeFileStream.Dispose(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackageService = new Mock(); + var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(fakePackage)); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + var fakePackageFileService = new Mock(); + fakePackageFileService.Setup(x => x.SavePackageFileAsync(fakePackage, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + + var fakeIndexingService = new Mock(MockBehavior.Strict); + fakeIndexingService.Setup(f => f.UpdateIndex()).Verifiable(); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage, + packageFileService: fakePackageFileService, + indexingService: fakeIndexingService); + controller.SetCurrentUser(TestUtility.FakeUser); + + // Act + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); + + // Assert + fakeIndexingService.Verify(); + } } [Fact] @@ -1411,34 +1560,35 @@ public async Task WillSaveChangesToEntitiesContext() { // Arrange var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)) - .Returns(Task.FromResult(fakeFileStream)); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)) - .Returns(Task.CompletedTask); - var fakePackageService = new Mock(); - var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(fakePackage)); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - - var entitiesContext = new Mock(); - entitiesContext.Setup(e => e.SaveChangesAsync()) - .Returns(Task.FromResult(0)).Verifiable(); - - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage, - entitiesContext: entitiesContext); - controller.SetCurrentUser(TestUtility.FakeUser); - - // Act - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); - - // Assert - entitiesContext.Verify(); - fakeFileStream.Dispose(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)) + .Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)) + .Returns(Task.CompletedTask); + var fakePackageService = new Mock(); + var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(fakePackage)); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var entitiesContext = new Mock(); + entitiesContext.Setup(e => e.SaveChangesAsync()) + .Returns(Task.FromResult(0)).Verifiable(); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage, + entitiesContext: entitiesContext); + controller.SetCurrentUser(TestUtility.FakeUser); + + // Act + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); + + // Assert + entitiesContext.Verify(); + } } [Fact] @@ -1446,110 +1596,113 @@ public async Task WillNotCommitChangesToPackageService() { // Arrange var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)) - .Returns(Task.FromResult(fakeFileStream)); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)) - .Returns(Task.CompletedTask); - var fakePackageService = new Mock(MockBehavior.Strict); - var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), false)) - .Returns(Task.FromResult(fakePackage)); - fakePackageService.Setup(x => x.PublishPackageAsync(fakePackage, false)) - .Returns(Task.CompletedTask); - fakePackageService.Setup(x => x.MarkPackageUnlistedAsync(fakePackage, false)) - .Returns(Task.CompletedTask); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage); - controller.SetCurrentUser(TestUtility.FakeUser); - - // Act - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }); - - // There's no assert. If the method completes, it means the test pass because we set MockBehavior to Strict - // for the fakePackageService. We verified that it only calls methods passing commitSettings = false. - - fakeFileStream.Dispose(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)) + .Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)) + .Returns(Task.CompletedTask); + var fakePackageService = new Mock(MockBehavior.Strict); + var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), false)) + .Returns(Task.FromResult(fakePackage)); + fakePackageService.Setup(x => x.PublishPackageAsync(fakePackage, false)) + .Returns(Task.CompletedTask); + fakePackageService.Setup(x => x.MarkPackageUnlistedAsync(fakePackage, false)) + .Returns(Task.CompletedTask); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage); + controller.SetCurrentUser(TestUtility.FakeUser); + + // Act + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }); + + // There's no assert. If the method completes, it means the test passed because we set MockBehavior to Strict + // for the fakePackageService. We verified that it only calls methods passing commitSettings = false. + } } [Fact] public async Task WillPublishThePackage() { var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); - var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; - var fakePackageService = new Mock(); - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(fakePackage)); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage); - controller.SetCurrentUser(TestUtility.FakeUser); - - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); - - fakePackageService.Verify(x => x.PublishPackageAsync(fakePackage, false), Times.Once()); - fakeFileStream.Dispose(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; + var fakePackageService = new Mock(); + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(fakePackage)); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage); + controller.SetCurrentUser(TestUtility.FakeUser); + + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = true, Edit = null }); + + fakePackageService.Verify(x => x.PublishPackageAsync(fakePackage, false), Times.Once()); + } } [Fact] public async Task WillMarkThePackageUnlistedWhenListedArgumentIsFalse() { var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - var fakePackageService = new Mock(); - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage); - controller.SetCurrentUser(TestUtility.FakeUser); - - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }); - - fakePackageService.Verify( - x => x.MarkPackageUnlistedAsync(It.Is(p => p.PackageRegistration.Id == "theId" && p.Version == "theVersion"), It.IsAny())); - fakeFileStream.Dispose(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + var fakePackageService = new Mock(); + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage); + controller.SetCurrentUser(TestUtility.FakeUser); + + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }); + + fakePackageService.Verify( + x => x.MarkPackageUnlistedAsync(It.Is(p => p.PackageRegistration.Id == "theId" && p.Version == "theVersion"), It.IsAny())); + } } [Theory] - [InlineData(new object[] { null })] - [InlineData(new object[] { true })] + [InlineData(null)] + [InlineData(true)] public async Task WillNotMarkThePackageUnlistedWhenListedArgumentIsNullorTrue(bool? listed) { var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); - var fakePackageService = new Mock(); - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage); - controller.SetCurrentUser(TestUtility.FakeUser); - - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = listed.GetValueOrDefault(true), Edit = null }); - - fakePackageService.Verify(x => x.MarkPackageUnlistedAsync(It.IsAny(), It.IsAny()), Times.Never()); - fakeFileStream.Dispose(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackageService = new Mock(); + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage); + controller.SetCurrentUser(TestUtility.FakeUser); + + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = listed.GetValueOrDefault(true), Edit = null }); + + fakePackageService.Verify(x => x.MarkPackageUnlistedAsync(It.IsAny(), It.IsAny()), Times.Never()); + } } [Fact] @@ -1557,99 +1710,171 @@ public async Task WillDeleteTheUploadFile() { var fakeUploadFileService = new Mock(); fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)).Verifiable(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - var fakePackageService = new Mock(); - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + var fakePackageService = new Mock(); + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage); - controller.SetCurrentUser(TestUtility.FakeUser); + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage); + controller.SetCurrentUser(TestUtility.FakeUser); - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }); + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }); - fakeUploadFileService.Verify(); - fakeFileStream.Dispose(); + fakeUploadFileService.Verify(); + } } [Fact] public async Task WillSetAFlashMessage() { var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - fakeUploadFileService.Setup(x => x.SaveUploadFileAsync(TestUtility.FakeUser.Key, It.IsAny())).Returns(Task.FromResult(0)); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); - var fakePackageService = new Mock(); - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage); - controller.SetCurrentUser(TestUtility.FakeUser); - - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }); - - Assert.Equal(String.Format(Strings.SuccessfullyUploadedPackage, "theId", "theVersion"), controller.TempData["Message"]); - fakeFileStream.Dispose(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.SaveUploadFileAsync(TestUtility.FakeUser.Key, It.IsAny())).Returns(Task.FromResult(0)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackageService = new Mock(); + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage); + controller.SetCurrentUser(TestUtility.FakeUser); + + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }); + + Assert.Equal(String.Format(Strings.SuccessfullyUploadedPackage, "theId", "theVersion"), controller.TempData["Message"]); + } } [Fact] public async Task WillRedirectToPackagePage() { var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); - var fakePackageService = new Mock(); - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage); - controller.SetCurrentUser(TestUtility.FakeUser); - - var result = await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }) as RedirectToRouteResult; - - Assert.NotNull(result); - Assert.Equal(RouteName.DisplayPackage, result.RouteName); - fakeFileStream.Dispose(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackageService = new Mock(); + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" })); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage); + controller.SetCurrentUser(TestUtility.FakeUser); + + var result = await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }) as RedirectToRouteResult; + + Assert.NotNull(result); + Assert.Equal(RouteName.DisplayPackage, result.RouteName); + } } [Fact] public async Task WillCurateThePackage() { var fakeUploadFileService = new Mock(); - var fakeFileStream = new MemoryStream(); - fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); - fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); - var fakePackageService = new Mock(); - var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; - fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(fakePackage)); - var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); - - var fakeAutoCuratePackageCmd = new Mock(); - var controller = CreateController( - packageService: fakePackageService, - uploadFileService: fakeUploadFileService, - fakeNuGetPackage: fakeNuGetPackage, - autoCuratePackageCmd: fakeAutoCuratePackageCmd); - controller.SetCurrentUser(TestUtility.FakeUser); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackageService = new Mock(); + var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(fakePackage)); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var fakeAutoCuratePackageCmd = new Mock(); + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage, + autoCuratePackageCmd: fakeAutoCuratePackageCmd); + controller.SetCurrentUser(TestUtility.FakeUser); + + await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }); + + fakeAutoCuratePackageCmd.Verify(fake => fake.ExecuteAsync(fakePackage, It.IsAny(), false)); + } + } - await controller.VerifyPackage(new VerifyPackageRequest() { Listed = false, Edit = null }); + [Fact] + public async Task WritesAnAuditRecord() + { + // Arrange + var fakeUploadFileService = new Mock(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(0)); + var fakePackageService = new Mock(); + var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(fakePackage)); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + + var auditingService = new TestAuditingService(); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage, + auditingService: auditingService); + controller.SetCurrentUser(TestUtility.FakeUser); + + // Act + await controller.VerifyPackage(new VerifyPackageRequest { Listed = true, Edit = null }); + + // Assert + Assert.True(auditingService.WroteRecord(ar => + ar.Action == AuditedPackageAction.Create + && ar.Id == fakePackage.PackageRegistration.Id + && ar.Version == fakePackage.Version)); + } + } - fakeAutoCuratePackageCmd.Verify(fake => fake.ExecuteAsync(fakePackage, It.IsAny(), false)); + [Fact] + public async Task WillSendPackagePublishedEvent() + { + // Arrange + var fakeUploadFileService = new Mock(); + using (var fakeFileStream = new MemoryStream()) + { + fakeUploadFileService.Setup(x => x.GetUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.FromResult(fakeFileStream)); + fakeUploadFileService.Setup(x => x.DeleteUploadFileAsync(TestUtility.FakeUser.Key)).Returns(Task.CompletedTask); + var fakePackageService = new Mock(); + var fakePackage = new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }; + fakePackageService.Setup(x => x.CreatePackageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(fakePackage)); + var fakeNuGetPackage = TestPackage.CreateTestPackageStream("theId", "1.0.0"); + var fakeTelemetryService = new Mock(); + + var controller = CreateController( + packageService: fakePackageService, + uploadFileService: fakeUploadFileService, + fakeNuGetPackage: fakeNuGetPackage, + telemetryService: fakeTelemetryService); + + controller.SetCurrentUser(TestUtility.FakeUser); + + // Act + await controller.VerifyPackage(new VerifyPackageRequest { Listed = true, Edit = null }); + + // Assert + fakeTelemetryService.Verify(x => x.TrackPackagePushEvent(It.IsAny(), TestUtility.FakeUser, controller.OwinContext.Request.User.Identity), Times.Once); + } } } @@ -1727,7 +1952,7 @@ public async Task IndexingAndPackageServicesAreUpdated() var indexingService = new Mock(); var controller = CreateController(packageService: packageService, httpContext: httpContext, indexingService: indexingService); - controller.SetCurrentUser("Smeagol"); + controller.SetCurrentUser(new User("Smeagol")); controller.Url = new UrlHelper(new RequestContext(), new RouteCollection()); // Act diff --git a/tests/NuGetGallery.Facts/Controllers/StatisticsControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/StatisticsControllerFacts.cs index 762eb78524..8fa81414e4 100644 --- a/tests/NuGetGallery.Facts/Controllers/StatisticsControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/StatisticsControllerFacts.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; using System.Collections.Generic; using System.Globalization; @@ -288,7 +289,7 @@ public async void StatisticsHomePage_Packages_ValidateReportStructureAndAvailabi sum += item.Downloads; } - Assert.Equal(106, sum); + Assert.Equal(106, sum); } [Fact] @@ -382,7 +383,89 @@ public async void StatisticsHomePage_Per_Package_ValidateReportStructureAndAvail { { "ClientName", "NuGet" }, { "ClientVersion", "2.1" }, - { "Operation", "unknow" }, + { "Operation", "unknown" }, + { "Downloads", 301 } + } + } + } + }, + } + } + }; + + var fakeReport = report.ToString(); + + var fakeReportService = new Mock(); + + string reportName = "recentpopularity/RecentPopularityDetail_" + PackageId + ".json"; + reportName = reportName.ToLowerInvariant(); + + var updatedUtc = new DateTime(2001, 01, 01, 10, 20, 30); + fakeReportService.Setup(x => x.Load(reportName)).Returns(Task.FromResult(new StatisticsReport(fakeReport, updatedUtc))); + + var controller = new StatisticsController(new JsonStatisticsService(fakeReportService.Object)); + + TestUtility.SetupUrlHelperForUrlGeneration(controller, new Uri("http://nuget.org")); + + var model = (StatisticsPackagesViewModel)((ViewResult)await controller.PackageDownloadsByVersion(PackageId, new[] { Constants.StatisticsDimensions.Version })).Model; + + int sum = 0; + + foreach (var row in model.Report.Table) + { + sum += int.Parse(row[row.GetLength(0) - 1].Data); + } + + Assert.Equal(603, sum); + Assert.Equal("603", model.Report.Total); + Assert.True(model.LastUpdatedUtc.HasValue); + Assert.Equal(updatedUtc, model.LastUpdatedUtc.Value); + } + + [Fact] + public async void StatisticsHomePage_Per_Package_ValidateReportStructureAndAvailabilityInvalidGroupBy() + { + string PackageId = "A"; + + JObject report = new JObject + { + { "Downloads", 603 }, + { "Items", new JArray + { + new JObject + { + { "Version", "1.0" }, + { "Downloads", 101 }, + { "Items", new JArray + { + new JObject + { + { "ClientName", "NuGet" }, + { "ClientVersion", "2.1" }, + { "Operation", "Install" }, + { "Downloads", 101 } + }, + } + } + }, + new JObject + { + { "Version", "2.0" }, + { "Downloads", 502 }, + { "Items", new JArray + { + new JObject + { + { "ClientName", "NuGet" }, + { "ClientVersion", "2.1" }, + { "Operation", "Install" }, + { "Downloads", 201 } + }, + new JObject + { + { "ClientName", "NuGet" }, + { "ClientVersion", "2.1" }, + { "Operation", "unknown" }, { "Downloads", 301 } } } @@ -406,7 +489,9 @@ public async void StatisticsHomePage_Per_Package_ValidateReportStructureAndAvail TestUtility.SetupUrlHelperForUrlGeneration(controller, new Uri("http://nuget.org")); - var model = (StatisticsPackagesViewModel)((ViewResult)await controller.PackageDownloadsByVersion(PackageId, new string[] { "Version" })).Model; + var invalidDimension = "this_dimension_does_not_exist"; + + var model = (StatisticsPackagesViewModel)((ViewResult)await controller.PackageDownloadsByVersion(PackageId, new[] { Constants.StatisticsDimensions.Version, invalidDimension })).Model; int sum = 0; @@ -419,6 +504,7 @@ public async void StatisticsHomePage_Per_Package_ValidateReportStructureAndAvail Assert.Equal("603", model.Report.Total); Assert.True(model.LastUpdatedUtc.HasValue); Assert.Equal(updatedUtc, model.LastUpdatedUtc.Value); + Assert.DoesNotContain(invalidDimension, model.Report.Columns); } [Fact] @@ -465,7 +551,7 @@ public async void Statistics_By_Client_Operation_ValidateReportStructureAndAvail { { "ClientName", "NuGet" }, { "ClientVersion", "2.1" }, - { "Operation", "unknow" }, + { "Operation", "unknown" }, { "Downloads", 301 } } } @@ -530,7 +616,7 @@ public async void UseServerCultureIfLanguageHeadersIsMissing() // Act var result = await controller.Totals() as JsonResult; - // Asssert + // Assert Assert.NotNull(result); dynamic data = result.Data; @@ -566,7 +652,7 @@ public async void UseClientCultureIfLanguageHeadersIsPresent() var result = await InvokeAction(() => (controller.Totals()), controller) as JsonResult; - // Asssert + // Assert Assert.NotNull(result); dynamic data = result.Data; diff --git a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs index e78ee3c8a3..a6a5efbcef 100644 --- a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Mail; using System.Threading.Tasks; using System.Web.Mvc; @@ -11,12 +12,15 @@ using NuGetGallery.Authentication; using NuGetGallery.Configuration; using NuGetGallery.Framework; +using NuGetGallery.Infrastructure.Authentication; using Xunit; namespace NuGetGallery { public class UsersControllerFacts { + public static readonly int CredentialKey = 123; + public class TheAccountAction : TestContainer { [Fact] @@ -53,11 +57,13 @@ public void WillReturnTheAccountViewModelWithTheCuratedFeeds() public void LoadsDescriptionsOfCredentialsInToViewModel() { // Arrange - var user = Fakes.CreateUser( + var credentialBuilder = new CredentialBuilder(); + var fakes = Get(); + var user = fakes.CreateUser( "test", - CredentialBuilder.CreatePbkdf2Password("hunter2"), - CredentialBuilder.CreateV1ApiKey(Guid.NewGuid()), - CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blarg", "Bloog")); + credentialBuilder.CreatePasswordCredential("hunter2"), + TestCredentialHelper.CreateV1ApiKey(Guid.NewGuid(), Fakes.ExpirationForApiKeyV1), + credentialBuilder.CreateExternalCredential("MicrosoftAccount", "blarg", "Bloog")); var controller = GetController(); controller.SetCurrentUser(user); @@ -72,6 +78,45 @@ public void LoadsDescriptionsOfCredentialsInToViewModel() Assert.Equal(Strings.CredentialType_ApiKey, descs[CredentialKind.Token].TypeCaption); Assert.Equal(Strings.MicrosoftAccount_Caption, descs[CredentialKind.External].TypeCaption); } + + + [Fact] + public void FiltersOutUnsupportedCredentialsInToViewModel() + { + // Arrange + var credentialBuilder = new CredentialBuilder(); + var fakes = Get(); + + var credentials = new List + { + credentialBuilder.CreatePasswordCredential("v3"), + TestCredentialHelper.CreatePbkdf2Password("pbkdf2"), + TestCredentialHelper.CreateSha1Password("sha1"), + TestCredentialHelper.CreateV1ApiKey(Guid.NewGuid(), Fakes.ExpirationForApiKeyV1), + TestCredentialHelper.CreateV2ApiKey(Guid.NewGuid(), Fakes.ExpirationForApiKeyV1), + credentialBuilder.CreateExternalCredential("MicrosoftAccount", "blarg", "Bloog"), + new Credential() { Type = "unsupported" } + }; + + var user = fakes.CreateUser("test", credentials.ToArray()); + + var controller = GetController(); + controller.SetCurrentUser(user); + + // Act + var result = controller.Account(); + + // Assert + var model = ResultAssert.IsView(result, viewName: "Account"); + var descs = model.Credentials.ToDictionary(c => c.Type); // Should only be one of each type + Assert.Equal(6, descs.Count); + Assert.True(descs.ContainsKey(credentials[0].Type)); + Assert.True(descs.ContainsKey(credentials[1].Type)); + Assert.True(descs.ContainsKey(credentials[2].Type)); + Assert.True(descs.ContainsKey(credentials[3].Type)); + Assert.True(descs.ContainsKey(credentials[4].Type)); + Assert.True(descs.ContainsKey(credentials[5].Type)); + } } public class TheConfirmationRequiredAction : TestContainer @@ -171,7 +216,7 @@ public async Task SendsEmailWithPasswordResetUrl() { EmailAddress = "some@example.com", PasswordResetToken = "confirmation", - PasswordResetTokenExpirationDate = DateTime.UtcNow.AddDays(1) + PasswordResetTokenExpirationDate = DateTime.UtcNow.AddHours(Constants.PasswordResetTokenExpirationHours) }; GetMock() .Setup(s => s.SendPasswordResetInstructions(user, resetUrl, true)); @@ -179,7 +224,7 @@ public async Task SendsEmailWithPasswordResetUrl() .Setup(s => s.FindByEmailAddress("user")) .Returns(user); GetMock() - .Setup(s => s.GeneratePasswordResetToken("user", 1440)) + .Setup(s => s.GeneratePasswordResetToken("user", Constants.PasswordResetTokenExpirationHours * 60)) .CompletesWith(user); var controller = GetController(); var model = new ForgotPasswordViewModel { Email = "user" }; @@ -195,7 +240,7 @@ public async Task RedirectsAfterGeneratingToken() { var user = new User { EmailAddress = "some@example.com", Username = "somebody" }; GetMock() - .Setup(s => s.GeneratePasswordResetToken("user", 1440)) + .Setup(s => s.GeneratePasswordResetToken("user", Constants.PasswordResetTokenExpirationHours * 60)) .CompletesWith(user) .Verifiable(); var controller = GetController(); @@ -206,14 +251,14 @@ public async Task RedirectsAfterGeneratingToken() Assert.NotNull(result); GetMock() - .Verify(s => s.GeneratePasswordResetToken("user", 1440)); + .Verify(s => s.GeneratePasswordResetToken("user", Constants.PasswordResetTokenExpirationHours * 60)); } [Fact] public async Task ReturnsSameViewIfTokenGenerationFails() { GetMock() - .Setup(s => s.GeneratePasswordResetToken("user", 1440)) + .Setup(s => s.GeneratePasswordResetToken("user", Constants.PasswordResetTokenExpirationHours * 60)) .CompletesWithNull(); var controller = GetController(); @@ -286,12 +331,30 @@ public async Task SendsPasswordAddedMessageWhenForgotFalse() ConfirmPassword = "pwd", NewPassword = "newpwd" }; - + await controller.ResetPassword("user", "token", model, forgot: false); GetMock() .Verify(m => m.SendCredentialAddedNotice(cred.User, cred)); } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task WhenModelIsInvalidItIsRetried(bool forgot) + { + var controller = GetController(); + + controller.ModelState.AddModelError("test", "test"); + + var result = await controller.ResetPassword("user", "token", new PasswordResetViewModel(), forgot); + + Assert.NotNull(result); + Assert.IsType(result); + + var viewResult = result as ViewResult; + Assert.Equal(forgot, viewResult.ViewBag.ForgotPassword); + } } public class TheConfirmAction : TestContainer @@ -474,34 +537,203 @@ public async Task DoesntSendAccountChangedEmailsIfConfirmationTokenDoesntMatch() public class TheGenerateApiKeyAction : TestContainer { + [InlineData(null)] + [InlineData(" ")] + [Theory] + public async Task WhenEmptyDescriptionProvidedRedirectsToAccountPageWithError(string description) + { + // Arrange + var user = new User { Username = "the-username" }; + var controller = GetController(); + controller.SetCurrentUser(user); + + // Act + var result = await controller.GenerateApiKey( + description: description, + scopes: null, + expirationInDays: null); + + // Assert + Assert.Equal((int)HttpStatusCode.BadRequest, controller.Response.StatusCode); + Assert.IsType(result); + Assert.True(string.Compare((string)((JsonResult)result).Data, Strings.ApiKeyDescriptionRequired) == 0); + } + + [InlineData(180, 180)] + [InlineData(700, 365)] + [InlineData(-1, 365)] + [InlineData(0, 365)] + [InlineData(null, 365)] + [Theory] + public async Task WhenExpirationInDaysIsProvidedItsUsed(int? inputExpirationInDays, int expectedExpirationInDays) + { + // Arrange + var user = new User("the-username"); + + var controller = GetController(); + controller.SetCurrentUser(user); + + var config = GetMock(); + config.SetupGet(x => x.ExpirationInDaysForApiKeyV1).Returns(365); + + // Act + await controller.GenerateApiKey( + description: "my new api key", + scopes: new [] { NuGetScopes.PackageUnlist }, + subjects: null, + expirationInDays: inputExpirationInDays); + + // Assert + var apiKey = user.Credentials.FirstOrDefault(x => x.Type == CredentialTypes.ApiKey.V2); + + Assert.NotNull(apiKey); + Assert.NotNull(apiKey.Expires); + Assert.Equal(expectedExpirationInDays, TimeSpan.FromTicks(apiKey.ExpirationTicks.Value).Days); + } + + public static IEnumerable CreatesNewApiKeyCredential_Input + { + get + { + return new[] + { + new object[] + { + "permissions to several scopes, several packages", + new[] {NuGetScopes.PackageUnlist, NuGetScopes.PackagePush}, + new[] {"abc", "def"}, + new [] + { + new Scope("abc", NuGetScopes.PackageUnlist), + new Scope("abc", NuGetScopes.PackagePush), + new Scope("def", NuGetScopes.PackageUnlist), + new Scope("def", NuGetScopes.PackagePush) + } + }, + new object[] + { + "permissions to several scopes, all packages", + new [] { NuGetScopes.PackageUnlist, NuGetScopes.PackagePush }, + null, + new [] + { + new Scope("*", NuGetScopes.PackageUnlist), + new Scope("*", NuGetScopes.PackagePush) + } + }, + new object[] + { + "permissions to single scope, all packages", + new [] { NuGetScopes.PackageUnlist }, + null, + new [] + { + new Scope("*", NuGetScopes.PackageUnlist) + } + }, + new object[] + { + "permissions to everything", + null, + null, + new [] + { + new Scope("*", NuGetScopes.All) + } + }, + new object[] + { + "empty subjects are ignored", + new [] { NuGetScopes.PackageUnlist }, + new[] {"abc", "def", string.Empty, null, " "}, + new [] + { + new Scope("abc", NuGetScopes.PackageUnlist), + new Scope("def", NuGetScopes.PackageUnlist) + } + } + }; + } + } + + [MemberData(nameof(CreatesNewApiKeyCredential_Input))] + [Theory] + public async Task CreatesNewApiKeyCredential(string description, string[] scopes, string[] subjects, Scope[] expectedScopes) + { + // Arrange + var user = new User("the-username"); + + var controller = GetController(); + controller.SetCurrentUser(user); + + // Act + await controller.GenerateApiKey( + description: description, + scopes: scopes, + subjects: subjects, + expirationInDays: null); + + // Assert + var apiKey = user.Credentials.FirstOrDefault(x => x.Type == CredentialTypes.ApiKey.V2); + + Assert.NotNull(apiKey); + Assert.Equal(description, apiKey.Description); + Assert.Equal(expectedScopes.Length, apiKey.Scopes.Count); + + foreach (var expectedScope in expectedScopes) + { + var actualScope = + apiKey.Scopes.First(x => x.AllowedAction == expectedScope.AllowedAction && + x.Subject == expectedScope.Subject); + Assert.NotNull(actualScope); + } + } + [Fact] - public async Task RedirectsToAccountPage() + public async Task ReturnsNewCredentialJson() { var user = new User { Username = "the-username" }; + var controller = GetController(); controller.SetCurrentUser(user); - var result = await controller.GenerateApiKey(); + var result = await controller.GenerateApiKey( + description: "description", + scopes: new [] { NuGetScopes.PackageUnlist, NuGetScopes.PackagePush }, + subjects: new [] { "a" }, + expirationInDays: 90); - ResultAssert.IsRedirectToRoute(result, new { action = "Account" }); + Assert.IsType(result); + + var credentialViewModel = ((JsonResult) result).Data as CredentialViewModel; + Assert.NotNull(credentialViewModel); + + var apiKey = user.Credentials.FirstOrDefault(x => x.Type == CredentialTypes.ApiKey.V2); + + Assert.Equal(apiKey.Value, credentialViewModel.Value); + Assert.Equal(apiKey.Key, credentialViewModel.Key); + Assert.Equal(apiKey.Description, credentialViewModel.Description); + Assert.Equal(apiKey.Expires, credentialViewModel.Expires); } [Fact] - public async Task ReplacesTheApiKeyCredential() + public async Task SendsNotificationMailToUser() { - var user = new User("the-username"); - GetMock() - .Setup(u => u.ReplaceCredential( - user, - It.Is(c => c.Type == CredentialTypes.ApiKeyV1))) - .Completes() - .Verifiable(); + var user = new User { Username = "the-username" }; + var controller = GetController(); controller.SetCurrentUser(user); - await controller.GenerateApiKey(); + var result = await controller.GenerateApiKey( + description: "description", + scopes: new[] { NuGetScopes.PackageUnlist, NuGetScopes.PackagePush }, + subjects: new[] { "a" }, + expirationInDays: 90); - GetMock().VerifyAll(); + var apiKey = user.Credentials.FirstOrDefault(x => x.Type == CredentialTypes.ApiKey.V2); + + GetMock() + .Verify(m => m.SendCredentialAddedNotice(user, apiKey)); } } @@ -517,9 +749,13 @@ public async Task DoesNotLetYouUseSomeoneElsesConfirmedEmailAddress() Key = 1, }; + var authResult = + new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.Success, + new AuthenticatedUser(user, new Credential())); + GetMock() .Setup(u => u.Authenticate(It.IsAny(), It.IsAny())) - .CompletesWith(new AuthenticatedUser(user, new Credential())); + .CompletesWith(authResult); GetMock() .Setup(u => u.ChangeEmailAddress(user, "new@example.com")) .Throws(new EntityException("msg")); @@ -548,9 +784,14 @@ public async Task SendsEmailChangeConfirmationNoticeWhenChangingAConfirmedEmailA EmailAllowed = true }; + var authResult = + new PasswordAuthenticationResult( + PasswordAuthenticationResult.AuthenticationResult.Success, + new AuthenticatedUser(user, new Credential())); + GetMock() .Setup(u => u.Authenticate("theUsername", "password")) - .CompletesWith(new AuthenticatedUser(user, new Credential())); + .CompletesWith(authResult); GetMock() .Setup(u => u.ChangeEmailAddress(user, "new@example.com")) .Callback(() => user.UpdateEmailAddress("new@example.com", () => "token")) @@ -582,9 +823,12 @@ public async Task DoesNotSendEmailChangeConfirmationNoticeWhenAddressDoesntChang Username = "aUsername", }; + var authResult = + new PasswordAuthenticationResult(PasswordAuthenticationResult.AuthenticationResult.Success, new AuthenticatedUser(user, new Credential())); + GetMock() .Setup(u => u.Authenticate("aUsername", "password")) - .CompletesWith(new AuthenticatedUser(user, new Credential())); + .CompletesWith(authResult); GetMock() .Setup(u => u.ChangeEmailAddress(It.IsAny(), It.IsAny())) .Callback(() => user.UpdateEmailAddress("old@example.com", () => "new-token")); @@ -619,7 +863,8 @@ public async Task DoesNotSendEmailChangeConfirmationNoticeWhenUserWasNotConfirme GetMock() .Setup(u => u.Authenticate("aUsername", "password")) - .CompletesWith(new AuthenticatedUser(user, new Credential())); + .CompletesWith(new PasswordAuthenticationResult( + PasswordAuthenticationResult.AuthenticationResult.Success, new AuthenticatedUser(user, new Credential()))); GetMock() .Setup(u => u.ChangeEmailAddress(It.IsAny(), It.IsAny())) .Callback(() => user.UpdateEmailAddress("new@example.com", () => "new-token")) @@ -644,6 +889,40 @@ public async Task DoesNotSendEmailChangeConfirmationNoticeWhenUserWasNotConfirme GetMock() .Verify(m => m.SendEmailChangeConfirmationNotice(It.IsAny(), It.IsAny()), Times.Never()); } + + [Fact] + public async Task WhenPasswordValidationFailsErrorIsReturned() + { + // Arrange + var user = new User + { + Username = "theUsername", + EmailAddress = "test@example.com", + Credentials = new [] { new Credential(CredentialTypes.Password.V3, "abc") } + }; + + Credential credential; + GetMock() + .Setup(u => u.ValidatePasswordCredential(It.IsAny>(), It.IsAny(), out credential)) + .Returns(false); + + var controller = GetController(); + controller.SetCurrentUser(user); + + var model = new AccountViewModel + { + ChangeEmail = new ChangeEmailViewModel + { + NewEmail = "new@example.com", + Password = "password" + } + }; + + var result = await controller.ChangeEmail(model); + + Assert.IsType(result); + Assert.IsType(((ViewResult) result).Model); + } } public class TheChangePasswordAction : TestContainer @@ -657,8 +936,8 @@ public async Task GivenInvalidView_ItReturnsView() var inputModel = new AccountViewModel(); controller.SetCurrentUser(new User() { - Credentials = new List() { - CredentialBuilder.CreatePbkdf2Password("abc") + Credentials = new List { + new CredentialBuilder().CreatePasswordCredential("abc") } }); @@ -675,7 +954,7 @@ public async Task GivenFailureInAuthService_ItAddsModelError() { // Arrange var user = new User("foo"); - user.Credentials.Add(CredentialBuilder.CreatePbkdf2Password("old")); + user.Credentials.Add(new CredentialBuilder().CreatePasswordCredential("old")); GetMock() .Setup(u => u.ChangePassword(user, "old", "new")) @@ -713,7 +992,7 @@ public async Task GivenSuccessInAuthService_ItRedirectsBackToManageCredentialsWi { // Arrange var user = new User("foo"); - user.Credentials.Add(CredentialBuilder.CreatePbkdf2Password("old")); + user.Credentials.Add(new CredentialBuilder().CreatePasswordCredential("old")); GetMock() .Setup(u => u.ChangePassword(user, "old", "new")) @@ -741,7 +1020,8 @@ public async Task GivenSuccessInAuthService_ItRedirectsBackToManageCredentialsWi public async Task GivenNoOldPassword_ItSendsAPasswordSetEmail() { // Arrange - var user = Fakes.CreateUser("test"); + var fakes = Get(); + var user = fakes.CreateUser("test"); user.EmailAddress = "confirmed@example.com"; GetMock() @@ -773,8 +1053,9 @@ public class TheRemovePasswordAction : TestContainer public async Task GivenNoOtherLoginCredentials_ItRedirectsBackWithAnErrorMessage() { // Arrange - var user = Fakes.CreateUser("test", - CredentialBuilder.CreatePbkdf2Password("password")); + var fakes = Get(); + var user = fakes.CreateUser("test", + new CredentialBuilder().CreatePasswordCredential("password")); var controller = GetController(); controller.SetCurrentUser(user); @@ -791,8 +1072,9 @@ public async Task GivenNoOtherLoginCredentials_ItRedirectsBackWithAnErrorMessage public async Task GivenNoPassword_ItRedirectsBackWithNoChangesMade() { // Arrange - var user = Fakes.CreateUser("test", - CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "bloog")); + var fakes = Get(); + var user = fakes.CreateUser("test", + new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "blorg", "bloog")); var controller = GetController(); controller.SetCurrentUser(user); @@ -801,6 +1083,7 @@ public async Task GivenNoPassword_ItRedirectsBackWithNoChangesMade() // Assert ResultAssert.IsRedirectToRoute(result, new { action = "Account" }); + Assert.Equal(Strings.CredentialNotFound, controller.TempData["Message"]); Assert.Equal(1, user.Credentials.Count); } @@ -808,10 +1091,12 @@ public async Task GivenNoPassword_ItRedirectsBackWithNoChangesMade() public async Task GivenValidRequest_ItRemovesCredAndSendsNotificationToUser() { // Arrange - var cred = CredentialBuilder.CreatePbkdf2Password("password"); - var user = Fakes.CreateUser("test", + var credentialBuilder = new CredentialBuilder(); + var fakes = Get(); + var cred = credentialBuilder.CreatePasswordCredential("password"); + var user = fakes.CreateUser("test", cred, - CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "bloog")); + credentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "bloog")); GetMock() .Setup(a => a.RemoveCredential(user, cred)) @@ -840,13 +1125,16 @@ public class TheRemoveCredentialAction : TestContainer public async Task GivenNoOtherLoginCredentials_ItRedirectsBackWithAnErrorMessage() { // Arrange - var cred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "bloog"); - var user = Fakes.CreateUser("test", cred); + var fakes = Get(); + var cred = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "blorg", "bloog"); + var user = fakes.CreateUser("test", cred); var controller = GetController(); controller.SetCurrentUser(user); // Act - var result = await controller.RemoveCredential(cred.Type); + var result = await controller.RemoveCredential( + credentialType: cred.Type, + credentialKey: null); // Assert ResultAssert.IsRedirectToRoute(result, new { action = "Account" }); @@ -855,19 +1143,49 @@ public async Task GivenNoOtherLoginCredentials_ItRedirectsBackWithAnErrorMessage } [Fact] - public async Task GivenNoCredential_ItRedirectsBackWithNoChangesMade() + public async Task GivenNoCredential_ErrorIsReturnedWithNoChangesMade() { // Arrange - var user = Fakes.CreateUser("test", - CredentialBuilder.CreatePbkdf2Password("password")); + var fakes = Get(); + var user = fakes.CreateUser("test", + new CredentialBuilder().CreatePasswordCredential("password")); var controller = GetController(); controller.SetCurrentUser(user); // Act - var result = await controller.RemoveCredential(CredentialTypes.ExternalPrefix + "MicrosoftAccount"); + var result = await controller.RemoveCredential( + credentialType: CredentialTypes.ExternalPrefix + "MicrosoftAccount", + credentialKey: null); // Assert ResultAssert.IsRedirectToRoute(result, new { action = "Account" }); + Assert.Equal(Strings.CredentialNotFound, controller.TempData["Message"]); + + Assert.Equal(1, user.Credentials.Count); + } + + [Theory] + [InlineData(CredentialTypes.ApiKey.V1)] + [InlineData(CredentialTypes.ApiKey.V2)] + public async Task GivenNoApiKeyCredential_ErrorIsReturnedWithNoChangesMade(string apiKeyType) + { + // Arrange + var fakes = Get(); + var user = fakes.CreateUser("test", + new CredentialBuilder().CreatePasswordCredential("password")); + var controller = GetController(); + controller.SetCurrentUser(user); + + // Act + var result = await controller.RemoveCredential( + credentialType: apiKeyType, + credentialKey: null); + + // Assert + Assert.Equal((int)HttpStatusCode.NotFound, controller.Response.StatusCode); + Assert.IsType(result); + Assert.True(string.Compare((string)((JsonResult)result).Data, Strings.CredentialNotFound) == 0); + Assert.Equal(1, user.Credentials.Count); } @@ -875,10 +1193,12 @@ public async Task GivenNoCredential_ItRedirectsBackWithNoChangesMade() public async Task GivenValidRequest_ItRemovesCredAndSendsNotificationToUser() { // Arrange - var cred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "bloog"); - var user = Fakes.CreateUser("test", + var credentialBuilder = new CredentialBuilder(); + var fakes = Get(); + var cred = credentialBuilder.CreateExternalCredential("MicrosoftAccount", "blorg", "bloog"); + var user = fakes.CreateUser("test", cred, - CredentialBuilder.CreatePbkdf2Password("password")); + credentialBuilder.CreatePasswordCredential("password")); GetMock() .Setup(a => a.RemoveCredential(user, cred)) @@ -892,13 +1212,377 @@ public async Task GivenValidRequest_ItRemovesCredAndSendsNotificationToUser() controller.SetCurrentUser(user); // Act - var result = await controller.RemoveCredential(cred.Type); + var result = await controller.RemoveCredential( + credentialType: cred.Type, + credentialKey: null); // Assert ResultAssert.IsRedirectToRoute(result, new { action = "Account" }); GetMock().VerifyAll(); GetMock().VerifyAll(); } + + [Fact] + public async Task GivenValidRequest_CanDeleteMicrosoftAccountWithMultipleMicrosoftAccounts() + { + // Arrange + var fakes = Get(); + var creds = new Credential[5]; + for (int i = 0; i < creds.Length; i++) { + creds[i] = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "blorg", "bloog" + i); + creds[i].Key = i + 1; + } + + var user = fakes.CreateUser("test", creds); + var controller = GetController(); + controller.SetCurrentUser(user); + Assert.Equal(creds.Length, user.Credentials.Count); + + for (int i = 0; i < creds.Length - 1; i++) + { + // Act + var result = await controller.RemoveCredential( + credentialType: creds[i].Type, + credentialKey: creds[i].Key); + + // Assert + ResultAssert.IsRedirectToRoute(result, new { action = "Account" }); + Assert.Equal(Strings.CredentialRemoved, controller.TempData["Message"]); + Assert.Equal(creds.Length - i - 1, user.Credentials.Count); + } + } + } + + public class TheRegenerateCredentialAction : TestContainer + { + [Fact] + public async Task GivenNoCredential_ErrorIsReturnedWithNoChangesMade() + { + // Arrange + var fakes = Get(); + + var user = fakes.CreateUser("test", + new CredentialBuilder().CreateApiKey(TimeSpan.FromHours(1))); + var cred = user.Credentials.First(); + + var controller = GetController(); + controller.SetCurrentUser(user); + + // Act + var result = await controller.RegenerateCredential( + credentialType: cred.Type, + credentialKey: CredentialKey); + + // Assert + Assert.Equal((int)HttpStatusCode.NotFound, controller.Response.StatusCode); + Assert.IsType(result); + Assert.True(string.Compare((string)((JsonResult)result).Data, Strings.CredentialNotFound) == 0); + + Assert.Equal(1, user.Credentials.Count); + Assert.True(user.Credentials.Contains(cred)); + } + + [Theory] + [InlineData(CredentialTypes.ApiKey.V1)] + [InlineData(CredentialTypes.Password.V3)] + [InlineData(CredentialTypes.ExternalPrefix + "bla")] + public async Task GivenANonApiKeyV2Credential_ReturnsUnsupported(string credentialType) + { + // Arrange + var controller = GetController(); + + // Act + var result = await controller.RegenerateCredential( + credentialType: credentialType, + credentialKey: CredentialKey); + + // Assert + Assert.Equal((int)HttpStatusCode.BadRequest, controller.Response.StatusCode); + Assert.IsType(result); + Assert.True(string.Compare((string)((JsonResult)result).Data, Strings.Unsupported) == 0); + } + + public static IEnumerable RegenerateApiKeyCredential_Input + { + get + { + return new[] + { + new object[] + { + "permissions to several scopes, several packages", + new [] + { + new Scope("abc", NuGetScopes.PackageUnlist), + new Scope("abc", NuGetScopes.PackagePush), + new Scope("def", NuGetScopes.PackageUnlist), + new Scope("def", NuGetScopes.PackagePush) + } + }, + new object[] + { + "permissions to everything", + new [] + { + new Scope(null, NuGetScopes.All) + } + } + }; + } + } + + [MemberData(nameof(RegenerateApiKeyCredential_Input))] + [Theory] + public async Task GivenValidRequest_ItGeneratesNewCredAndRemovesOldCredAndSendsNotificationToUser( + string description, Scope[] scopes) + { + // Arrange + var fakes = Get(); + var apiKey = new CredentialBuilder().CreateApiKey(TimeSpan.FromHours(1)); + apiKey.Description = description; + apiKey.Scopes = scopes; + apiKey.Expires -= TimeSpan.FromDays(1); + + var user = fakes.CreateUser("test", apiKey); + var cred = user.Credentials.First(); + cred.Key = CredentialKey; + + GetMock() + .Setup(u => u.AddCredential( + user, + It.Is(c => c.Type == CredentialTypes.ApiKey.V2))) + .Callback((u, c) => u.Credentials.Add(c)) + .Completes() + .Verifiable(); + + GetMock() + .Setup(a => a.RemoveCredential(user, cred)) + .Callback((u, c) => u.Credentials.Remove(c)) + .Completes() + .Verifiable(); + + var controller = GetController(); + controller.SetCurrentUser(user); + + // Act + var result = await controller.RegenerateCredential( + credentialType: cred.Type, + credentialKey: CredentialKey); + + // Assert + Assert.IsType(result); + var credentialViewModel = ((JsonResult) result).Data as CredentialViewModel; + + Assert.NotNull(credentialViewModel); + + GetMock().VerifyAll(); + + var newApiKey = user.Credentials.FirstOrDefault(x => x.Type == CredentialTypes.ApiKey.V2); + + Assert.NotNull(newApiKey); + Assert.Equal(newApiKey.Value, credentialViewModel.Value); + Assert.Equal(newApiKey.Key, credentialViewModel.Key); + Assert.Equal(description, credentialViewModel.Description); + Assert.Equal(newApiKey.Expires, credentialViewModel.Expires); + + Assert.Equal(description, newApiKey.Description); + Assert.Equal(scopes.Length, newApiKey.Scopes.Count); + Assert.True(newApiKey.Expires > DateTime.UtcNow); + + foreach (var expectedScope in scopes) + { + var actualScope = + newApiKey.Scopes.First(x => x.AllowedAction == expectedScope.AllowedAction && + x.Subject == expectedScope.Subject); + Assert.NotNull(actualScope); + } + } + } + + public class TheEditCredentialAction : TestContainer + { + + [Theory] + [InlineData(CredentialTypes.ApiKey.V1)] + [InlineData(CredentialTypes.Password.V3)] + [InlineData(CredentialTypes.ExternalPrefix + "bla")] + public async Task GivenANonApiKeyV2Credential_ReturnsUnsupported(string credentialType) + { + // Arrange + var controller = GetController(); + + // Act + var result = await controller.EditCredential( + credentialType: credentialType, + credentialKey: CredentialKey, + subjects: new[] { "a", "b" }); + + // Assert + Assert.Equal((int)HttpStatusCode.BadRequest, controller.Response.StatusCode); + Assert.IsType(result); + Assert.True(string.CompareOrdinal((string)((JsonResult)result).Data, Strings.Unsupported) == 0); + } + + [Fact] + public async Task GivenNoCredential_ErrorIsReturnedWithNoChangesMade() + { + // Arrange + var fakes = Get(); + + var user = fakes.CreateUser("test", new CredentialBuilder().CreateApiKey(TimeSpan.FromHours(1))); + var cred = user.Credentials.First(); + + var authenticationService = GetMock(); + authenticationService + .Setup(x => x.RemoveCredential(It.IsAny(), It.IsAny())) + .Verifiable(); + + var controller = GetController(); + controller.SetCurrentUser(user); + + // Act + var result = await controller.EditCredential( + credentialType: cred.Type, + credentialKey: CredentialKey, + subjects: new[] { "a", "b" }); + + // Assert + Assert.Equal((int)HttpStatusCode.NotFound, controller.Response.StatusCode); + Assert.IsType(result); + Assert.True(String.CompareOrdinal((string)((JsonResult)result).Data, Strings.CredentialNotFound) == 0); + + authenticationService.Verify(x => x.EditCredentialScopes(It.IsAny(), It.IsAny(), It.IsAny>()), Times.Never); + } + + public static IEnumerable GivenValidRequest_ItEditsCredential_Input + { + get + { + return new[] + { + new object[] + { + new [] // Removal of subjects + { + new Scope("abc", NuGetScopes.PackageUnlist), + new Scope("abc", NuGetScopes.PackagePush), + new Scope("def", NuGetScopes.PackageUnlist), + new Scope("def", NuGetScopes.PackagePush) + }, + new [] { "def" }, + new [] + { + new Scope("def", NuGetScopes.PackageUnlist), + new Scope("def", NuGetScopes.PackagePush) + }, + }, + new object[] + { + new [] // Addition of subjects + { + new Scope("abc", NuGetScopes.PackageUnlist), + new Scope("abc", NuGetScopes.PackagePush), + }, + new [] { "abc", "def" }, + new [] + { + new Scope("abc", NuGetScopes.PackageUnlist), + new Scope("abc", NuGetScopes.PackagePush), + new Scope("def", NuGetScopes.PackageUnlist), + new Scope("def", NuGetScopes.PackagePush) + } + }, + new object[] + { + new [] // No subjects + { + new Scope("abc", NuGetScopes.PackageUnlist), + new Scope("abc", NuGetScopes.PackagePush) + }, + new string[] {}, + new [] + { + new Scope("*", NuGetScopes.PackageUnlist), + new Scope("*", NuGetScopes.PackagePush) + } + }, + }; + } + } + + [MemberData(nameof(GivenValidRequest_ItEditsCredential_Input))] + [Theory] + public async Task GivenValidRequest_ItEditsCredential(Scope[] existingScopes, string[] modifiedSubjects, Scope[] expectedScopes) + { + // Arrange + const string description = "description"; + var fakes = Get(); + var credentialBuilder = new CredentialBuilder(); + var apiKey = credentialBuilder.CreateApiKey(TimeSpan.FromHours(1)); + apiKey.Description = description; + apiKey.Scopes = existingScopes; + + var apiKeyExpirationTime = apiKey.Expires; + var apiKeyValue = apiKey.Value; + + + var user = fakes.CreateUser("test", apiKey, credentialBuilder.CreateApiKey(null)); + var cred = user.Credentials.First(); + cred.Key = CredentialKey; + + GetMock() + .Setup(a => a.EditCredentialScopes(user, cred, It.IsAny>())) + .Callback>((u, cr, scs) => + { + cr.Scopes = scs; + }) + .Completes() + .Verifiable(); + + var controller = GetController(); + controller.SetCurrentUser(user); + + // Act + var result = await controller.EditCredential( + credentialType: cred.Type, + credentialKey: CredentialKey, + subjects: modifiedSubjects); + + // Assert + GetMock().Verify(x => x.EditCredentialScopes(user, apiKey, It.IsAny>()), Times.Once); + + // Check return value + Assert.IsType(result); + var credentialViewModel = ((JsonResult)result).Data as CredentialViewModel; + Assert.NotNull(credentialViewModel); + + Assert.Null(credentialViewModel.Value); + Assert.Equal(description, credentialViewModel.Description); + Assert.Equal(expectedScopes.Length, credentialViewModel.Scopes.Count); + + foreach (var expectedScope in expectedScopes) + { + var expectedAction = NuGetScopes.Describe(expectedScope.AllowedAction); + var actualScope = + credentialViewModel.Scopes.First(x => x.AllowedAction == expectedAction && + x.Subject == expectedScope.Subject); + Assert.NotNull(actualScope); + } + + // Check edited value + Assert.Equal(expectedScopes.Length, apiKey.Scopes.Count); + Assert.Equal(apiKeyExpirationTime, apiKey.Expires); // Expiration time wasn't modified by edit + Assert.Equal(description, apiKey.Description); // Description wasn't modified + Assert.Equal(apiKeyValue, apiKey.Value); // Value wasn't modified + + foreach (var expectedScope in expectedScopes) + { + var actualScope = + apiKey.Scopes.First(x => x.AllowedAction == expectedScope.AllowedAction && + x.Subject == expectedScope.Subject); + Assert.NotNull(actualScope); + } + } } } } diff --git a/tests/NuGetGallery.Facts/Filters/ApiAuthorizeAttributeFacts.cs b/tests/NuGetGallery.Facts/Filters/ApiAuthorizeAttributeFacts.cs index c318c6cf95..1f8206d404 100644 --- a/tests/NuGetGallery.Facts/Filters/ApiAuthorizeAttributeFacts.cs +++ b/tests/NuGetGallery.Facts/Filters/ApiAuthorizeAttributeFacts.cs @@ -24,6 +24,7 @@ public void ApiAuthorizeAttributeReturns401() var mockAuthContext = new Mock(MockBehavior.Strict); mockAuthContext.SetupGet(c => c.ActionDescriptor).Returns(actionDescriptor.Object); mockAuthContext.SetupGet(c => c.HttpContext).Returns(httpContext.Object); + mockAuthContext.SetupGet(c => c.Controller).Returns((Controller)null); var context = mockAuthContext.Object; var attribute = new ApiAuthorizeAttribute(); diff --git a/tests/NuGetGallery.Facts/Framework/Fakes.cs b/tests/NuGetGallery.Facts/Framework/Fakes.cs index adf78e35eb..42fcc04dad 100644 --- a/tests/NuGetGallery.Facts/Framework/Fakes.cs +++ b/tests/NuGetGallery.Facts/Framework/Fakes.cs @@ -8,54 +8,99 @@ using System.Security.Principal; using Microsoft.Owin; using Moq; +using NuGetGallery.Authentication; +using NuGetGallery.Infrastructure.Authentication; namespace NuGetGallery.Framework { - public static class Fakes + public class Fakes { + public static TimeSpan ExpirationForApiKeyV1 = TimeSpan.FromDays(90); + public static readonly string Password = "p@ssw0rd!"; + + public Fakes() + { + User = new User("testUser") + { + Key = 40, + EmailAddress = "confirmed0@example.com", + Credentials = new List + { + new CredentialBuilder().CreatePasswordCredential(Password), + TestCredentialHelper.CreateV1ApiKey(Guid.Parse("669e180e-335c-491a-ac26-e83c4bd31d65"), + ExpirationForApiKeyV1), + TestCredentialHelper.CreateV2ApiKey(Guid.Parse("779e180e-335c-491a-ac26-e83c4bd31d87"), + ExpirationForApiKeyV1), + TestCredentialHelper.CreateV2VerificationApiKey(Guid.Parse("b0c51551-823f-4701-8496-43980b4b3913")), + TestCredentialHelper.CreateExternalCredential("abc") + } + }; - public static readonly User User = new User("testUser") { - Key = 42, - EmailAddress = "confirmed1@example.com", - Credentials = new List() { - CredentialBuilder.CreatePbkdf2Password(Password), - CredentialBuilder.CreateV1ApiKey(Guid.Parse("519e180e-335c-491a-ac26-e83c4bd31d65")) - } - }; + Pbkdf2User = new User("testPbkdf2User") + { + Key = 41, + EmailAddress = "confirmed1@example.com", + Credentials = new List + { + TestCredentialHelper.CreatePbkdf2Password(Password), + TestCredentialHelper.CreateV1ApiKey(Guid.Parse("519e180e-335c-491a-ac26-e83c4bd31d65"), + ExpirationForApiKeyV1) + } + }; - public static readonly User ShaUser = new User("testShaUser") - { - Key = 42, - EmailAddress = "confirmed2@example.com", - Credentials = new List() { - CredentialBuilder.CreateSha1Password(Password), - CredentialBuilder.CreateV1ApiKey(Guid.Parse("b9704a41-4107-4cd2-bcfa-70d84e021ab2")) - } - }; - public static readonly User Admin = new User("testAdmin") { - Key = 43, - EmailAddress = "confirmed3@example.com", - Credentials = new List() { CredentialBuilder.CreatePbkdf2Password(Password) }, - Roles = new List() { new Role() { Name = Constants.AdminRoleName } } - }; - public static readonly User Owner = new User("testPackageOwner") { - Key = 44, - Credentials = new List() { CredentialBuilder.CreatePbkdf2Password(Password) }, - EmailAddress = "confirmed@example.com" //package owners need confirmed email addresses, obviously. - }; - - public static readonly PackageRegistration Package = new PackageRegistration() - { - Id = "FakePackage", - Owners = new List() { Owner }, - Packages = new List() { - new Package() { Version = "1.0" }, - new Package() { Version = "2.0" } - } - }; + ShaUser = new User("testShaUser") + { + Key = 42, + EmailAddress = "confirmed2@example.com", + Credentials = new List + { + TestCredentialHelper.CreateSha1Password(Password), + TestCredentialHelper.CreateV1ApiKey(Guid.Parse("b9704a41-4107-4cd2-bcfa-70d84e021ab2"), + ExpirationForApiKeyV1) + } + }; - public static User CreateUser(string userName, params Credential[] credentials) + Admin = new User("testAdmin") + { + Key = 43, + EmailAddress = "confirmed3@example.com", + Credentials = new List { TestCredentialHelper.CreatePbkdf2Password(Password)}, + Roles = new List {new Role {Name = Constants.AdminRoleName}} + }; + + Owner = new User("testPackageOwner") + { + Key = 44, + Credentials = new List { TestCredentialHelper.CreatePbkdf2Password(Password)}, + EmailAddress = "confirmed@example.com" //package owners need confirmed email addresses, obviously. + }; + + Package = new PackageRegistration + { + Id = "FakePackage", + Owners = new List {Owner}, + Packages = new List + { + new Package {Version = "1.0"}, + new Package {Version = "2.0"} + } + }; + } + + public User User { get; } + + public User ShaUser { get; } + + public User Pbkdf2User { get; } + + public User Admin { get; } + + public User Owner { get; } + + public PackageRegistration Package { get; } + + public User CreateUser(string userName, params Credential[] credentials) { return new User(userName) { @@ -64,7 +109,7 @@ public static User CreateUser(string userName, params Credential[] credentials) }; } - public static ClaimsPrincipal ToPrincipal(this User user) + public static ClaimsPrincipal ToPrincipal(User user) { ClaimsIdentity identity = new ClaimsIdentity( claims: Enumerable.Concat(new[] { @@ -73,19 +118,21 @@ public static ClaimsPrincipal ToPrincipal(this User user) authenticationType: "Test", nameType: ClaimsIdentity.DefaultNameClaimType, roleType: ClaimsIdentity.DefaultRoleClaimType); + return new ClaimsPrincipal(identity); } - public static IIdentity ToIdentity(this User user) + public static IIdentity ToIdentity(User user) { return new GenericIdentity(user.Username); } - internal static void ConfigureEntitiesContext(FakeEntitiesContext ctxt) + internal void ConfigureEntitiesContext(FakeEntitiesContext ctxt) { // Add Users var users = ctxt.Set(); users.Add(User); + users.Add(Pbkdf2User); users.Add(ShaUser); users.Add(Admin); users.Add(Owner); diff --git a/tests/NuGetGallery.Facts/Framework/TestAuditingService.cs b/tests/NuGetGallery.Facts/Framework/TestAuditingService.cs index b556f9bb50..38d4e33886 100644 --- a/tests/NuGetGallery.Facts/Framework/TestAuditingService.cs +++ b/tests/NuGetGallery.Facts/Framework/TestAuditingService.cs @@ -1,37 +1,30 @@ // 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.Text; using System.Threading.Tasks; using NuGetGallery.Auditing; -using Xunit; namespace NuGetGallery.Framework { - public class TestAuditingService : AuditingService + public class TestAuditingService : IAuditingService { private List _records = new List(); public IReadOnlyList Records { get { return _records.AsReadOnly(); } } - public override Task SaveAuditRecord(AuditRecord record) + public Task SaveAuditRecordAsync(AuditRecord record) { _records.Add(record); - return Task.FromResult(new Uri("http://nuget.local/auditing/test")); - } - - protected override Task SaveAuditRecord(string auditData, string resourceType, string filePath, string action, DateTime timestamp) - { - // Not necessary since we override the only caller of this protected method - throw new NotImplementedException(); + return Task.FromResult(0); } } public static class AuditingServiceTestExtensions { - public static bool WroteRecord(this AuditingService self, Func predicate) where T : AuditRecord + public static bool WroteRecord(this IAuditingService self, Func predicate) where T : AuditRecord { TestAuditingService testService = self as TestAuditingService; if (testService != null) @@ -41,7 +34,7 @@ public static bool WroteRecord(this AuditingService self, Func predi return false; } - public static bool WroteRecordOfType(this AuditingService self) where T : AuditRecord + public static bool WroteRecordOfType(this IAuditingService self) where T : AuditRecord { return WroteRecord(self, _ => true); } diff --git a/tests/NuGetGallery.Facts/Framework/TestContainer.cs b/tests/NuGetGallery.Facts/Framework/TestContainer.cs index 632bbfad06..8e0c5ec6cc 100644 --- a/tests/NuGetGallery.Facts/Framework/TestContainer.cs +++ b/tests/NuGetGallery.Facts/Framework/TestContainer.cs @@ -6,7 +6,6 @@ using System.Web.Mvc; using System.Web.Routing; using Autofac; -using Microsoft.Owin; using Moq; using NuGetGallery.Configuration; @@ -47,8 +46,8 @@ protected TController GetController() where TController : Controlle var appCtrl = c as AppController; if (appCtrl != null) { - appCtrl.OwinContext = Container.Resolve(); - appCtrl.NuGetContext.Config = Container.Resolve(); + appCtrl.SetOwinContextOverride(Fakes.CreateOwinContext()); + appCtrl.NuGetContext.Config = Container.Resolve(); } return c; @@ -65,7 +64,12 @@ protected TService GetService() protected FakeEntitiesContext GetFakeContext() { - var fakeContext = new FakeEntitiesContext(); + var fakeContext = Container.Resolve() as FakeEntitiesContext; + + if (fakeContext == null) + { + fakeContext = new FakeEntitiesContext(); + } var updater = new ContainerBuilder(); updater.RegisterInstance(fakeContext).As(); @@ -86,9 +90,10 @@ protected FakeEntitiesContext GetFakeContext() protected T Get() { - if(typeof(Controller).IsAssignableFrom(typeof(T))) { + if (typeof(Controller).IsAssignableFrom(typeof(T))) { throw new InvalidOperationException("Use GetController to get a controller instance"); } + return Container.Resolve(); } diff --git a/tests/NuGetGallery.Facts/Framework/TestExtensionMethods.cs b/tests/NuGetGallery.Facts/Framework/TestExtensionMethods.cs index 2d6420ee7a..e9d2790178 100644 --- a/tests/NuGetGallery.Facts/Framework/TestExtensionMethods.cs +++ b/tests/NuGetGallery.Facts/Framework/TestExtensionMethods.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Owin; using Moq; +using NuGetGallery.Authentication; namespace NuGetGallery { @@ -18,12 +19,25 @@ public static class TestExtensionMethods /// does NOT use AppController.GetCurrentUser()! In those cases, use /// TestExtensionMethods.SetCurrentUser(AppController, User) instead. /// - /// - public static void SetCurrentUser(this AppController self, string name) + public static void SetOwinContextCurrentUser(this AppController self, User user, string scopes = null) { - var principal = new ClaimsPrincipal( - new ClaimsIdentity( - new [] { new Claim(ClaimTypes.Name, String.IsNullOrEmpty(name) ? "theUserName" : name) })); + ClaimsIdentity identity = null; + + if (scopes != null) + { + identity = AuthenticationService.CreateIdentity( + user, + AuthenticationTypes.ApiKey, + new Claim(NuGetClaims.ApiKey, string.Empty), + new Claim(NuGetClaims.Scope, scopes)); + } + else + { + identity = new ClaimsIdentity( + new[] { new Claim(ClaimTypes.Name, string.IsNullOrEmpty(user.Username) ? "theUserName" : user.Username) }); + } + + var principal = new ClaimsPrincipal(identity); var mock = Mock.Get(self.HttpContext); mock.Setup(c => c.Request.IsAuthenticated).Returns(true); @@ -32,9 +46,9 @@ public static void SetCurrentUser(this AppController self, string name) self.OwinContext.Request.User = principal; } - public static void SetCurrentUser(this AppController self, User user) + public static void SetCurrentUser(this AppController self, User user, string scopes = null) { - SetCurrentUser(self, user.Username); + SetOwinContextCurrentUser(self, user, scopes); self.OwinContext.Environment[Constants.CurrentUserOwinEnvironmentKey] = user; } @@ -58,4 +72,3 @@ public static Task CaptureBodyAsString(this IOwinResponse self, Func Settings = new Dictionary(); + + public TestGalleryConfigurationService() : base(new EmptySecretReaderFactory()) + { + } + + protected override string GetAppSetting(string settingName) + { + if (Settings.ContainsKey(settingName)) + { + return Settings[settingName]; + } + + // Will cause ResolveConfigObject to populate a class with default values. + return string.Empty; + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Framework/UnitTestBindings.cs b/tests/NuGetGallery.Facts/Framework/UnitTestBindings.cs index 82bc816c98..ff58d83b78 100644 --- a/tests/NuGetGallery.Facts/Framework/UnitTestBindings.cs +++ b/tests/NuGetGallery.Facts/Framework/UnitTestBindings.cs @@ -8,6 +8,8 @@ using Moq; using NuGetGallery.Auditing; using NuGetGallery.Authentication; +using NuGetGallery.Configuration; +using NuGetGallery.Infrastructure.Authentication; namespace NuGetGallery.Framework { @@ -34,8 +36,14 @@ internal static IContainer CreateContainer(bool autoMock) protected override void Load(ContainerBuilder builder) { + var fakes = new Fakes(); + + builder.RegisterInstance(fakes) + .As() + .SingleInstance(); + builder.RegisterType() - .As(); + .As(); builder.Register(_ => { @@ -52,8 +60,8 @@ protected override void Load(ContainerBuilder builder) { var mockService = new Mock(); mockService - .Setup(p => p.FindPackageRegistrationById(Fakes.Package.Id)) - .Returns(Fakes.Package); + .Setup(p => p.FindPackageRegistrationById(fakes.Package.Id)) + .Returns(fakes.Package); return mockService.Object; }) .As() @@ -62,18 +70,17 @@ protected override void Load(ContainerBuilder builder) builder.Register(_ => { var mockService = new Mock(); - mockService.Setup(u => u.FindByUsername(Fakes.User.Username)).Returns(Fakes.User); - mockService.Setup(u => u.FindByUsername(Fakes.Owner.Username)).Returns(Fakes.Owner); - mockService.Setup(u => u.FindByUsername(Fakes.Admin.Username)).Returns(Fakes.Admin); + mockService.Setup(u => u.FindByUsername(fakes.User.Username)).Returns(fakes.User); + mockService.Setup(u => u.FindByUsername(fakes.Owner.Username)).Returns(fakes.Owner); + mockService.Setup(u => u.FindByUsername(fakes.Admin.Username)).Returns(fakes.Admin); return mockService.Object; - }) - .As() - .SingleInstance(); + }).As() + .SingleInstance(); builder.Register(_ => { var ctxt = new FakeEntitiesContext(); - Fakes.ConfigureEntitiesContext(ctxt); + fakes.ConfigureEntitiesContext(ctxt); return ctxt; }) .As() @@ -82,6 +89,14 @@ protected override void Load(ContainerBuilder builder) builder.Register(_ => Fakes.CreateOwinContext()) .As() .SingleInstance(); + + builder.Register(_ => new TestGalleryConfigurationService()) + .As() + .SingleInstance(); + + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); } } } diff --git a/tests/NuGetGallery.Facts/Infrastructure/CookieTempDataProviderFacts.cs b/tests/NuGetGallery.Facts/Infrastructure/CookieTempDataProviderFacts.cs index e6a54185c2..0d2633f1f6 100644 --- a/tests/NuGetGallery.Facts/Infrastructure/CookieTempDataProviderFacts.cs +++ b/tests/NuGetGallery.Facts/Infrastructure/CookieTempDataProviderFacts.cs @@ -18,6 +18,7 @@ public void RetrievesValuesFromCookie() { var cookies = new HttpCookieCollection(); var cookie = new HttpCookie("__Controller::TempData"); + cookie.HttpOnly = true; cookies.Add(cookie); cookie["message"] = "Say hello to my little friend"; cookie["question"] = "How am I funny?"; @@ -53,6 +54,7 @@ public void WithEmptyCookieReturnsEmptyDictionary() { var cookies = new HttpCookieCollection(); var cookie = new HttpCookie("__Controller::TempData"); + cookie.HttpOnly = true; cookies.Add(cookie); var httpContext = new Mock(); httpContext.Setup(c => c.Request.Cookies).Returns(cookies); @@ -87,6 +89,7 @@ public void StoresValuesInCookie() Assert.Equal(1, cookies.Count); Assert.True(cookies[0].HttpOnly); + Assert.True(cookies[0].Secure); Assert.Equal(3, cookies[0].Values.Count); Assert.Equal("Say hello to my little friend", cookies[0]["message"]); Assert.Equal("123", cookies[0]["key2"]); @@ -106,6 +109,31 @@ public void WithNoValuesDoesNotAddCookie() Assert.Equal(0, cookies.Count); } + + [Fact] + public void WithInitialStateAndNoValuesClearsCookie() + { + // Arrange and Setup + var cookies = new HttpCookieCollection(); + var cookie = new HttpCookie("__Controller::TempData"); + cookie.HttpOnly = true; + cookie.Secure = true; + cookies.Add(cookie); + cookie["message"] = "clear"; + var httpContext = new Mock(); + httpContext.Setup(c => c.Request.Cookies).Returns(cookies); + ITempDataProvider provider = new CookieTempDataProvider(httpContext.Object); + var controllerContext = new ControllerContext(); + + var tempData = provider.LoadTempData(controllerContext); + + // Validate + provider.SaveTempData(controllerContext, new Dictionary()); + Assert.Equal(1, cookies.Count); + Assert.True(cookies[0].HttpOnly); + Assert.True(cookies[0].Secure); + Assert.Equal("", cookies[0].Value); + } } } } \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Infrastructure/LuceneSearchServiceFacts.cs b/tests/NuGetGallery.Facts/Infrastructure/LuceneSearchServiceFacts.cs index 71b21f6252..8db43c6de0 100644 --- a/tests/NuGetGallery.Facts/Infrastructure/LuceneSearchServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Infrastructure/LuceneSearchServiceFacts.cs @@ -304,7 +304,6 @@ public void SearchUsingExactPackageId() [InlineData("Owners", "NugetCoreOwner")] [InlineData("Authors", "Alpha")] [InlineData("Author", "Alpha")] - [InlineData("Authors", "Alpha")] [InlineData("author", "\"Alpha Beta Gamma\"")] [InlineData("Description", "core framework")] [InlineData("Tags", "dotnet")] @@ -493,6 +492,7 @@ public void IndexAndSearchRetrievesCanDriveV2Feed() DownloadCount = 12345, FlattenedDependencies = "adjunct-System.FluentCast:1.0.0.4|xunit:1.8.0.1545|adjunct-XUnit.Assertions:1.0.0.5|adjunct-XUnit.Assertions.Linq2Xml:1.0.0.3", HashAlgorithm = "SHA512", + // This is a test hash Hash = "Ii4+Gr44RAClAno38k5MYAkcBE6yn2LE2xO+/ViKco45+hoxtwKAytmPWEMCJWhH8FyitjebvS5Fsf+ixI5xIg==", IsLatest = true, IsLatestStable = true, diff --git a/tests/NuGetGallery.Facts/Infrastructure/UserAuditRecordFacts.cs b/tests/NuGetGallery.Facts/Infrastructure/UserAuditRecordFacts.cs new file mode 100644 index 0000000000..3e47b2c189 --- /dev/null +++ b/tests/NuGetGallery.Facts/Infrastructure/UserAuditRecordFacts.cs @@ -0,0 +1,54 @@ +// 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 NuGetGallery.Auditing; +using NuGetGallery.Authentication; +using NuGetGallery.Framework; +using NuGetGallery.Infrastructure.Authentication; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace NuGetGallery.Infrastructure +{ + public class UserAuditRecordFacts + { + [Fact] + public void FiltersOutUnsupportedCredentials() + { + // Arrange + var credentialBuilder = new CredentialBuilder(); + var credentials = new List { + credentialBuilder.CreatePasswordCredential("v3"), + TestCredentialHelper.CreatePbkdf2Password("pbkdf2"), + TestCredentialHelper.CreateSha1Password("sha1"), + TestCredentialHelper.CreateV1ApiKey(Guid.NewGuid(), Fakes.ExpirationForApiKeyV1), + TestCredentialHelper.CreateV2ApiKey(Guid.NewGuid(), Fakes.ExpirationForApiKeyV1), + TestCredentialHelper.CreateV2VerificationApiKey(Guid.NewGuid()), + credentialBuilder.CreateExternalCredential("MicrosoftAccount", "blarg", "Bloog"), + new Credential { Type = "unsupported" } + }; + + var user = new User + { + Username = "name", + Credentials = credentials + }; + + // Act + var userAuditRecord = new UserAuditRecord(user, AuditedUserAction.AddCredential); + + // Assert + var auditRecords = userAuditRecord.Credentials.ToDictionary(c => c.Type); + Assert.Equal(7, auditRecords.Count); + Assert.True(auditRecords.ContainsKey(credentials[0].Type)); + Assert.True(auditRecords.ContainsKey(credentials[1].Type)); + Assert.True(auditRecords.ContainsKey(credentials[2].Type)); + Assert.True(auditRecords.ContainsKey(credentials[3].Type)); + Assert.True(auditRecords.ContainsKey(credentials[4].Type)); + Assert.True(auditRecords.ContainsKey(credentials[5].Type)); + Assert.True(auditRecords.ContainsKey(credentials[6].Type)); + } + } +} diff --git a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj index b28a4b2669..614d869dc4 100644 --- a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj +++ b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj @@ -2,7 +2,6 @@ - Debug AnyCPU @@ -16,7 +15,6 @@ v4.6.1 512 - true @@ -31,7 +29,7 @@ true false 0618 - ManagedMinimumRules.ruleset + Sdl7.0.ruleset pdbonly @@ -53,6 +51,9 @@ ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll True + + ..\..\packages\Castle.Core.4.0.0\lib\net45\Castle.Core.dll + False ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll @@ -63,6 +64,10 @@ ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll True + + ..\..\packages\Hyak.Common.1.0.2\lib\net45\Hyak.Common.dll + True + False ..\..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll @@ -78,6 +83,21 @@ ..\..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll True + + ..\..\packages\Microsoft.Azure.Common.2.0.4\lib\net45\Microsoft.Azure.Common.dll + True + + + ..\..\packages\Microsoft.Azure.Common.2.0.4\lib\net45\Microsoft.Azure.Common.NetFramework.dll + True + + + ..\..\packages\Microsoft.Azure.KeyVault.1.0.0\lib\net45\Microsoft.Azure.KeyVault.dll + True + + + ..\..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll + False ..\..\packages\Microsoft.Data.Edm.5.6.5-beta\lib\net40\Microsoft.Data.Edm.dll @@ -98,6 +118,14 @@ ..\..\packages\Microsoft.Data.Services.Client.5.6.5-beta\lib\net40\Microsoft.Data.Services.Client.dll True + + ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.4\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + True + + + ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.4\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll + True + False ..\..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll @@ -142,65 +170,56 @@ ..\..\packages\Microsoft.Web.Xdt.2.1.1\lib\net40\Microsoft.Web.XmlTransform.dll True + + ..\..\packages\Microsoft.Win32.Primitives.4.0.1\lib\net46\Microsoft.Win32.Primitives.dll + True + False ..\..\packages\Microsoft.WindowsAzure.ConfigurationManager.3.1.0\lib\net40\Microsoft.WindowsAzure.Configuration.dll True - - False - ..\..\packages\WindowsAzure.Storage.4.3.0\lib\net40\Microsoft.WindowsAzure.Storage.dll - True + + ..\..\packages\WindowsAzure.Storage.7.0.0\lib\net40\Microsoft.WindowsAzure.Storage.dll - - False - ..\..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll - True + + ..\..\packages\Moq.4.7.0\lib\net45\Moq.dll False ..\..\packages\MvcHaack.Ajax.MVC4.2.0.0.0\lib\net40\MvcHaack.Ajax.dll True - - ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - True + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - - False - ..\..\packages\NuGet.Common.3.5.0-beta-final\lib\net45\NuGet.Common.dll - True + + ..\..\packages\NuGet.Common.4.0.0\lib\net45\NuGet.Common.dll - - False - ..\..\packages\NuGet.Frameworks.3.5.0-beta-final\lib\net45\NuGet.Frameworks.dll - True + + ..\..\packages\NuGet.Frameworks.4.0.0\lib\net45\NuGet.Frameworks.dll False ..\..\packages\NuGet.Logging.3.5.0-beta-1160\lib\net45\NuGet.Logging.dll True - - False - ..\..\packages\NuGet.Packaging.3.5.0-beta-final\lib\net45\NuGet.Packaging.dll - True + + ..\..\packages\NuGet.Packaging.4.0.0\lib\net45\NuGet.Packaging.dll - - False - ..\..\packages\NuGet.Packaging.Core.3.5.0-beta-final\lib\net45\NuGet.Packaging.Core.dll - True + + ..\..\packages\NuGet.Packaging.Core.4.0.0\lib\net45\NuGet.Packaging.Core.dll - - False - ..\..\packages\NuGet.Packaging.Core.Types.3.5.0-beta-final\lib\net45\NuGet.Packaging.Core.Types.dll - True + + ..\..\packages\NuGet.Packaging.Core.Types.4.0.0\lib\net45\NuGet.Packaging.Core.Types.dll - - False - ..\..\packages\NuGet.Versioning.3.5.0-beta-final\lib\net45\NuGet.Versioning.dll + + ..\..\packages\NuGet.Services.KeyVault.1.0.0.0\lib\net45\NuGet.Services.KeyVault.dll True + + ..\..\packages\NuGet.Versioning.4.0.0\lib\net45\NuGet.Versioning.dll + False ..\..\packages\Owin.1.0\lib\net40\Owin.dll @@ -217,13 +236,21 @@ True + + + ..\..\packages\System.Diagnostics.DiagnosticSource.4.0.0\lib\net46\System.Diagnostics.DiagnosticSource.dll + True + - + + ..\..\packages\System.Net.Http.4.3.0-beta-24431-01\lib\net46\System.Net.Http.dll + True + False ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll @@ -240,6 +267,23 @@ True + + + ..\..\packages\System.Security.Cryptography.Algorithms.4.2.0\lib\net461\System.Security.Cryptography.Algorithms.dll + True + + + ..\..\packages\System.Security.Cryptography.Encoding.4.0.0\lib\net46\System.Security.Cryptography.Encoding.dll + True + + + ..\..\packages\System.Security.Cryptography.Primitives.4.0.0\lib\net46\System.Security.Cryptography.Primitives.dll + True + + + ..\..\packages\System.Security.Cryptography.X509Certificates.4.1.0\lib\net461\System.Security.Cryptography.X509Certificates.dll + True + False @@ -330,14 +374,18 @@ + + + + @@ -347,10 +395,17 @@ + + + + + + + @@ -419,12 +474,17 @@ + - - + + Designer + + + Designer + @@ -444,17 +504,13 @@ - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/OData/Filter/ODataFilterFacts.cs b/tests/NuGetGallery.Facts/OData/Filter/ODataFilterFacts.cs new file mode 100644 index 0000000000..1f31401df5 --- /dev/null +++ b/tests/NuGetGallery.Facts/OData/Filter/ODataFilterFacts.cs @@ -0,0 +1,55 @@ +// 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.Net.Http; +using System.Web.Http.OData; +using System.Web.Http.OData.Query; +using NuGetGallery.OData.QueryFilter; +using Xunit; + +namespace NuGetGallery.OData.Filter +{ + public class ODataFilterFacts + { + const string Host = "https://localhost:8081/"; + + [Theory] + [InlineData("apiv2getupdates.json")] + [InlineData("apiv2packages.json")] + [InlineData("apiv2search.json")] + public void ODataNoOperatorsAreAllowedV2(string apiResourceFileName) + { + // Arrange + var odataOptions = new ODataQueryOptions( + new ODataQueryContext(NuGetODataV2FeedConfig.GetEdmModel(), typeof(V2FeedPackage)), + new HttpRequestMessage(HttpMethod.Get, Host)); + + var queryFilter = new ODataQueryFilter(apiResourceFileName); + + // Act + var result = ODataQueryVerifier.AreODataOptionsAllowed(odataOptions, queryFilter, true,"TestContext"); + + // Assert + Assert.True(result, "A request with no OData operators should be allowed."); + } + + [Theory] + [InlineData("apiv1packages.json")] + [InlineData("apiv1search.json")] + public void ODataNoOperatorsAreAllowedV1(string apiResourceFileName) + { + // Arrange + var odataOptions = new ODataQueryOptions( + new ODataQueryContext(NuGetODataV1FeedConfig.GetEdmModel(), typeof(V1FeedPackage)), + new HttpRequestMessage(HttpMethod.Get, Host)); + + var queryFilter = new ODataQueryFilter(apiResourceFileName); + + // Act + var result = ODataQueryVerifier.AreODataOptionsAllowed(odataOptions, queryFilter, true, "TestContext"); + + // Assert + Assert.True(result, "A request with no OData operators should be allowed."); + } + } +} diff --git a/tests/NuGetGallery.Facts/OData/Interceptors/PackageExtensionsFacts.cs b/tests/NuGetGallery.Facts/OData/Interceptors/PackageExtensionsFacts.cs index 4c0bcdf3de..3bcc61bc79 100644 --- a/tests/NuGetGallery.Facts/OData/Interceptors/PackageExtensionsFacts.cs +++ b/tests/NuGetGallery.Facts/OData/Interceptors/PackageExtensionsFacts.cs @@ -51,7 +51,7 @@ public void MapsBasicPackagePropertiesCorrectly() Assert.Equal("Mostly Harmless", actual.ReleaseNotes); Assert.True(actual.RequireLicenseAcceptance); Assert.Equal(new DateTime(1979, 10, 12), actual.Published); - Assert.Equal("A truely remarkable book", actual.Summary); + Assert.Equal("A truly remarkable book", actual.Summary); Assert.Equal("Guide, Harmless, Mostly", actual.Tags); Assert.Equal("The Hitchhiker's Guide to the Galaxy", actual.Title); Assert.Equal(421, actual.VersionDownloadCount); @@ -158,7 +158,7 @@ public static Package CreateFakeBasePackage() ReleaseNotes = "Mostly Harmless", RequiresLicenseAcceptance = true, Published = new DateTime(1979, 10, 12), - Summary = "A truely remarkable book", + Summary = "A truly remarkable book", Tags = "Guide, Harmless, Mostly", Title = "The Hitchhiker's Guide to the Galaxy", DownloadCount = 421, diff --git a/tests/NuGetGallery.Facts/OData/SearchService/SearchHijackerFacts.cs b/tests/NuGetGallery.Facts/OData/SearchService/SearchHijackerFacts.cs new file mode 100644 index 0000000000..34ebcfe104 --- /dev/null +++ b/tests/NuGetGallery.Facts/OData/SearchService/SearchHijackerFacts.cs @@ -0,0 +1,61 @@ +// 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.Net.Http; +using System.Web.Http.OData; +using System.Web.Http.OData.Query; +using NuGetGallery.OData; +using Xunit; + +namespace NuGetGallery +{ + public class SearchHijackerFacts + { + protected static ODataQueryOptions GetODataQueryOptionsForTest(Uri requestUri) + { + return new ODataQueryOptions( + new ODataQueryContext(NuGetODataV2FeedConfig.GetEdmModel(), typeof(V2FeedPackage)), + new HttpRequestMessage(HttpMethod.Get, requestUri)); + } + + public class TheIsHijackableMethod + { + public static IEnumerable IsHijackableReturnsFalseIfFilterContainsSubstringOf_Input + { + get + { + return new [] + { + // Valid substringof in SingleValueExpression + new object[] { "https://nuget.localtest.me/api/v2/Packages()?$filter=substringof(Id,%27MyPackage%27)" }, + // Valid substringof in BinaryOperatorExpression, nested in ConvertNode + new object[] { "https://nuget.localtest.me/api/v2/Packages()?$filter=substringof(Id,%27MyPackage%27)%20and%20Id%20eq%20%27MyPackageId%27" }, + // Invalid substringof in SingleValueExpression + new object[] { "https://localhost:8081/api/v2/Packages?$filter=substringof(null,Tags)" }, + // Invalid substringof in left-most node of BinaryOperationExpression (traversal is right to left) + new object[] { "https://nuget.localtest.me/api/v2/Packages()?$filter=substringof(null,Tags)%20and%20IsLatestVersion%20and%20IsLatestVersion" }, + // Invalid substringof in right node of BinaryOperationExpression (traversal is right to left) + new object[] { "https://nuget.localtest.me/api/v2/Packages()?$filter=IsLatestVersion%20and%20substringof(null,Tags)" } + }; + } + } + + [Theory] + [MemberData("IsHijackableReturnsFalseIfFilterContainsSubstringOf_Input")] + public void IsHijackableReturnsFalseIfFilterContainsSubstringOf(string uri) + { + // Arrange + var requestUri = new Uri(uri); + + // Act + HijackableQueryParameters hijackableQueryParameters = null; + var result = SearchHijacker.IsHijackable(GetODataQueryOptionsForTest(requestUri), out hijackableQueryParameters); + + // Assert + Assert.False(result); + } + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/PasswordValidationRegexTests.cs b/tests/NuGetGallery.Facts/PasswordValidationRegexTests.cs new file mode 100644 index 0000000000..d2552db259 --- /dev/null +++ b/tests/NuGetGallery.Facts/PasswordValidationRegexTests.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.Text.RegularExpressions; +using NuGetGallery.Configuration; +using NuGetGallery.Framework; +using Xunit; + +namespace NuGetGallery +{ + /// + /// The regex checks that the password is at least 8 characters, one uppercase letter, one lowercase letter, and a digit. + /// + public class PasswordValidationRegexTests : TestContainer + { + private readonly string _defaultPasswordRegex; + + public PasswordValidationRegexTests() + { + var configuration = Get(); + _defaultPasswordRegex = configuration.Current.UserPasswordRegex; + } + + [Theory] + [InlineData("aA1aaaaa")] + [InlineData("abcdefg$0B")] + [InlineData("****1bB***")] + public void Accepts(string password) + { + + var match = new Regex(_defaultPasswordRegex).IsMatch(password); + Assert.True(match); + } + + [Theory] + [InlineData("v")] // Single letter + [InlineData("V")] // Single upper case letter + [InlineData("8")] // Single number + [InlineData("89984214214")] // Just numbers + [InlineData("%*`~&*()%#@$!@<>?\"")] // Special characters + [InlineData("aaAAaaAAaaAA")] // No digit + [InlineData("12345678a")] // No uppercase letter + [InlineData("12345678A")] // No lowercase letter + [InlineData("1aA")] // Too short + public void DoesNotAccept(string password) + { + var match = new Regex(_defaultPasswordRegex).IsMatch(password); + Assert.False(match); + } + } +} diff --git a/tests/NuGetGallery.Facts/SearchClient/RetryingHttpClientWrapperFacts.cs b/tests/NuGetGallery.Facts/SearchClient/RetryingHttpClientWrapperFacts.cs index f19eadaf5d..fc4d502fc7 100644 --- a/tests/NuGetGallery.Facts/SearchClient/RetryingHttpClientWrapperFacts.cs +++ b/tests/NuGetGallery.Facts/SearchClient/RetryingHttpClientWrapperFacts.cs @@ -14,7 +14,7 @@ namespace NuGetGallery.SearchClient public class RetryingHttpClientWrapperFacts { private static readonly Uri ValidUri1 = new Uri("http://www.microsoft.com"); - private static readonly Uri ValidUri2 = new Uri("http://www.bing.com"); + private static readonly Uri ValidUri2 = new Uri("http://www.nuget.org"); private static readonly Uri InvalidUri1 = new Uri("http://nonexisting.domain.atleast.ihope"); private static readonly Uri InvalidUri2 = new Uri("http://nonexisting.domain.atleast.ihope/foo"); private static readonly Uri InvalidUri3 = new Uri("http://www.nuget.org/com/ibm/mq/com.ibm.mq.soap/7.0.1.10/com.ibm.mq.soap-7.0.1.10"); @@ -83,7 +83,7 @@ public async Task LoadBalancesBetweenValidUrisForGetStringAsync() bool hasHitUri2 = false; int numRequests = 0; - while (!hasHitUri1 || !hasHitUri2 || numRequests < 25) + while ((!hasHitUri1 || !hasHitUri2) && numRequests < 25) { numRequests++; var result = await client.GetStringAsync(new[] { ValidUri1, ValidUri2 }); @@ -107,7 +107,7 @@ public async Task LoadBalancesBetweenValidUrisForGetAsync() bool hasHitUri2 = false; int numRequests = 0; - while (!hasHitUri1 || !hasHitUri2 || numRequests < 25) + while ((!hasHitUri1 || !hasHitUri2) && numRequests < 25) { numRequests++; var result = await client.GetAsync(new[] { ValidUri1, ValidUri2 }); diff --git a/tests/NuGetGallery.Facts/Services/CloudBlobFileStorageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/CloudBlobFileStorageServiceFacts.cs index 675f94f2a7..842f165c14 100644 --- a/tests/NuGetGallery.Facts/Services/CloudBlobFileStorageServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/CloudBlobFileStorageServiceFacts.cs @@ -449,7 +449,7 @@ public async Task WillGetTheBlobFromTheCorrectFolderContainer(string folderName) } [Fact] - public async Task WillDeleteTheBlobIfItExists() + public async Task WillDeleteBlobIfItExistsAndOverwriteTrue() { var fakeBlobClient = new Mock(); var fakeBlobContainer = new Mock(); @@ -470,6 +470,27 @@ public async Task WillDeleteTheBlobIfItExists() fakeBlob.Verify(); } + [Fact] + public async Task WillThrowIfBlobExistsAndOverwriteFalse() + { + var fakeBlobClient = new Mock(); + var fakeBlobContainer = new Mock(); + var fakeBlob = new Mock(); + fakeBlob.Setup(x => x.UploadFromStreamAsync(It.IsAny())).Returns(Task.FromResult(0)); + fakeBlob.Setup(x => x.ExistsAsync()).Returns(Task.FromResult(true)).Verifiable(); + fakeBlobClient.Setup(x => x.GetContainerReference(It.IsAny())).Returns(fakeBlobContainer.Object); + fakeBlobContainer.Setup(x => x.GetBlobReference(It.IsAny())).Returns(fakeBlob.Object); + fakeBlobContainer.Setup(x => x.SetPermissionsAsync(It.IsAny())).Returns(Task.FromResult(0)); + fakeBlobContainer.Setup(x => x.CreateIfNotExistAsync()).Returns(Task.FromResult(0)); + fakeBlob.Setup(x => x.Properties).Returns(new BlobProperties()); + fakeBlob.Setup(x => x.Uri).Returns(new Uri("http://theUri")); + var service = CreateService(fakeBlobClient: fakeBlobClient); + + await Assert.ThrowsAsync(async () => await service.SaveFileAsync(Constants.PackagesFolderName, "theFileName", new MemoryStream(), overwrite: false)); + + fakeBlob.Verify(); + } + [Fact] public async Task WillUploadThePackageFileToTheBlob() { diff --git a/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs b/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs index a74246bd17..bf60fa44d6 100644 --- a/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs @@ -2,7 +2,9 @@ // 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.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; @@ -13,6 +15,7 @@ using NuGetGallery.Configuration; using NuGetGallery.Infrastructure.Lucene; using NuGetGallery.OData; +using NuGetGallery.OData.QueryFilter; using NuGetGallery.TestUtils.Infrastructure; using NuGetGallery.WebApi; using Xunit; @@ -112,6 +115,8 @@ public async Task ReturnCorrectDefaultContentTypes(string requestUrl, string res [Theory] [InlineData("https://nuget.org/api/v2/Packages(Id='NoFoo',Version='1.0.0')")] + [InlineData("https://nuget.org/api/v2/Packages(Id='Foo',Version='1.0.0')?$filter=Id%20eq%20%27SomethingElse%27")] + [InlineData("https://nuget.org/api/v2/Packages(Id='Foo',Version='1.0.0')?$filter=Id%20eq%20%27SomethingElse%27&$select=Id")] public async Task Return404NotFoundForUnexistingPackage(string requestUrl) { using (var server = FeedServiceHelpers.SetupODataServer()) @@ -119,7 +124,7 @@ public async Task Return404NotFoundForUnexistingPackage(string requestUrl) var client = new HttpClient(server); var response = await client.GetAsync(requestUrl); - Assert.False(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } @@ -160,7 +165,7 @@ public class TheGetSiteRootMethod public void AddsTrailingSlashes(string siteRoot, string expected) { // Arrange - var config = new Mock(); + var config = new Mock(); config.Setup(s => s.GetSiteRoot(false)).Returns(siteRoot); var feed = new TestableV1Feed(null, config.Object, null); feed.Request = new HttpRequestMessage(HttpMethod.Get, siteRoot); @@ -176,7 +181,7 @@ public void AddsTrailingSlashes(string siteRoot, string expected) public void UsesCurrentRequestToDetermineSiteRoot() { // Arrange - var config = new Mock(); + var config = new Mock(); config.Setup(s => s.GetSiteRoot(true)).Returns("https://nuget.org").Verifiable(); var feed = new TestableV2Feed(null, config.Object, null); feed.Request = new HttpRequestMessage(HttpMethod.Get, "https://nuget.org"); @@ -218,8 +223,9 @@ public async Task V1FeedSearchDoesNotReturnPrereleasePackages() Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns , string>((_, __) => Task.FromResult(new SearchResults(_.Count(), DateTime.UtcNow, _))); @@ -269,8 +275,9 @@ public async Task V1FeedSearchDoesNotReturnDeletedPackages() Deleted = false }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns , string>((_, __) => Task.FromResult(new SearchResults(_.Count(), DateTime.UtcNow, _))); @@ -321,7 +328,7 @@ public async Task V1FeedFindPackagesByIdReturnsUnlistedPackagesButNotPrereleaseP Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); var v1Service = new TestableV1Feed(repo.Object, configuration.Object, null); @@ -367,7 +374,7 @@ public async Task V1FeedFindPackagesByIdDoesNotReturnDeletedPackages() Deleted = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); var v1Service = new TestableV1Feed(repo.Object, configuration.Object, null); @@ -385,6 +392,60 @@ public async Task V1FeedFindPackagesByIdDoesNotReturnDeletedPackages() Assert.Equal(0, result.Count()); } } + + public class TheODataFilter + { + [Fact] + public async Task ODataQueryFilterV2Search() + { + ODataQueryVerifier.V1Search = GetQueryFilter(false); + var v1Service = GetService("https://localhost:8081/"); + var result = (await v1Service.Search( + new ODataQueryOptions(new ODataQueryContext( + NuGetODataV1FeedConfig.GetEdmModel(), + typeof(V1FeedPackage)), + v1Service.Request))); + var badRequest = result as BadRequestErrorMessageResult; + Assert.NotEqual(null, badRequest); + } + + [Fact] + public void ODataQueryFilterV1Packages() + { + ODataQueryVerifier.V1Packages = GetQueryFilter(false); + var service = GetService("https://localhost:8081/"); + var result = service.Get( + new ODataQueryOptions(new ODataQueryContext( + NuGetODataV1FeedConfig.GetEdmModel(), + typeof(V1FeedPackage)), + service.Request)); + var badRequest = result as BadRequestErrorMessageResult; + Assert.NotEqual(null, badRequest); + } + + private TestableV1Feed GetService(string host, string arguments = "?$skip=10") + { + var repo = new Mock>(MockBehavior.Loose); + var configuration = new Mock(MockBehavior.Strict); + configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns(host); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = true }); + var searchService = new Mock(MockBehavior.Strict); + searchService.Setup(s => s.Search(It.IsAny())).Returns + , string>((_, __) => Task.FromResult(new SearchResults(_.Count(), DateTime.UtcNow, _))); + searchService.Setup(s => s.ContainsAllVersions).Returns(false); + var v1Service = new TestableV1Feed(repo.Object, configuration.Object, searchService.Object); + v1Service.Request = new HttpRequestMessage(HttpMethod.Get, $"{host}{arguments}"); + + return v1Service; + } + + private ODataQueryFilter GetQueryFilter(bool allow) + { + var mockODataQueryFilter = new Mock(); + mockODataQueryFilter.Setup(qf => qf.IsAllowed(It.IsAny>())).Returns(allow); + return mockODataQueryFilter.Object; + } + } } public class TheV2Feed @@ -401,9 +462,10 @@ public async Task V2FeedPackagesReturnsCollection(string filter, int top, int ex // Arrange var repo = FeedServiceHelpers.SetupTestPackageRepository(); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns @@ -440,9 +502,10 @@ public async Task V2FeedPackagesUsesSearchHijackForIdOrIdVersionQueries(string f // Arrange var repo = FeedServiceHelpers.SetupTestPackageRepository(); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Loose); searchService.CallBase = true; @@ -479,9 +542,10 @@ public async Task V2FeedPackagesDoesNotUseSearchHijackForFunkyQueries(string fil // Arrange var repo = FeedServiceHelpers.SetupTestPackageRepository(); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); bool called = false; var searchService = new Mock(MockBehavior.Loose); @@ -520,9 +584,10 @@ public async Task V2FeedPackagesCountReturnsCorrectCount(string filter, int top, // Arrange var repo = FeedServiceHelpers.SetupTestPackageRepository(); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns @@ -559,9 +624,10 @@ public async Task V2FeedPackagesByIdAndVersionReturnsPackage(string expectedId, // Arrange var repo = FeedServiceHelpers.SetupTestPackageRepository(); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns @@ -591,9 +657,10 @@ public async Task V2FeedPackagesByIdAndVersionReturnsNotFoundWhenPackageNotFound // Arrange var repo = FeedServiceHelpers.SetupTestPackageRepository(); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns @@ -621,9 +688,10 @@ public async Task V2FeedPackagesCollectionDoesNotContainDeletedPackages(string f // Arrange var repo = FeedServiceHelpers.SetupTestPackageRepository(); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns @@ -679,7 +747,7 @@ public async Task V2FeedFindPackagesByIdReturnsUnlistedAndPrereleasePackages() Tags = string.Empty }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); var searchService = new Mock(MockBehavior.Strict); @@ -713,7 +781,7 @@ public async Task V2FeedFindPackagesByIdReturnsEmptyCollectionWhenNoPackages() var repo = new Mock>(MockBehavior.Strict); repo.Setup(r => r.GetAll()).Returns(() => Enumerable.Empty().AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); @@ -743,7 +811,7 @@ public async Task V2FeedFindPackagesByIdDoesNotHitBackendWhenIdIsEmpty() // Arrange var repo = new Mock>(MockBehavior.Loose); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); @@ -788,7 +856,7 @@ public async Task V2FeedFindPackagesByIdDoesNotReturnDeletedPackages() Deleted = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); @@ -821,9 +889,10 @@ public async Task V2FeedSearchFiltersPackagesBySearchTermAndPrereleaseFlag(strin // Arrange var repo = FeedServiceHelpers.SetupTestPackageRepository(); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns @@ -866,9 +935,10 @@ public async Task V2FeedSearchCountFiltersPackagesBySearchTermAndPrereleaseFlag( // Arrange var repo = FeedServiceHelpers.SetupTestPackageRepository(); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns @@ -915,8 +985,9 @@ public void V2FeedGetUpdatesReturnsEmptyResultsIfInputIsMalformed(string id, str { // Arrange var repo = Mock.Of>(); - var configuration = Mock.Of(); - var v2Service = new TestableV2Feed(repo, configuration, null); + var configuration = new Mock(MockBehavior.Default); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); + var v2Service = new TestableV2Feed(repo, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); // Act @@ -950,9 +1021,10 @@ public void V2FeedGetUpdatesIgnoresItemsWithMalformedVersions() new Package { PackageRegistration = packageRegistrationA, Version = "1.2.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -990,9 +1062,10 @@ public void V2FeedGetUpdatesReturnsVersionsNewerThanListedVersion() new Package { PackageRegistration = packageRegistrationA, Version = "1.2.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1039,9 +1112,10 @@ public void V2FeedGetUpdatesReturnsEmptyIfVersionConstraintsContainWrongNumberOf new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationB, Version = "3.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(false)).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1077,9 +1151,10 @@ public void V2FeedGetUpdatesReturnsVersionsConformingToConstraints() new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationB, Version = "3.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1120,9 +1195,10 @@ public void V2FeedGetUpdatesIgnoreInvalidVersionConstraints() new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationB, Version = "3.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1164,9 +1240,10 @@ public void V2FeedGetUpdatesReturnsVersionsConformingToConstraintsWithMissingCon new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationB, Version = "3.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1207,9 +1284,10 @@ public void V2FeedGetUpdatesReturnsEmptyPackagesIfNoPackageSatisfiesConstraints( new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationB, Version = "3.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1244,9 +1322,10 @@ public void V2FeedGetUpdatesReturnsCaseInsensitiveMatches() new Package { PackageRegistration = packageRegistrationA, Version = "1.2.0-alpha", IsPrerelease = true, Listed = true }, new Package { PackageRegistration = packageRegistrationA, Version = "1.2.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1287,9 +1366,10 @@ public void V2FeedGetUpdatesReturnsUpdateIfAnyOfTheProvidedVersionsIsOlder() new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationB, Version = "3.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1329,9 +1409,10 @@ public void V2FeedGetUpdatesReturnsPrereleasePackages() new Package { PackageRegistration = packageRegistrationA, Version = "1.2.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1368,9 +1449,10 @@ public void V2FeedGetUpdatesDoesNotReturnDeletedPackages() new Package { PackageRegistration = packageRegistrationA, Version = "1.0.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationA, Version = "1.1.0", IsPrerelease = false, Deleted = true } }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1408,9 +1490,10 @@ public void V2FeedGetUpdatesReturnsResultsIfDuplicatesInPackageList() new Package { PackageRegistration = packageRegistrationA, Version = "1.2.0", IsPrerelease = false, Listed = true }, new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1479,10 +1562,10 @@ public void V2FeedGetUpdatesFiltersByTargetFramework() }, new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); - configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1558,9 +1641,10 @@ public void V2FeedGetUpdatesFiltersIncludesHighestPrereleasePackage() }, new Package { PackageRegistration = packageRegistrationB, Version = "2.0", IsPrerelease = false, Listed = true }, }.AsQueryable()); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/"); configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var v2Service = new TestableV2Feed(repo.Object, configuration.Object, null); v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/"); @@ -1584,6 +1668,78 @@ public void V2FeedGetUpdatesFiltersIncludesHighestPrereleasePackage() AssertPackage(new { Id = "Qux", Version = "2.0" }, result[1]); } } + + public class TheODataFilter + { + [Fact] + public void ODataQueryFilterV2FeedGetUpdates() + { + ODataQueryVerifier.V2GetUpdates = GetQueryFilter(false); + var v2Service = GetService("https://localhost:8081/"); + var result = (v2Service.GetUpdates( + new ODataQueryOptions( + new ODataQueryContext(NuGetODataV2FeedConfig.GetEdmModel(), + typeof(V2FeedPackage)), + v2Service.Request), + "Pid", "Version", false, false)); + var badRequest = result as BadRequestErrorMessageResult; + Assert.NotEqual(null, badRequest); + } + + [Fact] + public async Task ODataQueryFilterV2Search() + { + ODataQueryVerifier.V2Search = GetQueryFilter(false); + var v2Service = GetService("https://localhost:8081/"); + var result = (await v2Service.Search( + new ODataQueryOptions(new ODataQueryContext( + NuGetODataV2FeedConfig.GetEdmModel(), + typeof(V2FeedPackage)), + v2Service.Request))); + var badRequest = result as BadRequestErrorMessageResult; + Assert.NotEqual(null, badRequest); + } + + [Fact] + public async Task ODataQueryFilterV2Packages() + { + ODataQueryVerifier.V2Packages = GetQueryFilter(false); + var v2Service = GetService("https://localhost:8081/"); + var result = (await v2Service.Get( + new ODataQueryOptions(new ODataQueryContext( + NuGetODataV2FeedConfig.GetEdmModel(), + typeof(V2FeedPackage)), + v2Service.Request))); + var badRequest = result as BadRequestErrorMessageResult; + Assert.NotEqual(null, badRequest); + } + + private TestableV2Feed GetService(string host, string arguments = "?$skip=10") + { + var repo = new Mock>(MockBehavior.Loose); + var configuration = new Mock(MockBehavior.Default); + configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns(host); + configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = true }); + + var searchService = new Mock(MockBehavior.Strict); + searchService.Setup(s => s.Search(It.IsAny())).Returns + , string>((_, __) => Task.FromResult(new SearchResults(_.Count(), DateTime.UtcNow, _))); + searchService.Setup(s => s.ContainsAllVersions).Returns(false); + + var v2Service = new TestableV2Feed(repo.Object, configuration.Object, searchService.Object); + v2Service.Request = new HttpRequestMessage(HttpMethod.Get, $"{host}{arguments}"); + + return v2Service; + } + + private ODataQueryFilter GetQueryFilter(bool allow) + { + var mockODataQueryFilter = new Mock(); + mockODataQueryFilter.Setup(qf => qf.IsAllowed(It.IsAny>())).Returns(allow); + return mockODataQueryFilter.Object; + } + } } private static void AssertPackage(dynamic expected, V2FeedPackage package) diff --git a/tests/NuGetGallery.Facts/Services/FileSystemFileStorageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/FileSystemFileStorageServiceFacts.cs index 4e539aaf83..9acd8b366e 100644 --- a/tests/NuGetGallery.Facts/Services/FileSystemFileStorageServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/FileSystemFileStorageServiceFacts.cs @@ -11,7 +11,7 @@ namespace NuGetGallery { - public class FileSystemFileStorgeServiceFacts + public class FileSystemFileStorageServiceFacts { private static readonly Uri HttpRequestUrl = new Uri("http://nuget.org/something"); @@ -19,7 +19,7 @@ public class FileSystemFileStorgeServiceFacts private static MemoryStream CreateFileStream() { - return new MemoryStream(new byte[] { 0, 0, 1, 0, 1, 0, 1, 0 }, 0, 8, true, true); + return new MemoryStream(new byte[] { 0, 0, 1, 0, 1, 0, 1, 0 }, index: 0, count: 8, writable: true, publiclyVisible: true); } private static FileSystemFileStorageService CreateService( @@ -261,13 +261,15 @@ public async Task WillReturnTheRequestFileStreamWhenItExists() "theFileName"); var fakeFileSystemService = new Mock(); fakeFileSystemService.Setup(x => x.FileExists(It.IsAny())).Returns(true); - var fakeFileStream = new MemoryStream(); - fakeFileSystemService.Setup(x => x.OpenRead(expectedPath)).Returns(fakeFileStream); - var service = CreateService(fileSystemService: fakeFileSystemService); + using (var fakeFileStream = new MemoryStream()) + { + fakeFileSystemService.Setup(x => x.OpenRead(expectedPath)).Returns(fakeFileStream); + var service = CreateService(fileSystemService: fakeFileSystemService); - var fileStream = await service.GetFileAsync("theFolderName", "theFileName"); + var fileStream = await service.GetFileAsync("theFolderName", "theFileName"); - Assert.Same(fakeFileStream, fileStream); + Assert.Same(fakeFileStream, fileStream); + } } [Fact] @@ -292,9 +294,12 @@ public async Task WillThrowIfFolderNameIsNull(string folderName) { var service = CreateService(); - var ex = await Assert.ThrowsAsync(() => service.SaveFileAsync(folderName, "theFileName", CreateFileStream())); + using (var fakeFileStream = CreateFileStream()) + { + var ex = await Assert.ThrowsAsync(() => service.SaveFileAsync(folderName, "theFileName", fakeFileStream)); - Assert.Equal("folderName", ex.ParamName); + Assert.Equal("folderName", ex.ParamName); + } } [Theory] @@ -304,9 +309,12 @@ public async Task WillThrowIfFileNameIsNull(string fileName) { var service = CreateService(); - var ex = await Assert.ThrowsAsync(() => service.SaveFileAsync("theFolderName", fileName, CreateFileStream())); + using (var fakeFileStream = CreateFileStream()) + { + var ex = await Assert.ThrowsAsync(() => service.SaveFileAsync("theFolderName", fileName, fakeFileStream)); - Assert.Equal("fileName", ex.ParamName); + Assert.Equal("fileName", ex.ParamName); + } } [Fact] @@ -322,14 +330,21 @@ public async Task WillThrowIfFileStreamIsNull() [Fact] public async Task WillCreateTheConfiguredFileStorageDirectoryIfItDoesNotExist() { + const string folderName = "theFolderName"; var fakeFileSystemService = new Mock(); fakeFileSystemService.Setup(x => x.DirectoryExists(It.IsAny())).Returns(false); - fakeFileSystemService.Setup(x => x.OpenWrite(It.IsAny())).Returns(new MemoryStream(new byte[8])); - var service = CreateService(fileSystemService: fakeFileSystemService); + using (var fakeMemoryStream = CreateFileStream()) + { + fakeFileSystemService.Setup(x => x.OpenWrite(It.IsAny())).Returns(fakeMemoryStream); + var service = CreateService(fileSystemService: fakeFileSystemService); - await service.SaveFileAsync("theFolderName", "theFileName", CreateFileStream()); + using (var fakePackageStream = CreateFileStream()) + { + await service.SaveFileAsync("theFolderName", "theFileName", fakePackageStream); + } - fakeFileSystemService.Verify(x => x.CreateDirectory(FakeConfiguredFileStorageDirectory)); + fakeFileSystemService.Verify(x => x.CreateDirectory($"{FakeConfiguredFileStorageDirectory}\\{folderName}")); + } } [Fact] @@ -337,12 +352,18 @@ public async Task WillCreateTheFolderPathIfItDoesNotExist() { var fakeFileSystemService = new Mock(); fakeFileSystemService.Setup(x => x.DirectoryExists(It.IsAny())).Returns(false); - fakeFileSystemService.Setup(x => x.OpenWrite(It.IsAny())).Returns(new MemoryStream(new byte[8])); - var service = CreateService(fileSystemService: fakeFileSystemService); + using (var fakeMemoryStream = CreateFileStream()) + { + fakeFileSystemService.Setup(x => x.OpenWrite(It.IsAny())).Returns(fakeMemoryStream); + var service = CreateService(fileSystemService: fakeFileSystemService); - await service.SaveFileAsync("theFolderName", "theFileName", CreateFileStream()); + using (var fakePackageStream = CreateFileStream()) + { + await service.SaveFileAsync("theFolderName", "theFileName", fakePackageStream); + } - fakeFileSystemService.Verify(x => x.CreateDirectory(Path.Combine(FakeConfiguredFileStorageDirectory, "theFolderName"))); + fakeFileSystemService.Verify(x => x.CreateDirectory(Path.Combine(FakeConfiguredFileStorageDirectory, "theFolderName"))); + } } [Fact] @@ -350,35 +371,91 @@ public async Task WillSaveThePackageFileToTheSpecifiedFolder() { var fakeFileSystemService = new Mock(); fakeFileSystemService.Setup(x => x.DirectoryExists(It.IsAny())).Returns(true); - fakeFileSystemService.Setup(x => x.OpenWrite(It.IsAny())).Returns(new MemoryStream(new byte[8])); - var service = CreateService(fileSystemService: fakeFileSystemService); - - await service.SaveFileAsync("theFolderName", "theFileName", CreateFileStream()); - - fakeFileSystemService.Verify( - x => - x.OpenWrite( - Path.Combine( - FakeConfiguredFileStorageDirectory, - "theFolderName", - "theFileName"))); + using (var fakeMemoryStream = CreateFileStream()) + { + fakeFileSystemService.Setup(x => x.OpenWrite(It.IsAny())).Returns(fakeMemoryStream); + var service = CreateService(fileSystemService: fakeFileSystemService); + + using (var fakePackageStream = CreateFileStream()) + { + await service.SaveFileAsync("theFolderName", "theFileName", fakePackageStream); + } + + fakeFileSystemService.Verify( + x => + x.OpenWrite( + Path.Combine( + FakeConfiguredFileStorageDirectory, + "theFolderName", + "theFileName"))); + } } [Fact] public async Task WillSaveThePackageFileBytes() { - var fakePackageFile = CreateFileStream(); - var fakeFileStream = new MemoryStream(new byte[8], 0, 8, true, true); - var fakeFileSystemService = new Mock(); - fakeFileSystemService.Setup(x => x.DirectoryExists(It.IsAny())).Returns(true); - fakeFileSystemService.Setup(x => x.OpenWrite(It.IsAny())).Returns(fakeFileStream); - var service = CreateService(fileSystemService: fakeFileSystemService); + using (var fakePackageFile = CreateFileStream()) + { + using (var fakeFileStream = CreateFileStream()) + { + var fakeFileSystemService = new Mock(); + fakeFileSystemService.Setup(x => x.DirectoryExists(It.IsAny())).Returns(true); + fakeFileSystemService.Setup(x => x.OpenWrite(It.IsAny())).Returns(fakeFileStream); + var service = CreateService(fileSystemService: fakeFileSystemService); + + using (var fakePackageStream = CreateFileStream()) + { + await service.SaveFileAsync("theFolderName", "theFileName", fakePackageStream); + } + + for (var i = 0; i < fakePackageFile.Length; i++) + { + Assert.Equal(fakePackageFile.GetBuffer()[i], fakeFileStream.GetBuffer()[i]); + } + } + } + } - await service.SaveFileAsync("theFolderName", "theFileName", CreateFileStream()); + [Fact] + public async Task WillOverwriteFileIfOverwriteTrue() + { + using (var fakePackageFile = CreateFileStream()) + { + using (var fakeFileStream = CreateFileStream()) + { + var fakeFileSystemService = new Mock(); + fakeFileSystemService.Setup(x => x.DirectoryExists(It.IsAny())).Returns(true); + fakeFileSystemService.Setup(x => x.OpenWrite(It.IsAny())).Returns(fakeFileStream); + fakeFileSystemService.Setup(x => x.FileExists(It.IsAny())).Returns(true); + fakeFileSystemService.Setup(x => x.DeleteFile(It.IsAny())).Verifiable(); + var service = CreateService(fileSystemService: fakeFileSystemService); + + await service.SaveFileAsync("theFolderName", "theFileName", fakePackageFile); + + for (var i = 0; i < fakePackageFile.Length; i++) + { + Assert.Equal(fakePackageFile.GetBuffer()[i], fakeFileStream.GetBuffer()[i]); + } + + fakeFileSystemService.Verify(); + } + } + } - for (var i = 0; i < fakePackageFile.Length; i++) + [Fact] + public async Task WillThrowIfFileExistsAndOverwriteFalse() + { + using (var fakeFileStream = CreateFileStream()) { - Assert.Equal(fakePackageFile.GetBuffer()[i], fakeFileStream.GetBuffer()[i]); + var fakeFileSystemService = new Mock(); + fakeFileSystemService.Setup(x => x.DirectoryExists(It.IsAny())).Returns(true); + fakeFileSystemService.Setup(x => x.OpenWrite(It.IsAny())).Returns(fakeFileStream); + fakeFileSystemService.Setup(x => x.FileExists(It.IsAny())).Returns(true); + var service = CreateService(fileSystemService: fakeFileSystemService); + + await Assert.ThrowsAsync(async () => await service.SaveFileAsync("theFolderName", "theFileName", fakeFileStream, false)); + + fakeFileSystemService.Verify(); } } } diff --git a/tests/NuGetGallery.Facts/Services/MessageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/MessageServiceFacts.cs index 00a810757f..aef8174bde 100644 --- a/tests/NuGetGallery.Facts/Services/MessageServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/MessageServiceFacts.cs @@ -1,6 +1,7 @@ // 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.Collections.ObjectModel; using System.Linq; @@ -11,6 +12,7 @@ using NuGetGallery.Authentication.Providers; using NuGetGallery.Configuration; using NuGetGallery.Framework; +using NuGetGallery.Infrastructure.Authentication; using Xunit; namespace NuGetGallery @@ -18,6 +20,7 @@ namespace NuGetGallery public class MessageServiceFacts { public static readonly MailAddress TestGalleryOwner = new MailAddress("joe@example.com", "Joe Shmoe"); + public static readonly MailAddress TestGalleryNoReplyAddress = new MailAddress("noreply@example.com", "No Reply"); public class TheReportAbuseMethod { @@ -315,7 +318,7 @@ public void WillSendEmailToNewUser() var message = messageService.MockMailSender.Sent.Last(); Assert.Equal("legit@example.com", message.To[0].Address); - Assert.Equal(TestGalleryOwner.Address, message.From.Address); + Assert.Equal(TestGalleryNoReplyAddress.Address, message.From.Address); Assert.Equal("[Joe Shmoe] Please verify your account.", message.Subject); Assert.Contains("http://example.com/confirmation-token-url", message.Body); } @@ -330,19 +333,36 @@ public void SendsPackageOwnerRequestConfirmationUrl() var from = new User { Username = "Existing", EmailAddress = "existing-owner@example.com" }; var package = new PackageRegistration { Id = "CoolStuff" }; const string confirmationUrl = "http://example.com/confirmation-token-url"; - + const string userMessage = "Hello World!"; var messageService = new TestableMessageService(); - messageService.SendPackageOwnerRequest(from, to, package, confirmationUrl); + messageService.SendPackageOwnerRequest(from, to, package, confirmationUrl, userMessage); var message = messageService.MockMailSender.Sent.Last(); Assert.Equal("new-owner@example.com", message.To[0].Address); - Assert.Equal(TestGalleryOwner.Address, message.From.Address); + Assert.Equal(TestGalleryNoReplyAddress.Address, message.From.Address); Assert.Equal("existing-owner@example.com", message.ReplyToList.Single().Address); Assert.Equal("[Joe Shmoe] The user 'Existing' wants to add you as an owner of the package 'CoolStuff'.", message.Subject); + Assert.Contains("The user 'Existing' added the following message for you", message.Body); + Assert.Contains(userMessage, message.Body); Assert.Contains(confirmationUrl, message.Body); + Assert.Contains(userMessage, message.Body); Assert.Contains("The user 'Existing' wants to add you as an owner of the package 'CoolStuff'.", message.Body); } + [Fact] + public void SendsPackageOwnerRequestConfirmationUrlWithoutUserMessage() + { + var to = new User { Username = "Noob", EmailAddress = "new-owner@example.com", EmailAllowed = true }; + var from = new User { Username = "Existing", EmailAddress = "existing-owner@example.com" }; + var package = new PackageRegistration { Id = "CoolStuff" }; + const string confirmationUrl = "http://example.com/confirmation-token-url"; + var messageService = new TestableMessageService(); + messageService.SendPackageOwnerRequest(from, to, package, confirmationUrl, string.Empty); + var message = messageService.MockMailSender.Sent.Last(); + + Assert.DoesNotContain("The user 'Existing' added the following message for you", message.Body); + } + [Fact] public void DoesNotSendRequestIfUserDoesNotAllowEmails() { @@ -352,7 +372,7 @@ public void DoesNotSendRequestIfUserDoesNotAllowEmails() const string confirmationUrl = "http://example.com/confirmation-token-url"; var messageService = new TestableMessageService(); - messageService.SendPackageOwnerRequest(from, to, package, confirmationUrl); + messageService.SendPackageOwnerRequest(from, to, package, confirmationUrl, string.Empty); Assert.Empty(messageService.MockMailSender.Sent); } @@ -372,7 +392,7 @@ public void SendsPackageOwnerRemovedNotice() var message = messageService.MockMailSender.Sent.Last(); Assert.Equal("old-owner@example.com", message.To[0].Address); - Assert.Equal(TestGalleryOwner.Address, message.From.Address); + Assert.Equal(TestGalleryNoReplyAddress.Address, message.From.Address); Assert.Equal("existing-owner@example.com", message.ReplyToList.Single().Address); Assert.Contains("The user 'Existing' has removed you as an owner of the package 'CoolStuff'.", message.Subject); Assert.Contains("The user 'Existing' removed you as an owner of the package 'CoolStuff'", message.Body); @@ -404,7 +424,7 @@ public void WillSendInstructions() var message = messageService.MockMailSender.Sent.Last(); Assert.Equal("legit@example.com", message.To[0].Address); - Assert.Equal(TestGalleryOwner.Address, message.From.Address); + Assert.Equal(TestGalleryNoReplyAddress.Address, message.From.Address); Assert.Equal("[Joe Shmoe] Please reset your password.", message.Subject); Assert.Contains("Click the following link within the next", message.Body); Assert.Contains("http://example.com/pwd-reset-token-url", message.Body); @@ -417,12 +437,14 @@ public class TheSendCredentialRemovedNoticeMethod public void UsesProviderNounToDescribeCredentialIfPresent() { var user = new User { EmailAddress = "legit@example.com", Username = "foo" }; - var cred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "abc123", "Test User"); + var cred = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "abc123", "Test User"); + const string MicrosoftAccountCredentialName = "Microsoft Account"; var messageService = new TestableMessageService(); messageService.MockAuthService .Setup(a => a.DescribeCredential(cred)) - .Returns(new CredentialViewModel() { - AuthUI = new AuthenticatorUI("sign in", "Microsoft Account", "Microsoft Account", "You can use this Microsoft account to sign in to NuGet.org") + .Returns(new CredentialViewModel + { + AuthUI = new AuthenticatorUI("sign in", MicrosoftAccountCredentialName, MicrosoftAccountCredentialName) }); messageService.SendCredentialRemovedNotice(user, cred); @@ -430,20 +452,21 @@ public void UsesProviderNounToDescribeCredentialIfPresent() Assert.Equal(user.ToMailAddress(), message.To[0]); Assert.Equal(TestGalleryOwner, message.From); - Assert.Equal("[Joe Shmoe] Microsoft Account removed from your account", message.Subject); - Assert.Contains("A Microsoft Account was removed from your account", message.Body); + Assert.Equal(string.Format(Strings.Emails_CredentialRemoved_Subject, TestGalleryOwner.DisplayName, MicrosoftAccountCredentialName), message.Subject); + Assert.Contains(string.Format(Strings.Emails_CredentialRemoved_Body, MicrosoftAccountCredentialName), message.Body); } [Fact] public void UsesTypeCaptionToDescribeCredentialIfNoProviderNounPresent() { var user = new User { EmailAddress = "legit@example.com", Username = "foo" }; - var cred = CredentialBuilder.CreatePbkdf2Password("bogus"); + var cred = new CredentialBuilder().CreatePasswordCredential("bogus"); var messageService = new TestableMessageService(); messageService.MockAuthService .Setup(a => a.DescribeCredential(cred)) - .Returns(new CredentialViewModel() { - TypeCaption = "Password" + .Returns(new CredentialViewModel() + { + TypeCaption = Strings.CredentialType_Password }); messageService.SendCredentialRemovedNotice(user, cred); @@ -451,8 +474,31 @@ public void UsesTypeCaptionToDescribeCredentialIfNoProviderNounPresent() Assert.Equal(user.ToMailAddress(), message.To[0]); Assert.Equal(TestGalleryOwner, message.From); - Assert.Equal("[Joe Shmoe] Password removed from your account", message.Subject); - Assert.Contains("A Password was removed from your account", message.Body); + Assert.Equal(string.Format(Strings.Emails_CredentialRemoved_Subject, TestGalleryOwner.DisplayName, Strings.CredentialType_Password), message.Subject); + Assert.Contains(string.Format(Strings.Emails_CredentialRemoved_Body, Strings.CredentialType_Password), message.Body); + } + + [Fact] + public void ApiKeyRemovedMessageIsCorrect() + { + var user = new User { EmailAddress = "legit@example.com", Username = "foo" }; + var cred = new CredentialBuilder().CreateApiKey(TimeSpan.FromDays(1)); + cred.Description = "new api key"; + var messageService = new TestableMessageService(); + messageService.MockAuthService + .Setup(a => a.DescribeCredential(cred)) + .Returns(new CredentialViewModel + { + Description = cred.Description + }); + + messageService.SendCredentialRemovedNotice(user, cred); + var message = messageService.MockMailSender.Sent.Last(); + + Assert.Equal(user.ToMailAddress(), message.To[0]); + Assert.Equal(TestGalleryOwner, message.From); + Assert.Equal(string.Format(Strings.Emails_CredentialRemoved_Subject, TestGalleryOwner.DisplayName, Strings.CredentialType_ApiKey), message.Subject); + Assert.Contains(string.Format(Strings.Emails_ApiKeyRemoved_Body, cred.Description), message.Body); } } @@ -462,12 +508,15 @@ public class TheSendCredentialAddedNoticeMethod public void UsesProviderNounToDescribeCredentialIfPresent() { var user = new User { EmailAddress = "legit@example.com", Username = "foo" }; - var cred = CredentialBuilder.CreateExternalCredential("MicrosoftAccount", "abc123", "Test User"); + var cred = new CredentialBuilder().CreateExternalCredential("MicrosoftAccount", "abc123", "Test User"); + const string MicrosoftAccountCredentialName = "Microsoft Account"; + var messageService = new TestableMessageService(); messageService.MockAuthService .Setup(a => a.DescribeCredential(cred)) - .Returns(new CredentialViewModel() { - AuthUI = new AuthenticatorUI("sign in", "Microsoft Account", "Microsoft Account", "You can use this Microsoft account to sign in to NuGet.org") + .Returns(new CredentialViewModel + { + AuthUI = new AuthenticatorUI("sign in", MicrosoftAccountCredentialName, MicrosoftAccountCredentialName) }); messageService.SendCredentialAddedNotice(user, cred); @@ -475,19 +524,20 @@ public void UsesProviderNounToDescribeCredentialIfPresent() Assert.Equal(user.ToMailAddress(), message.To[0]); Assert.Equal(TestGalleryOwner, message.From); - Assert.Equal("[Joe Shmoe] Microsoft Account added to your account", message.Subject); - Assert.Contains("A Microsoft Account was added to your account", message.Body); + Assert.Equal(string.Format(Strings.Emails_CredentialAdded_Subject, TestGalleryOwner.DisplayName, MicrosoftAccountCredentialName), message.Subject); + Assert.Contains(string.Format(Strings.Emails_CredentialAdded_Body, MicrosoftAccountCredentialName), message.Body); } [Fact] public void UsesTypeCaptionToDescribeCredentialIfNoProviderNounPresent() { var user = new User { EmailAddress = "legit@example.com", Username = "foo" }; - var cred = CredentialBuilder.CreatePbkdf2Password("bogus"); + var cred = new CredentialBuilder().CreatePasswordCredential("bogus"); var messageService = new TestableMessageService(); messageService.MockAuthService .Setup(a => a.DescribeCredential(cred)) - .Returns(new CredentialViewModel() { + .Returns(new CredentialViewModel + { TypeCaption = "Password" }); @@ -496,8 +546,31 @@ public void UsesTypeCaptionToDescribeCredentialIfNoProviderNounPresent() Assert.Equal(user.ToMailAddress(), message.To[0]); Assert.Equal(TestGalleryOwner, message.From); - Assert.Equal("[Joe Shmoe] Password added to your account", message.Subject); - Assert.Contains("A Password was added to your account", message.Body); + Assert.Equal(string.Format(Strings.Emails_CredentialAdded_Subject, TestGalleryOwner.DisplayName, Strings.CredentialType_Password), message.Subject); + Assert.Contains(string.Format(Strings.Emails_CredentialAdded_Body, Strings.CredentialType_Password), message.Body); + } + + [Fact] + public void ApiKeyAddedMessageIsCorrect() + { + var user = new User { EmailAddress = "legit@example.com", Username = "foo" }; + var cred = new CredentialBuilder().CreateApiKey(TimeSpan.FromDays(1)); + cred.Description = "new api key"; + var messageService = new TestableMessageService(); + messageService.MockAuthService + .Setup(a => a.DescribeCredential(cred)) + .Returns(new CredentialViewModel + { + Description = cred.Description + }); + + messageService.SendCredentialAddedNotice(user, cred); + var message = messageService.MockMailSender.Sent.Last(); + + Assert.Equal(user.ToMailAddress(), message.To[0]); + Assert.Equal(TestGalleryOwner, message.From); + Assert.Equal(string.Format(Strings.Emails_CredentialAdded_Subject, TestGalleryOwner.DisplayName, Strings.CredentialType_ApiKey), message.Subject); + Assert.Contains(string.Format(Strings.Emails_ApiKeyAdded_Body, cred.Description), message.Body); } } @@ -532,7 +605,7 @@ public void WillSendEmailToAllOwners() Assert.Equal("yung@example.com", message.To[0].Address); Assert.Equal("flynt@example.com", message.To[1].Address); - Assert.Equal(TestGalleryOwner, message.From); + Assert.Equal(TestGalleryNoReplyAddress, message.From); Assert.Contains("[Joe Shmoe] Package published - smangit 1.2.3", message.Subject); Assert.Contains( "The package [smangit 1.2.3](http://dummy1) was just published on Joe Shmoe. If this was not intended, please [contact support](http://dummy2).", message.Body); @@ -611,6 +684,7 @@ public TestableMessageService() MailSender = MockMailSender = new TestMailSender(); MockConfig.Setup(x => x.GalleryOwner).Returns(TestGalleryOwner); + MockConfig.Setup(x => x.GalleryNoReplyAddress).Returns(TestGalleryNoReplyAddress); } } diff --git a/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs index 15ff74ee79..61eca071d9 100644 --- a/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs @@ -23,7 +23,7 @@ private static IPackageDeleteService CreateService( Mock packageService = null, Mock indexingService = null, Mock packageFileService = null, - Mock auditingService = null, + Mock auditingService = null, Action> setup = null) { packageRepository = packageRepository ?? new Mock>(); @@ -37,7 +37,7 @@ private static IPackageDeleteService CreateService( indexingService = indexingService ?? new Mock(); packageFileService = packageFileService ?? new Mock(); - auditingService = auditingService ?? new Mock(); + auditingService = auditingService ?? new Mock(); var packageDeleteService = new Mock( packageRepository.Object, @@ -63,7 +63,7 @@ public class TestPackageDeleteService { public PackageAuditRecord LastAuditRecord { get; set; } - public TestPackageDeleteService(IEntityRepository packageRepository, IEntityRepository packageDeletesRepository, IEntitiesContext entitiesContext, IPackageService packageService, IIndexingService indexingService, IPackageFileService packageFileService, AuditingService auditingService) + public TestPackageDeleteService(IEntityRepository packageRepository, IEntityRepository packageDeletesRepository, IEntitiesContext entitiesContext, IPackageService packageService, IIndexingService indexingService, IPackageFileService packageFileService, IAuditingService auditingService) : base(packageRepository, packageDeletesRepository, entitiesContext, packageService, indexingService, packageFileService, auditingService) { } @@ -79,7 +79,7 @@ public virtual Task TestExecuteSqlCommandAsync(Database database, string sql, pa return Task.FromResult(0); } - protected override PackageAuditRecord CreateAuditRecord(Package package, PackageRegistration packageRegistration, PackageAuditAction action, string reason) + protected override PackageAuditRecord CreateAuditRecord(Package package, PackageRegistration packageRegistration, AuditedPackageAction action, string reason) { LastAuditRecord = base.CreateAuditRecord(package, packageRegistration, action, reason); return LastAuditRecord; @@ -213,7 +213,7 @@ public async Task WillBackupAndDeleteThePackageFile() [Fact] public async Task WillCreateAuditRecordUsingAuditService() { - var auditingService = new Mock(); + var auditingService = new Mock(); var service = CreateService(auditingService: auditingService); var packageRegistration = new PackageRegistration(); var package = new Package { PackageRegistration = packageRegistration, Version = "1.0.0", Hash = _packageHashForTests }; @@ -227,7 +227,7 @@ public async Task WillCreateAuditRecordUsingAuditService() var testService = service as TestPackageDeleteService; Assert.Equal(package.PackageRegistration.Id, testService.LastAuditRecord.Id); Assert.Equal(package.Version, testService.LastAuditRecord.Version); - auditingService.Verify(x => x.SaveAuditRecord(testService.LastAuditRecord)); + auditingService.Verify(x => x.SaveAuditRecordAsync(testService.LastAuditRecord)); } } @@ -391,7 +391,7 @@ public async Task WillBackupAndDeleteThePackageFile() [Fact] public async Task WillCreateAuditRecordUsingAuditService() { - var auditingService = new Mock(); + var auditingService = new Mock(); var service = CreateService(auditingService: auditingService); var packageRegistration = new PackageRegistration(); var package = new Package { Key = 123, PackageRegistration = packageRegistration, Version = "1.0.0", Hash = _packageHashForTests }; @@ -405,7 +405,7 @@ public async Task WillCreateAuditRecordUsingAuditService() var testService = service as TestPackageDeleteService; Assert.Equal(package.PackageRegistration.Id, testService.LastAuditRecord.Id); Assert.Equal(package.Version, testService.LastAuditRecord.Version); - auditingService.Verify(x => x.SaveAuditRecord(testService.LastAuditRecord)); + auditingService.Verify(x => x.SaveAuditRecordAsync(testService.LastAuditRecord)); } } } diff --git a/tests/NuGetGallery.Facts/Services/PackageFileServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackageFileServiceFacts.cs index f674aa1aa5..8c873bb53c 100644 --- a/tests/NuGetGallery.Facts/Services/PackageFileServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackageFileServiceFacts.cs @@ -246,7 +246,7 @@ public async Task WillUseNormalizedRegularVersionIfNormalizedVersionMissing() var service = CreateService(fileStorageSvc: fileStorageSvc); var packageRegistraion = new PackageRegistration { Id = "theId" }; var package = new Package { PackageRegistration = packageRegistraion, NormalizedVersion = null, Version = "01.01.01" }; - fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), BuildFileName("theId", "1.1.1"), It.IsAny())) + fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), BuildFileName("theId", "1.1.1"), It.IsAny(), It.Is(b => !b))) .Completes() .Verifiable(); @@ -260,7 +260,7 @@ public async Task WillSaveTheFileViaTheFileStorageServiceUsingThePackagesFolder( { var fileStorageSvc = new Mock(); var service = CreateService(fileStorageSvc: fileStorageSvc); - fileStorageSvc.Setup(x => x.SaveFileAsync(Constants.PackagesFolderName, It.IsAny(), It.IsAny())) + fileStorageSvc.Setup(x => x.SaveFileAsync(Constants.PackagesFolderName, It.IsAny(), It.IsAny(), It.Is(b => !b))) .Completes() .Verifiable(); @@ -274,7 +274,7 @@ public async Task WillSaveTheFileViaTheFileStorageServiceUsingAFileNameWithIdAnd { var fileStorageSvc = new Mock(); var service = CreateService(fileStorageSvc: fileStorageSvc); - fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), BuildFileName("theId", "theNormalizedVersion"), It.IsAny())) + fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), BuildFileName("theId", "theNormalizedVersion"), It.IsAny(), It.Is(b => !b))) .Completes() .Verifiable(); @@ -289,7 +289,7 @@ public async Task WillSaveTheFileStreamViaTheFileStorageService() var fileStorageSvc = new Mock(); var fakeStream = new MemoryStream(); var service = CreateService(fileStorageSvc: fileStorageSvc); - fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), It.IsAny(), fakeStream)) + fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), It.IsAny(), fakeStream, It.Is(b => !b))) .Completes() .Verifiable(); @@ -370,7 +370,7 @@ public async Task WillUseNormalizedRegularVersionIfNormalizedVersionMissing() var package = new Package { PackageRegistration = packageRegistraion, NormalizedVersion = null, Version = "01.01.01", Hash = packageHashForTests}; package.Hash = packageHashForTests; - fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), BuildBackupFileName("theId", "1.1.1", packageHashForTests), It.IsAny())) + fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), BuildBackupFileName("theId", "1.1.1", packageHashForTests), It.IsAny(), It.Is(b => b))) .Completes() .Verifiable(); @@ -384,7 +384,7 @@ public async Task WillSaveTheFileViaTheFileStorageServiceUsingThePackagesFolder( { var fileStorageSvc = new Mock(); var service = CreateService(fileStorageSvc: fileStorageSvc); - fileStorageSvc.Setup(x => x.SaveFileAsync(Constants.PackageBackupsFolderName, It.IsAny(), It.IsAny())) + fileStorageSvc.Setup(x => x.SaveFileAsync(Constants.PackageBackupsFolderName, It.IsAny(), It.IsAny(), It.Is(b => b))) .Completes() .Verifiable(); @@ -401,7 +401,7 @@ public async Task WillSaveTheFileViaTheFileStorageServiceUsingAFileNameWithIdAnd { var fileStorageSvc = new Mock(); var service = CreateService(fileStorageSvc: fileStorageSvc); - fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), BuildBackupFileName("theId", "theNormalizedVersion", packageHashForTests), It.IsAny())) + fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), BuildBackupFileName("theId", "theNormalizedVersion", packageHashForTests), It.IsAny(), It.Is(b => b))) .Completes() .Verifiable(); @@ -419,7 +419,7 @@ public async Task WillSaveTheFileStreamViaTheFileStorageService() var fileStorageSvc = new Mock(); var fakeStream = new MemoryStream(); var service = CreateService(fileStorageSvc: fileStorageSvc); - fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), It.IsAny(), fakeStream)) + fileStorageSvc.Setup(x => x.SaveFileAsync(It.IsAny(), It.IsAny(), fakeStream, It.Is(b => b))) .Completes() .Verifiable(); diff --git a/tests/NuGetGallery.Facts/Services/PackageNamingConflictValidatorFacts.cs b/tests/NuGetGallery.Facts/Services/PackageNamingConflictValidatorFacts.cs index 1c738c5039..41483b9a97 100644 --- a/tests/NuGetGallery.Facts/Services/PackageNamingConflictValidatorFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackageNamingConflictValidatorFacts.cs @@ -16,7 +16,7 @@ public class PackageNamingConflictValidatorFacts [InlineData("Microsoft.FooBar", "Another.Package", false)] [InlineData("Microsoft.FooBar", "another.package", false)] [InlineData("Microsoft.FooBar", "Microsoft.FooBar contribution package", false)] - private void TitleConflictsWithExistingRegistrationIdTests(string existingRegistrationId, string newPackageTitle, bool shouldBeConflict) + public void TitleConflictsWithExistingRegistrationIdTests(string existingRegistrationId, string newPackageTitle, bool shouldBeConflict) { // Arrange var existingPackageRegistration = new PackageRegistration @@ -25,12 +25,7 @@ private void TitleConflictsWithExistingRegistrationIdTests(string existingRegist Owners = new HashSet() }; - var packageRegistrationRepository = new Mock>(); - packageRegistrationRepository.Setup(r => r.GetAll()).Returns(new[] { existingPackageRegistration }.AsQueryable()); - - var packageRepository = new Mock>(); - - var target = new PackageNamingConflictValidator(packageRegistrationRepository.Object, packageRepository.Object); + var target = CreateValidator(existingPackageRegistration, package: null); // Act var result = target.TitleConflictsWithExistingRegistrationId("NewPackageId", newPackageTitle); @@ -39,7 +34,6 @@ private void TitleConflictsWithExistingRegistrationIdTests(string existingRegist Assert.True(result == shouldBeConflict); } - [Theory] [InlineData("ExistingPackageId", "ExistingPackageTitle", "NewPackageId", false)] [InlineData("ExistingPackageId", "ExistingPackageTitle", "newpackageid", false)] @@ -60,13 +54,7 @@ public void IdConflictsWithExistingPackageTitleTests(string existingPackageId, s Title = existingPackageTitle }; - var packageRegistrationRepository = new Mock>(); - packageRegistrationRepository.Setup(r => r.GetAll()).Returns(new[] { existingPackageRegistration }.AsQueryable()); - - var packageRepository = new Mock>(); - packageRepository.Setup(r => r.GetAll()).Returns(new[] { existingPackage }.AsQueryable()); - - var target = new PackageNamingConflictValidator(packageRegistrationRepository.Object, packageRepository.Object); + var target = CreateValidator(existingPackageRegistration, existingPackage); // Act var result = target.IdConflictsWithExistingPackageTitle(newPackageId); @@ -74,5 +62,74 @@ public void IdConflictsWithExistingPackageTitleTests(string existingPackageId, s // Assert Assert.True(result == shouldBeConflict); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IdConflictsWithExistingPackageTitle_DoesNotSupportTitleReuseWithNonDeletedPackage(bool isExistingPackageListed) + { + // Arrange + var packageRegistration = new PackageRegistration + { + Id = "A", + Owners = new HashSet() + }; + var package = new Package + { + PackageRegistration = packageRegistration, + Version = "1.0.0", + Title = "B", + Listed = isExistingPackageListed, + Deleted = false + }; + var target = CreateValidator(packageRegistration, package); + + // Act + var actualResult = target.IdConflictsWithExistingPackageTitle(registrationId: "B"); + + // Assert + Assert.True(actualResult); + } + + [Fact] + public void IdConflictsWithExistingPackageTitle_SupportsTitleReuseWithSoftDeletedPackage() + { + // Arrange + var packageRegistration = new PackageRegistration + { + Id = "A", + Owners = new HashSet() + }; + var package = new Package + { + PackageRegistration = packageRegistration, + Version = "1.0.0", + Title = "B", + Listed = false, + Deleted = true + }; + var target = CreateValidator(packageRegistration, package); + + // Act + var actualResult = target.IdConflictsWithExistingPackageTitle(registrationId: "B"); + + // Assert + Assert.False(actualResult); + } + + private static PackageNamingConflictValidator CreateValidator(PackageRegistration packageRegistration, Package package) + { + var packageRegistrationRepository = new Mock>(); + packageRegistrationRepository.Setup(r => r.GetAll()).Returns(new[] { packageRegistration }.AsQueryable()); + + var packageRepository = new Mock>(); + + if (package != null) + { + packageRepository.Setup(r => r.GetAll()).Returns(new[] { package }.AsQueryable()); + } + + return new PackageNamingConflictValidator(packageRegistrationRepository.Object, packageRepository.Object); + } } } \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs index 3c0f6ad00f..cf54495582 100644 --- a/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs @@ -8,7 +8,9 @@ using Moq; using NuGet.Frameworks; using NuGet.Packaging; +using NuGet.Packaging.Core; using NuGet.Versioning; +using NuGetGallery.Auditing; using NuGetGallery.Framework; using NuGetGallery.Packaging; using Xunit; @@ -34,7 +36,8 @@ private static Mock CreateNuGetPackage( Uri projectUrl = null, Uri iconUrl = null, bool requireLicenseAcceptance = true, - IEnumerable packageDependencyGroups = null) + IEnumerable packageDependencyGroups = null, + IEnumerable packageTypes = null) { licenseUrl = licenseUrl ?? new Uri("http://thelicenseurl/"); projectUrl = projectUrl ?? new Uri("http://theprojecturl/"); @@ -71,11 +74,20 @@ private static Mock CreateNuGetPackage( }; } + if (packageTypes == null) + { + packageTypes = new[] + { + new NuGet.Packaging.Core.PackageType("dependency", new Version("1.0.0")), + new NuGet.Packaging.Core.PackageType("DotNetCliTool", new Version("2.1.1")) + }; + } + var testPackage = TestPackage.CreateTestPackageStream( id, version, title, summary, authors, owners, description, tags, language, copyright, releaseNotes, minClientVersion, licenseUrl, projectUrl, iconUrl, - requireLicenseAcceptance, packageDependencyGroups); + requireLicenseAcceptance, packageDependencyGroups, packageTypes); var mock = new Mock(testPackage); mock.CallBase = true; @@ -88,12 +100,14 @@ private static IPackageService CreateService( Mock> packageOwnerRequestRepo = null, Mock indexingService = null, IPackageNamingConflictValidator packageNamingConflictValidator = null, + IAuditingService auditingService = null, Action> setup = null) { packageRegistrationRepository = packageRegistrationRepository ?? new Mock>(); packageRepository = packageRepository ?? new Mock>(); packageOwnerRequestRepo = packageOwnerRequestRepo ?? new Mock>(); indexingService = indexingService ?? new Mock(); + auditingService = auditingService ?? new TestAuditingService(); if (packageNamingConflictValidator == null) { @@ -107,7 +121,8 @@ private static IPackageService CreateService( packageRepository.Object, packageOwnerRequestRepo.Object, indexingService.Object, - packageNamingConflictValidator); + packageNamingConflictValidator, + auditingService); packageService.CallBase = true; @@ -159,6 +174,27 @@ public async Task RemovesRelatedPendingOwnerRequest() repository.VerifyAll(); } + + [Fact] + public async Task WritesAnAuditRecord() + { + // Arrange + var package = new PackageRegistration { Key = 2, Id = "pkg42" }; + var pendingOwner = new User { Key = 100, Username = "teamawesome" }; + var packageRepository = new Mock>(); + var auditingService = new TestAuditingService(); + var service = CreateService( + packageRepository: packageRepository, + auditingService: auditingService); + + // Act + await service.AddPackageOwnerAsync(package, pendingOwner); + + // Assert + Assert.True(auditingService.WroteRecord(ar => + ar.Action == AuditedPackageRegistrationAction.AddOwner + && ar.Id == package.Id)); + } } public class TheConfirmPackageOwnerMethod @@ -729,12 +765,19 @@ private async Task WillThrowIfTheNuGetPackageDependenciesIsLongerThanInt16MaxVal { var service = CreateService(); var versionSpec = VersionRange.Parse("[1.0]"); + + var numDependencies = 5000; + var packageDependencies = new List(); + for (int i = 0; i < numDependencies; i++) + { + packageDependencies.Add(new NuGet.Packaging.Core.PackageDependency("dependency" + i, versionSpec)); + } + var nugetPackage = CreateNuGetPackage(packageDependencyGroups: new[] { new PackageDependencyGroup( new NuGetFramework("net40"), - Enumerable.Repeat( - new NuGet.Packaging.Core.PackageDependency("theFirstDependency", versionSpec), 5000)), + packageDependencies), }); var ex = await Assert.ThrowsAsync(async () => await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), null)); @@ -899,29 +942,7 @@ private async Task WillSaveSupportedFrameworks() Assert.Equal("net40", package.SupportedFrameworks.First().TargetFramework); Assert.Equal("net35", package.SupportedFrameworks.ElementAt(1).TargetFramework); } - - [Fact] - private async Task WillNotSaveAnySupportedFrameworksWhenThereIsANullTargetFramework() - { - var packageRegistrationRepository = new Mock>(); - var service = CreateService(packageRegistrationRepository: packageRegistrationRepository, setup: mockPackageService => - { - mockPackageService.Setup(p => p.FindPackageRegistrationById(It.IsAny())).Returns((PackageRegistration)null); - mockPackageService.Setup(p => p.GetSupportedFrameworks(It.IsAny())).Returns( - new[] - { - null, - NuGetFramework.Parse("net35") - }); - }); - var nugetPackage = CreateNuGetPackage(); - var currentUser = new User(); - - var package = await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), currentUser); - - Assert.Empty(package.SupportedFrameworks); - } - + [Fact] private async Task WillNotSaveAnySupportedFrameworksWhenThereIsAnAnyTargetFramework() { @@ -1401,6 +1422,28 @@ public async Task ThrowsWhenPackageDeleted() await Assert.ThrowsAsync(async () => await service.MarkPackageListedAsync(package)); } + + [Fact] + public async Task WritesAnAuditRecord() + { + // Arrange + var packageRegistration = new PackageRegistration { Id = "theId" }; + var package = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = false }; + var packageRepository = new Mock>(); + var auditingService = new TestAuditingService(); + var service = CreateService( + packageRepository: packageRepository, + auditingService: auditingService); + + // Act + await service.MarkPackageListedAsync(package); + + // Assert + Assert.True(auditingService.WroteRecord(ar => + ar.Action == AuditedPackageAction.List + && ar.Id == package.PackageRegistration.Id + && ar.Version == package.Version)); + } } public class TheMarkPackageUnlistedMethod @@ -1487,6 +1530,28 @@ public async Task OnOnlyListedPackageSetsNoPackageToLatestVersion() Assert.False(package.IsLatest, "IsLatest"); Assert.False(package.IsLatestStable, "IsLatestStable"); } + + [Fact] + public async Task WritesAnAuditRecord() + { + // Arrange + var packageRegistration = new PackageRegistration { Id = "theId" }; + var package = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = true }; + var packageRepository = new Mock>(); + var auditingService = new TestAuditingService(); + var service = CreateService( + packageRepository: packageRepository, + auditingService: auditingService); + + // Act + await service.MarkPackageUnlistedAsync(package); + + // Assert + Assert.True(auditingService.WroteRecord(ar => + ar.Action == AuditedPackageAction.Unlist + && ar.Id == package.PackageRegistration.Id + && ar.Version == package.Version)); + } } public class ThePublishPackageMethod @@ -1821,6 +1886,27 @@ public async Task RemovesPendingPackageOwner() Assert.Contains(owner, package.Owners); packageOwnerRequestRepository.VerifyAll(); } + + [Fact] + public async Task WritesAnAuditRecord() + { + // Arrange + var package = new PackageRegistration { Key = 2, Id = "pkg42" }; + var ownerToRemove = new User { Key = 100, Username = "teamawesome" }; + var packageRepository = new Mock>(); + var auditingService = new TestAuditingService(); + var service = CreateService( + packageRepository: packageRepository, + auditingService: auditingService); + + // Act + await service.RemovePackageOwnerAsync(package, ownerToRemove); + + // Assert + Assert.True(auditingService.WroteRecord(ar => + ar.Action == AuditedPackageRegistrationAction.RemoveOwner + && ar.Id == package.Id)); + } } public class TheSetLicenseReportVisibilityMethod diff --git a/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs new file mode 100644 index 0000000000..3fe39f40f3 --- /dev/null +++ b/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs @@ -0,0 +1,393 @@ +// 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.Data.Entity; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using Moq; +using NuGet.Packaging; +using NuGetGallery.Framework; +using NuGetGallery.Packaging; +using Xunit; + +namespace NuGetGallery +{ + public class ReflowPackageServiceFacts + { + private static readonly string _packageHashForTests = "NzMzMS1QNENLNEczSDQ1SA=="; + + private static ReflowPackageService CreateService( + Mock entitiesContext = null, + Mock packageService = null, + Mock packageFileService = null, + Action> setup = null) + { + var dbContext = new Mock(); + entitiesContext = entitiesContext ?? new Mock(); + entitiesContext.Setup(m => m.GetDatabase()).Returns(dbContext.Object.Database); + + packageService = packageService ?? new Mock(); + packageFileService = packageFileService ?? new Mock(); + + var reflowPackageService = new Mock( + entitiesContext.Object, + packageService.Object, + packageFileService.Object); + + reflowPackageService.CallBase = true; + + if (setup != null) + { + setup(reflowPackageService); + } + + return reflowPackageService.Object; + } + + public class TheReflowAsyncMethod + { + [Fact] + public async Task ReturnsNullWhenPackageNotFound() + { + // Arrange + var package = CreateTestPackage(); + + var packageService = SetupPackageService(package); + var entitiesContext = SetupEntitiesContext(); + var packageFileService = SetupPackageFileService(package); + + var service = CreateService( + packageService: packageService, + entitiesContext: entitiesContext, + packageFileService: packageFileService); + + // Act + var result = await service.ReflowAsync("unknownpackage", "1.0.0"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task RetrievesOriginalPackageBinary() + { + // Arrange + var package = CreateTestPackage(); + + var packageService = SetupPackageService(package); + var entitiesContext = SetupEntitiesContext(); + var packageFileService = SetupPackageFileService(package); + + var service = CreateService( + packageService: packageService, + entitiesContext: entitiesContext, + packageFileService: packageFileService); + + // Act + await service.ReflowAsync("test", "1.0.0"); + + // Assert + packageFileService.Verify(); + } + + [Fact] + public async Task RetrievesOriginalPackageMetadata() + { + // Arrange + var package = CreateTestPackage(); + + var packageService = SetupPackageService(package); + var entitiesContext = SetupEntitiesContext(); + var packageFileService = SetupPackageFileService(package); + + var service = CreateService( + packageService: packageService, + entitiesContext: entitiesContext, + packageFileService: packageFileService); + + // Act + await service.ReflowAsync("test", "1.0.0"); + + // Assert + packageService.Verify(); + } + + [Fact] + public async Task RemovesOriginalFrameworks_Authors_Dependencies() + { + // Arrange + var package = CreateTestPackage(); + + var packageService = SetupPackageService(package); + var entitiesContext = SetupEntitiesContext(); + var packageFileService = SetupPackageFileService(package); + + var service = CreateService( + packageService: packageService, + entitiesContext: entitiesContext, + packageFileService: packageFileService); + + // Act + await service.ReflowAsync("test", "1.0.0"); + + // Assert + entitiesContext.Verify(); + } + + [Fact] + public async Task UpdatesPackageMetadata() + { + // Arrange + var package = CreateTestPackage(); + + var packageService = SetupPackageService(package); + var entitiesContext = SetupEntitiesContext(); + var packageFileService = SetupPackageFileService(package); + + var service = CreateService( + packageService: packageService, + entitiesContext: entitiesContext, + packageFileService: packageFileService); + + // Act + var result = await service.ReflowAsync("test", "1.0.0"); + + // Assert + Assert.Equal("test", result.PackageRegistration.Id); + Assert.Equal("1.0.0", result.Version); + Assert.Equal("1.0.0", result.NormalizedVersion); + Assert.Equal("Test package", result.Title); + + Assert.Equal(2, result.Authors.Count); + Assert.True(result.Authors.Any(a => a.Name == "authora")); + Assert.True(result.Authors.Any(a => a.Name == "authorb")); + Assert.Equal("authora, authorb", result.FlattenedAuthors); + + Assert.Equal(false, result.RequiresLicenseAcceptance); + Assert.Equal("package A description.", result.Description); + Assert.Equal("en-US", result.Language); + + Assert.Equal("WebActivator:[1.1.0, ):net40|PackageC:[1.1.0, 2.0.1):net40|jQuery:(, ):net451", result.FlattenedDependencies); + Assert.Equal(3, result.Dependencies.Count); + + Assert.True(result.Dependencies.Any(d => + d.Id == "WebActivator" + && d.VersionSpec == "[1.1.0, )" + && d.TargetFramework == "net40")); + + Assert.True(result.Dependencies.Any(d => + d.Id == "PackageC" + && d.VersionSpec == "[1.1.0, 2.0.1)" + && d.TargetFramework == "net40")); + + Assert.True(result.Dependencies.Any(d => + d.Id == "jQuery" + && d.VersionSpec == "(, )" + && d.TargetFramework == "net451")); + + Assert.Equal(0, result.SupportedFrameworks.Count); + } + + [Fact] + public async Task UpdatesPackageLastEdited() + { + // Arrange + var package = CreateTestPackage(); + var lastEdited = package.LastEdited; + + var packageService = SetupPackageService(package); + var entitiesContext = SetupEntitiesContext(); + var packageFileService = SetupPackageFileService(package); + + var service = CreateService( + packageService: packageService, + entitiesContext: entitiesContext, + packageFileService: packageFileService); + + // Act + var result = await service.ReflowAsync("test", "1.0.0"); + + // Assert + Assert.NotEqual(lastEdited, result.LastEdited); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DoesNotUpdatePackageListed(bool listed) + { + // Arrange + var package = CreateTestPackage(); + package.Listed = listed; + + var packageService = SetupPackageService(package); + var entitiesContext = SetupEntitiesContext(); + var packageFileService = SetupPackageFileService(package); + + var service = CreateService( + packageService: packageService, + entitiesContext: entitiesContext, + packageFileService: packageFileService); + + // Act + var result = await service.ReflowAsync("test", "1.0.0"); + + // Assert + Assert.Equal(listed, result.Listed); + } + } + + private static Package CreateTestPackage() + { + var packageRegistration = new PackageRegistration(); + packageRegistration.Id = "test"; + + var framework = new PackageFramework(); + var author = new PackageAuthor { Name = "maarten" }; + var dependency = new PackageDependency { Id = "other" }; + + var package = new Package + { + Key = 123, + PackageRegistration = packageRegistration, + Version = "1.0.0", + Hash = _packageHashForTests, + SupportedFrameworks = new List + { + framework + }, + FlattenedAuthors = "maarten", + Authors = new List + { + author + }, + Dependencies = new List + { + dependency + }, + User = new User("test") + }; + + packageRegistration.Packages.Add(package); + + return package; + } + + private static Mock SetupPackageService(Package package) + { + var packageRegistrationRepository = new Mock>(); + var packageRepository = new Mock>(); + var packageOwnerRequestRepo = new Mock>(); + var indexingService = new Mock(); + var packageNamingConflictValidator = new PackageNamingConflictValidator( + packageRegistrationRepository.Object, + packageRepository.Object); + var auditingService = new TestAuditingService(); + + var packageService = new Mock( + packageRegistrationRepository.Object, + packageRepository.Object, + packageOwnerRequestRepo.Object, + indexingService.Object, + packageNamingConflictValidator, + auditingService); + + packageService.CallBase = true; + + packageService + .Setup(s => s.FindPackageByIdAndVersion("test", "1.0.0", true)) + .Returns(package) + .Verifiable(); + + packageService + .Setup(s => s.EnrichPackageFromNuGetPackage( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .CallBase() + .Verifiable(); + + return packageService; + } + + private static Mock SetupEntitiesContext() + { + var entitiesContext = new Mock(); + + entitiesContext + .Setup(s => s.Set().Remove(It.IsAny())) + .Verifiable(); + + entitiesContext + .Setup(s => s.Set().Remove(It.IsAny())) + .Verifiable(); + + entitiesContext + .Setup(s => s.Set().Remove(It.IsAny())) + .Verifiable(); + + return entitiesContext; + } + + private static Mock SetupPackageFileService(Package package) + { + var packageFileService = new Mock(); + + packageFileService + .Setup(s => s.DownloadPackageFileAsync(package)) + .Returns(Task.FromResult(CreateTestPackageStream())) + .Verifiable(); + + return packageFileService; + } + + private static Stream CreateTestPackageStream() + { + var packageStream = new MemoryStream(); + using (var packageArchive = new ZipArchive(packageStream, ZipArchiveMode.Create, true)) + { + var nuspecEntry = packageArchive.CreateEntry("TestPackage.nuspec", CompressionLevel.Fastest); + using (var streamWriter = new StreamWriter(nuspecEntry.Open())) + { + streamWriter.WriteLine(@" + + + test + 1.0.0 + Test package + authora, authorb + ownera + false + package A description. + en-US + http://www.nuget.org/ + http://www.nuget.org/ + http://www.nuget.org/ + + + + + + + + + + + "); + } + + packageArchive.CreateEntry("content\\HelloWorld.cs", CompressionLevel.Fastest); + } + + packageStream.Position = 0; + + return packageStream; + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Services/ScopeEvaluatorFacts.cs b/tests/NuGetGallery.Facts/Services/ScopeEvaluatorFacts.cs new file mode 100644 index 0000000000..3a56aabb29 --- /dev/null +++ b/tests/NuGetGallery.Facts/Services/ScopeEvaluatorFacts.cs @@ -0,0 +1,173 @@ +// 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 Newtonsoft.Json; +using NuGetGallery.Authentication; +using Xunit; + +namespace NuGetGallery +{ + public class ScopeEvaluatorFacts + { + [Theory] + [MemberData("ScopeClaimsAllowsActionForSubjectEvaluatesCorrectlyData")] + public void ScopeClaimsAllowsActionForSubjectEvaluatesCorrectly( + string scopeClaims, string subject, string[] requestedActions, bool expectedResult) + { + var result = ScopeEvaluator.ScopeClaimsAllowsActionForSubject( + scopeClaims, + subject, + requestedActions); + + Assert.Equal(expectedResult, result); + } + + private static string BuildScopeClaim(params Scope[] scopes) + { + return JsonConvert.SerializeObject(scopes); + } + + public static IEnumerable ScopeClaimsAllowsActionForSubjectEvaluatesCorrectlyData + { + get + { + return new[] + { + // Push new package with legacy API key (no scopes) + new object[] { + string.Empty, + "SomePackage", + new[] { NuGetScopes.PackagePush }, + true + }, + + // Push new package with legacy API key (no scopes) + new object[] { + BuildScopeClaim(), + "SomePackage", + new[] { NuGetScopes.PackagePush }, + true + }, + + new object[] + { + BuildScopeClaim( + new Scope("SomePackage", NuGetScopes.PackagePush), + new Scope("SomePackage", NuGetScopes.PackagePush)), + null, + new[] { NuGetScopes.PackagePush }, + true + }, + + // Push new package with scoped API key which allows NuGetScopes.PackagePush for the given package + new object[] + { + BuildScopeClaim( + new Scope("SomePackage", NuGetScopes.PackagePush)), + "SomePackage", + new[] { NuGetScopes.PackagePush }, + true + }, + + // Push new package with scoped API key which allows NuGetScopes.All for all packages + new object[] + { + BuildScopeClaim( + new Scope("*", NuGetScopes.All)), + "SomePackage", + new[] { NuGetScopes.PackagePush }, + true + }, + + // Push new package with scoped API key which allows NuGetScopes.List + new object[] + { + BuildScopeClaim( + new Scope("*", NuGetScopes.PackageUnlist)), + "SomePackage", + new[] { NuGetScopes.PackagePush }, + false + }, + + // Push new package with scoped API key which allows NuGetScopes.List for all packages, + // and NuGetScopes.PackagePush for the given package + new object[] + { + BuildScopeClaim( + new Scope("*", NuGetScopes.PackageUnlist), + new Scope("SomePackage", NuGetScopes.PackagePush)), + "SomePackage", + new[] { NuGetScopes.PackagePush }, + true + }, + + // Push new package with scoped API key which allows NuGetScopes.List for the given package, + // and NuGetScopes.PackagePush for another package + new object[] + { + BuildScopeClaim( + new Scope("SomePackage", NuGetScopes.PackageUnlist), + new Scope("SomeOtherPackage", NuGetScopes.All)), + "SomePackage", + new[] { NuGetScopes.PackagePush }, + false + }, + + // Push new package with scoped API key which allows NuGetScopes.List for the given package, + // and NuGetScopes.All for another package, and no subject known + new object[] + { + BuildScopeClaim( + new Scope("SomePackage", NuGetScopes.PackageUnlist), + new Scope("SomeOtherPackage", NuGetScopes.All)), + "", + new[] { NuGetScopes.PackagePush }, + true + }, + + // Push new package with scoped API key which allows NuGetScopes.PackagePush for all packages, + // and no subject known + new object[] + { + BuildScopeClaim( + new Scope("*", NuGetScopes.PackagePush)), + "", + new[] { NuGetScopes.PackagePush }, + true + }, + + // Push package with a matching package pattern + new object[] + { + BuildScopeClaim( + new Scope("Microsoft.*.Abstract", NuGetScopes.PackagePush)), + "Microsoft.Configuration.Abstract", + new [] { NuGetScopes.PackagePush }, + true + }, + + // Push package with a non-matching package pattern + new object[] + { + BuildScopeClaim( + new Scope("Microsoft.*.Abstract", NuGetScopes.PackagePush)), + "Microsoft.Configuration", + new [] { NuGetScopes.PackagePush }, + false + }, + + // Push package when package pattern subject contains invalid characters + new object[] + { + BuildScopeClaim( + new Scope("%@~!>^/\"*", NuGetScopes.PackagePush)), + "Microsoft.Configuration", + new [] { NuGetScopes.PackagePush }, + false + } + }; + } + } + } +} \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Services/UploadFileServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UploadFileServiceFacts.cs index de60927c90..c33767d1cc 100644 --- a/tests/NuGetGallery.Facts/Services/UploadFileServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UploadFileServiceFacts.cs @@ -137,7 +137,7 @@ public void WillSaveTheUploadToTheUploadsFolder() service.SaveUploadFileAsync(1, new MemoryStream()); - fakeFileStorageService.Verify(x => x.SaveFileAsync(Constants.UploadsFolderName, It.IsAny(), It.IsAny())); + fakeFileStorageService.Verify(x => x.SaveFileAsync(Constants.UploadsFolderName, It.IsAny(), It.IsAny(), It.Is(b => b))); } [Fact] @@ -149,7 +149,7 @@ public void WillUseTheUserKeyInTheFileName() service.SaveUploadFileAsync(1, new MemoryStream()); - fakeFileStorageService.Verify(x => x.SaveFileAsync(It.IsAny(), expectedFileName, It.IsAny())); + fakeFileStorageService.Verify(x => x.SaveFileAsync(It.IsAny(), expectedFileName, It.IsAny(), It.Is(b => b))); } [Fact] @@ -161,7 +161,7 @@ public void WillSaveTheUploadFileStream() service.SaveUploadFileAsync(1, fakeUploadFileStream); - fakeFileStorageService.Verify(x => x.SaveFileAsync(It.IsAny(), It.IsAny(), fakeUploadFileStream)); + fakeFileStorageService.Verify(x => x.SaveFileAsync(It.IsAny(), It.IsAny(), fakeUploadFileStream, It.Is(b => b))); } } } diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index fcfdbabf10..06cc0f2228 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -15,58 +15,6 @@ namespace NuGetGallery { public class UserServiceFacts { - public static bool VerifyPasswordHash(string hash, string algorithm, string password) - { - bool canAuthenticate = CryptographyService.ValidateSaltedHash( - hash, - password, - algorithm); - - bool sanity = CryptographyService.ValidateSaltedHash( - hash, - "not_the_password", - algorithm); - - return canAuthenticate && !sanity; - } - - public static Credential CreatePasswordCredential(string password) - { - return new Credential( - type: CredentialTypes.Password.Pbkdf2, - value: CryptographyService.GenerateSaltedHash( - password, - Constants.PBKDF2HashAlgorithmId)); - } - - // Now only for things that actually need a MOCK UserService object. - private static UserService CreateMockUserService(Action> setup, Mock> userRepo = null, Mock config = null) - { - if (config == null) - { - config = new Mock(); - config.Setup(x => x.ConfirmEmailAddresses).Returns(true); - } - - userRepo = userRepo ?? new Mock>(); - var credRepo = new Mock>(); - - var userService = new Mock( - config.Object, - userRepo.Object, - credRepo.Object) - { - CallBase = true - }; - - if (setup != null) - { - setup(userService); - } - - return userService.Object; - } - public class TheConfirmEmailAddressMethod { [Fact] @@ -166,7 +114,7 @@ public async Task WritesAuditRecord() var confirmed = await service.ConfirmEmailAddress(user, "secret"); Assert.True(service.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.ConfirmEmail && + ar.Action == AuditedUserAction.ConfirmEmail && ar.AffectedEmailAddress == "new@example.com")); } } @@ -298,7 +246,7 @@ public async Task WritesAuditRecord() // Assert Assert.True(service.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.ChangeEmail && + ar.Action == AuditedUserAction.ChangeEmail && ar.AffectedEmailAddress == "new@example.org" && ar.EmailAddress == "old@example.org")); } @@ -353,7 +301,7 @@ public async Task WritesAuditRecord() // Assert Assert.True(service.Auditing.WroteRecord(ar => - ar.Action == UserAuditAction.CancelChangeEmail && + ar.Action == AuditedUserAction.CancelChangeEmail && ar.AffectedEmailAddress == "unconfirmedEmail@example.org" && ar.EmailAddress == "confirmedEmail@example.org")); } diff --git a/tests/NuGetGallery.Facts/TestPackage.cs b/tests/NuGetGallery.Facts/TestPackage.cs index da2d3acc09..8853612102 100644 --- a/tests/NuGetGallery.Facts/TestPackage.cs +++ b/tests/NuGetGallery.Facts/TestPackage.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using NuGet.Packaging; +using NuGet.Packaging.Core; namespace NuGetGallery { @@ -32,7 +33,8 @@ public static void WriteNuspec( Uri projectUrl = null, Uri iconUrl = null, bool requireLicenseAcceptance = false, - IEnumerable packageDependencyGroups = null) + IEnumerable packageDependencyGroups = null, + IEnumerable packageTypes = null) { using (var streamWriter = new StreamWriter(stream, new UTF8Encoding(false, true), 1024, leaveStreamOpen)) { @@ -54,12 +56,39 @@ public static void WriteNuspec( " + (licenseUrl?.ToString() ?? string.Empty) + @" " + (projectUrl?.ToString() ?? string.Empty) + @" " + (iconUrl?.ToString() ?? string.Empty) + @" + " + WritePackageTypes(packageTypes) + @" " + WriteDependencies(packageDependencyGroups) + @" "); } } + private static string WritePackageTypes(IEnumerable packageTypes) + { + if (packageTypes == null || !packageTypes.Any()) + { + return string.Empty; + } + + var output = new StringBuilder(); + foreach(var packageType in packageTypes) + { + output.Append(""); + } + return output.ToString(); + } + private static string WriteDependencies(IEnumerable packageDependencyGroups) { if (packageDependencyGroups == null || !packageDependencyGroups.Any()) @@ -112,6 +141,7 @@ public static Stream CreateTestPackageStream( Uri iconUrl = null, bool requireLicenseAcceptance = false, IEnumerable packageDependencyGroups = null, + IEnumerable packageTypes = null, Action populatePackage = null) { return CreateTestPackageStream(packageArchive => @@ -121,7 +151,7 @@ public static Stream CreateTestPackageStream( { WriteNuspec(stream, true, id, version, title, summary, authors, owners, description, tags, language, copyright, releaseNotes, minClientVersion, licenseUrl, projectUrl, iconUrl, - requireLicenseAcceptance, packageDependencyGroups); + requireLicenseAcceptance, packageDependencyGroups, packageTypes); } if (populatePackage != null) diff --git a/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs b/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs index 614415c2c1..969583c167 100644 --- a/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs +++ b/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs @@ -74,6 +74,18 @@ public IDbSet Credentials } } + public IDbSet Scopes + { + get + { + return Set(); + } + set + { + throw new NotSupportedException(); + } + } + public IDbSet Users { get diff --git a/tests/NuGetGallery.Facts/TestUtils/Infrastructure/FeedServiceHelpers.cs b/tests/NuGetGallery.Facts/TestUtils/Infrastructure/FeedServiceHelpers.cs index e63a359abb..96f2dce4b6 100644 --- a/tests/NuGetGallery.Facts/TestUtils/Infrastructure/FeedServiceHelpers.cs +++ b/tests/NuGetGallery.Facts/TestUtils/Infrastructure/FeedServiceHelpers.cs @@ -148,9 +148,10 @@ public static HttpServer SetupODataServer(TestDependencyResolver dependencyResol // Controllers var repo = SetupTestPackageRepository(); - var configuration = new Mock(MockBehavior.Strict); + var configuration = new Mock(MockBehavior.Strict); configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://nuget.org/"); - configuration.Setup(c => c.Features).Returns(new FeatureConfiguration() { FriendlyLicenses = true }); + configuration.Setup(c => c.Features).Returns(new FeatureConfiguration { FriendlyLicenses = true }); + configuration.Setup(c => c.Current).Returns(new AppConfiguration() { IsODataFilterEnabled = false }); var searchService = new Mock(MockBehavior.Strict); searchService.Setup(s => s.Search(It.IsAny())).Returns diff --git a/tests/NuGetGallery.Facts/TestUtils/Infrastructure/TestableV1Feed.cs b/tests/NuGetGallery.Facts/TestUtils/Infrastructure/TestableV1Feed.cs index 02d07f8928..c9672a1933 100644 --- a/tests/NuGetGallery.Facts/TestUtils/Infrastructure/TestableV1Feed.cs +++ b/tests/NuGetGallery.Facts/TestUtils/Infrastructure/TestableV1Feed.cs @@ -11,7 +11,7 @@ public class TestableV1Feed : ODataV1FeedController { public TestableV1Feed( IEntityRepository repo, - ConfigurationService configuration, + IGalleryConfigurationService configuration, ISearchService searchService) : base(repo, configuration, searchService) { diff --git a/tests/NuGetGallery.Facts/TestUtils/Infrastructure/TestableV2Feed.cs b/tests/NuGetGallery.Facts/TestUtils/Infrastructure/TestableV2Feed.cs index a919165194..15041b62c1 100644 --- a/tests/NuGetGallery.Facts/TestUtils/Infrastructure/TestableV2Feed.cs +++ b/tests/NuGetGallery.Facts/TestUtils/Infrastructure/TestableV2Feed.cs @@ -11,7 +11,7 @@ public class TestableV2Feed : ODataV2FeedController { public TestableV2Feed( IEntityRepository repo, - ConfigurationService configuration, + IGalleryConfigurationService configuration, ISearchService searchService) : base(repo, configuration, searchService) { diff --git a/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs b/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs index e3baebcab2..041684b023 100644 --- a/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs +++ b/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs @@ -63,20 +63,40 @@ public static void VerifyCommitChanges(this IEntitiesContext self) public static void VerifyCommitted(this Mock> self) where T : class, IEntity, new() { - self.Verify(e => e.CommitChangesAsync()); + self.VerifyCommitted(Times.AtLeastOnce()); + } + + public static void VerifyCommitted(this Mock> self, Times times) + where T : class, IEntity, new() + { + self.Verify(e => e.CommitChangesAsync(), times); } public static void VerifyCommitted(this Mock self) { - self.Verify(e => e.SaveChangesAsync()); + self.VerifyCommitted(Times.AtLeastOnce()); + } + + public static void VerifyCommitted(this Mock self, Times times) + { + self.Verify(e => e.SaveChangesAsync(), times); } public static IReturnsResult SetupAuth(this Mock self, Credential cred, User user) { - return self.Setup(us => us.Authenticate(It.Is(c => - String.Equals(c.Type, cred.Type, StringComparison.OrdinalIgnoreCase) && - String.Equals(c.Value, cred.Value, StringComparison.Ordinal)))) - .Returns(user == null ? null : new AuthenticatedUser(user, cred)); + if (CredentialTypes.IsApiKey(cred.Type)) + { + return self.Setup(us => us.Authenticate(It.Is(c => + string.Equals(c, cred.Value, StringComparison.Ordinal)))) + .Returns(Task.FromResult(user == null ? null : new AuthenticatedUser(user, cred))); + } + else + { + return self.Setup(us => us.Authenticate(It.Is(c => + string.Equals(c.Type, cred.Type, StringComparison.OrdinalIgnoreCase) && + string.Equals(c.Value, cred.Value, StringComparison.Ordinal)))) + .Returns(Task.FromResult(user == null ? null : new AuthenticatedUser(user, cred))); + } } } } diff --git a/tests/NuGetGallery.Facts/TestUtils/OwinAssert.cs b/tests/NuGetGallery.Facts/TestUtils/OwinAssert.cs index 344e442f2e..6953b372cf 100644 --- a/tests/NuGetGallery.Facts/TestUtils/OwinAssert.cs +++ b/tests/NuGetGallery.Facts/TestUtils/OwinAssert.cs @@ -25,7 +25,7 @@ public static void WillRedirect(IOwinContext context, string expectedLocation) Assert.Equal(expectedLocation, context.Response.Headers["Location"]); } - public static void SetsCookie(IOwinResponse response, string name, string expectedValue) + public static void SetsCookie(IOwinResponse response, string name, string expectedValue, bool secure = true) { // Get the cookie var cookie = GetCookie(response, name); @@ -33,6 +33,12 @@ public static void SetsCookie(IOwinResponse response, string name, string expect // Check the value Assert.NotNull(cookie); Assert.Equal(expectedValue, cookie.Value); + + // Check cookie attributes + var header = response.Headers["Set-Cookie"]; + Assert.True(!String.IsNullOrEmpty(header)); + Assert.True(header.IndexOf("HttpOnly", StringComparison.OrdinalIgnoreCase) > 0); + Assert.Equal(secure, header.IndexOf("Secure", StringComparison.OrdinalIgnoreCase) > 0); } public static void DeletesCookie(IOwinResponse response, string name) diff --git a/tests/NuGetGallery.Facts/ViewModels/ListPackageItemViewModelFacts.cs b/tests/NuGetGallery.Facts/ViewModels/ListPackageItemViewModelFacts.cs new file mode 100644 index 0000000000..bc5d7d3a74 --- /dev/null +++ b/tests/NuGetGallery.Facts/ViewModels/ListPackageItemViewModelFacts.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace NuGetGallery.ViewModels +{ + public class ListPackageItemViewModelFacts + { + // start with replicating the PackageViewModelFacts here since we shouldn't be breaking these + // ListPackageItemViewModel extends PackageViewModel + #region CopiedFromPackageViewModelFacts + [Fact] + public void UsesNormalizedVersionForDisplay() + { + var package = new Package() + { + Version = "01.02.00.00", + NormalizedVersion = "1.3.0" // Different just to prove the View Model is using the DB column. + }; + var packageViewModel = new ListPackageItemViewModel(package); + Assert.Equal("1.3.0", packageViewModel.Version); + } + + [Fact] + public void UsesNormalizedPackageVersionIfNormalizedVersionMissing() + { + var package = new Package() + { + Version = "01.02.00.00" + }; + var packageViewModel = new ListPackageItemViewModel(package); + Assert.Equal("1.2.0", packageViewModel.Version); + } + + [Fact] + public void LicenseNamesAreParsedByCommas() + { + var package = new Package + { + LicenseNames = "l1,l2, l3 ,l4 , l5 ", + }; + var packageViewModel = new ListPackageItemViewModel(package); + Assert.Equal(new string[] { "l1", "l2", "l3", "l4", "l5" }, packageViewModel.LicenseNames); + } + + [Fact] + public void LicenseReportFieldsKeptWhenLicenseReportDisabled() + { + var package = new Package + { + HideLicenseReport = true, + LicenseNames = "l1", + LicenseReportUrl = "url" + }; + var packageViewModel = new ListPackageItemViewModel(package); + Assert.NotNull(packageViewModel.LicenseNames); + Assert.NotNull(packageViewModel.LicenseReportUrl); + } + + [Fact] + public void LicenseReportUrlKeptWhenLicenseReportEnabled() + { + var package = new Package + { + HideLicenseReport = false, + LicenseReportUrl = "url" + }; + var packageViewModel = new ListPackageItemViewModel(package); + Assert.NotNull(packageViewModel.LicenseReportUrl); + } + + [Fact] + public void LicenseNamesKeptWhenLicenseReportEnabled() + { + var package = new Package + { + HideLicenseReport = false, + LicenseNames = "l1" + }; + var packageViewModel = new ListPackageItemViewModel(package); + Assert.NotNull(packageViewModel.LicenseNames); + } + + [Fact] + public void LicenseUrlKeptWhenLicenseReportDisabled() + { + var package = new Package + { + HideLicenseReport = true, + LicenseUrl = "url" + }; + var packageViewModel = new ListPackageItemViewModel(package); + Assert.NotNull(packageViewModel.LicenseUrl); + } + #endregion + + [Fact] + public void ShortDescriptionsNotTruncated() + { + var description = "A Short Description"; + var package = new Package() + { + Description = description + }; + + var listPackageItemViewModel = new ListPackageItemViewModel(package); + + Assert.Equal(description, listPackageItemViewModel.ShortDescription); + Assert.Equal(false, listPackageItemViewModel.IsDescriptionTruncated); + } + + [Fact] + public void LongDescriptionsTruncated() + { + var omission = "..."; + var description = @"A Longer description full of nonsense that will get truncated. Lorem ipsum dolor sit amet, ad nemore gubergren eam. Ea quaeque labores deseruisse his, eos munere convenire at, in eos audire persius corpora. Te his volumus detracto offendit, has ne illud choro. No illum quaestio mel, novum democritum te sea, et nam nisl officiis salutandi. Vis ut harum docendi incorrupte, nam affert putent sententiae id, mei cibo omnium id. Ea est falli graeci voluptatibus, est mollis denique ne. +An nec tempor cetero vituperata.Ius cu dicunt regione interpretaris, posse veniam facilisis ad vim, sit ei sale integre. Mel cu aliquid impedit scribentur.Nostro recusabo sea ei, nec habeo instructior no, saepe altera adversarium vel cu.Nonumes molestiae sit at, per enim necessitatibus cu. +At mei iriure dignissim theophrastus.Meis nostrud te sit, equidem maiorum pri ex.Vim dolorem fuisset an. At sit veri illum oratio, et per dicat contentiones. In eam tale tation, mei dicta labitur corpora ei, homero equidem suscipit ut eam."; + + var package = new Package() + { + Description = description + }; + + var listPackageItemViewModel = new ListPackageItemViewModel(package); + + Assert.NotEqual(description, listPackageItemViewModel.ShortDescription); + Assert.Equal(true, listPackageItemViewModel.IsDescriptionTruncated); + Assert.True(listPackageItemViewModel.ShortDescription.EndsWith(omission)); + Assert.True(description.Contains(listPackageItemViewModel.ShortDescription.Substring(0, listPackageItemViewModel.ShortDescription.Length - 1 - omission.Length))); + } + + [Fact] + public void LongDescriptionsSingleWordTruncatedToLimit() + { + var charLimit = 300; + var omission = "..."; + var description = @"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"; + + var package = new Package() + { + Description = description + }; + + var listPackageItemViewModel = new ListPackageItemViewModel(package); + + Assert.Equal(charLimit + omission.Length, listPackageItemViewModel.ShortDescription.Length); + Assert.Equal(true, listPackageItemViewModel.IsDescriptionTruncated); + Assert.True(listPackageItemViewModel.ShortDescription.EndsWith(omission)); + } + + [Fact] + public void EmptyTagsAreParsedEmpty() + { + var package = new Package() { }; + + var listPackageItemViewModel = new ListPackageItemViewModel(package); + + Assert.Equal(null, listPackageItemViewModel.Tags); + } + + [Fact] + public void TagsAreParsed() + { + var package = new Package() + { + Tags = "tag1 tag2 tag3" + }; + + var listPackageItemViewModel = new ListPackageItemViewModel(package); + + Assert.Equal(3, listPackageItemViewModel.Tags.Count()); + Assert.True(listPackageItemViewModel.Tags.Contains("tag1")); + Assert.True(listPackageItemViewModel.Tags.Contains("tag2")); + Assert.True(listPackageItemViewModel.Tags.Contains("tag3")); + } + + [Fact] + public void AuthorsIsFlattenedAuthors() + { + var authors = new HashSet(); + var author1 = new PackageAuthor + { + Name = "author1" + }; + var author2 = new PackageAuthor + { + Name = "author2" + }; + + authors.Add(author1); + authors.Add(author2); + + var flattenedAuthors = "something Completely different"; + + var package = new Package() + { + Authors = authors, + FlattenedAuthors = flattenedAuthors + }; + + var listPackageItemViewModel = new ListPackageItemViewModel(package); + + Assert.Equal(flattenedAuthors, listPackageItemViewModel.Authors); + } + + [Fact] + public void UseVersionIfLatestAndStableNotSame() + { + var package = new Package() + { + IsLatest = true, + IsLatestStable = false + }; + + var listPackageItemViewModel = new ListPackageItemViewModel(package); + Assert.True(listPackageItemViewModel.UseVersion); + + listPackageItemViewModel.LatestVersion = false; + listPackageItemViewModel.LatestStableVersion = true; + Assert.True(listPackageItemViewModel.UseVersion); + + listPackageItemViewModel.LatestVersion = false; + listPackageItemViewModel.LatestStableVersion = false; + Assert.True(listPackageItemViewModel.UseVersion); + + listPackageItemViewModel.LatestVersion = true; + listPackageItemViewModel.LatestStableVersion = true; + Assert.False(listPackageItemViewModel.UseVersion); + } + } +} diff --git a/tests/NuGetGallery.Facts/packages.config b/tests/NuGetGallery.Facts/packages.config index 414e95d794..260d85fe54 100644 --- a/tests/NuGetGallery.Facts/packages.config +++ b/tests/NuGetGallery.Facts/packages.config @@ -2,7 +2,9 @@ + + @@ -11,6 +13,10 @@ + + + + @@ -18,6 +24,7 @@ + @@ -25,28 +32,55 @@ + - + - - - + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/tests/NuGetGallery.FunctionalTests.Core/EnvironmentSettings.cs b/tests/NuGetGallery.FunctionalTests.Core/EnvironmentSettings.cs index b2428d337e..4599b471d1 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/EnvironmentSettings.cs +++ b/tests/NuGetGallery.FunctionalTests.Core/EnvironmentSettings.cs @@ -2,6 +2,8 @@ // 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; namespace NuGetGallery.FunctionalTests { @@ -11,78 +13,115 @@ namespace NuGetGallery.FunctionalTests public class EnvironmentSettings { private static string _baseurl; + private static string _searchServiceBaseurl; private static string _testAccountName; private static string _testAccountPassword; private static string _testAccountApiKey; + private static string _testAccountApiKey_Unlist; + private static string _testAccountApiKey_PushPackage; + private static string _testAccountApiKey_PushVersion; private static string _testEmailServerHost; - private static string _runFunctionalTests; - private static string _readOnlyMode; + private static List _trustedHttpsCertificates; /// - /// Option to enable or disable functional tests from the current run. + /// The environment against which the test has to be run. The value would be picked from env variable. + /// If nothing is specified, preview is used as default. /// - public static string RunFunctionalTests + public static string BaseUrl { get { - if (string.IsNullOrEmpty(_runFunctionalTests)) + if (string.IsNullOrEmpty(_baseurl)) { - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("RunFunctionalTests"))) - _runFunctionalTests = "False"; + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GalleryUrl", + EnvironmentVariableTarget.User)) && + string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GalleryUrl", + EnvironmentVariableTarget.Process)) && + string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GalleryUrl", + EnvironmentVariableTarget.Machine))) + { + _baseurl = "https://int.nugettest.org/"; + } else - _runFunctionalTests = Environment.GetEnvironmentVariable("RunFunctionalTests"); + { + // Check for the environment variable under all scopes. This is to make sure that the variables are acessed properly in teamcity where we cannot set process leve variables. + if (!string.IsNullOrEmpty( + Environment.GetEnvironmentVariable("GalleryUrl", EnvironmentVariableTarget.User))) + { + _baseurl = Environment.GetEnvironmentVariable("GalleryUrl", + EnvironmentVariableTarget.User); + } + else if (!string.IsNullOrEmpty( + Environment.GetEnvironmentVariable("GalleryUrl", EnvironmentVariableTarget.Process))) + { + _baseurl = Environment.GetEnvironmentVariable("GalleryUrl", + EnvironmentVariableTarget.Process); + } + else + { + _baseurl = Environment.GetEnvironmentVariable("GalleryUrl", + EnvironmentVariableTarget.Machine); + } + } } - return _runFunctionalTests; - } - } - /// - /// Option to enable or disable functional tests from the current run. - /// - public static string ReadOnlyMode - { - get - { - if (string.IsNullOrEmpty(_readOnlyMode)) + if (string.IsNullOrEmpty(_baseurl)) { - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ReadOnlyMode"))) - _readOnlyMode = "False"; - else - _readOnlyMode = Environment.GetEnvironmentVariable("ReadOnlyMode"); + _baseurl = "https://int.nugettest.org/"; } - return _readOnlyMode; + + return _baseurl; } } /// - /// The environment against which the test has to be run. The value would be picked from env variable. + /// The environment against which the (search service) test has to be run. The value would be picked from env variable. /// If nothing is specified, preview is used as default. /// - public static string BaseUrl + public static string SearchServiceBaseUrl { get { - if (string.IsNullOrEmpty(_baseurl)) + if (string.IsNullOrEmpty(_searchServiceBaseurl)) { - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GalleryUrl", EnvironmentVariableTarget.User)) && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GalleryUrl", EnvironmentVariableTarget.Process)) && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GalleryUrl", EnvironmentVariableTarget.Machine))) - _baseurl = "https://int.nugettest.org/"; + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SearchServiceUrl", + EnvironmentVariableTarget.User)) && + string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SearchServiceUrl", + EnvironmentVariableTarget.Process)) && + string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SearchServiceUrl", + EnvironmentVariableTarget.Machine))) + { + _searchServiceBaseurl = "http://nuget-int-0-v2v3search.cloudapp.net/"; + } else { - //Check for the environment variable under all scopes. This is to make sure that the variables are acessed properly in teamcity where we cannot set process leve variables. - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GalleryUrl", EnvironmentVariableTarget.User))) - _baseurl = Environment.GetEnvironmentVariable("GalleryUrl", EnvironmentVariableTarget.User); - else if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GalleryUrl", EnvironmentVariableTarget.Process))) - _baseurl = Environment.GetEnvironmentVariable("GalleryUrl", EnvironmentVariableTarget.Process); + // Check for the environment variable under all scopes. This is to make sure that the variables are acessed properly in teamcity where we cannot set process leve variables. + if (!string.IsNullOrEmpty( + Environment.GetEnvironmentVariable("SearchServiceUrl", EnvironmentVariableTarget.User))) + { + _searchServiceBaseurl = Environment.GetEnvironmentVariable("SearchServiceUrl", + EnvironmentVariableTarget.User); + } + else if (!string.IsNullOrEmpty( + Environment.GetEnvironmentVariable("SearchServiceUrl", EnvironmentVariableTarget.Process))) + { + _searchServiceBaseurl = Environment.GetEnvironmentVariable("SearchServiceUrl", + EnvironmentVariableTarget.Process); + } else - _baseurl = Environment.GetEnvironmentVariable("GalleryUrl", EnvironmentVariableTarget.Machine); + { + _searchServiceBaseurl = Environment.GetEnvironmentVariable("SearchServiceUrl", + EnvironmentVariableTarget.Machine); + } } } - if (string.IsNullOrEmpty(_baseurl)) + if (string.IsNullOrEmpty(_searchServiceBaseurl)) { - _baseurl = "https://int.nugettest.org/"; + _searchServiceBaseurl = "http://nuget-int-0-v2v3search.cloudapp.net/"; } - return _baseurl; + + return _searchServiceBaseurl; } } @@ -117,7 +156,7 @@ public static string TestAccountPassword } /// - /// The password for the test account. + /// The full access API key for the test account. /// public static string TestAccountApiKey { @@ -131,6 +170,51 @@ public static string TestAccountApiKey } } + /// + /// Scoped API key for account. Permission: unlist + /// + public static string TestAccountApiKey_Unlist + { + get + { + if (string.IsNullOrEmpty(_testAccountApiKey_Unlist)) + { + _testAccountApiKey_Unlist = Environment.GetEnvironmentVariable("TestAccountApiKey_Unlist"); + } + return _testAccountApiKey_Unlist; + } + } + + /// + /// Scoped API key for account. Permission: push + /// + public static string TestAccountApiKey_Push + { + get + { + if (string.IsNullOrEmpty(_testAccountApiKey_PushPackage)) + { + _testAccountApiKey_PushPackage = Environment.GetEnvironmentVariable("TestAccountApiKey_Push"); + } + return _testAccountApiKey_PushPackage; + } + } + + /// + /// Scoped API key for account. Permission: push version + /// + public static string TestAccountApiKey_PushVersion + { + get + { + if (string.IsNullOrEmpty(_testAccountApiKey_PushVersion)) + { + _testAccountApiKey_PushVersion = Environment.GetEnvironmentVariable("TestAccountApiKey_PushVersion"); + } + return _testAccountApiKey_PushVersion; + } + } + public static string TestEmailServerHost { get @@ -142,5 +226,43 @@ public static string TestEmailServerHost return _testEmailServerHost; } } + + public static IEnumerable TrustedHttpsCertificates + { + get + { + if (_trustedHttpsCertificates == null) + { + var unparsedValued = Environment.GetEnvironmentVariable("TrustedHttpsCertificates") ?? string.Empty; + + List pieces; + if (unparsedValued.Length == 0) + { + // This list will need to be modified as DEV, INT, and PROD certificates change and are + // renewed. These values are easily and publicly discoverable by inspecting the certificate + // returned from HTTPS browser interactions with the gallery. + pieces = new List + { + "8c11c16610b7a147d10bbcc6a65ce23d321c12c2", // *.nugettest.org + "9d984f91f40d8b3a1fb29153179415523c4e64d1", // *.int.nugettest.org + "3751cb513b93ee67ec9f18a1f2aec1eac87af9bc", // *.nuget.org (old) + "03984834f27d5c94f46b3bb190e5a8099787268a" // *.nuget.org (new) + }; + } + else + { + pieces = unparsedValued + .Split(',') + .Select(p => p.Trim()) + .Where(p => p.Length > 0) + .ToList(); + } + + _trustedHttpsCertificates = pieces; + } + + return _trustedHttpsCertificates; + } + } } } diff --git a/tests/NuGetGallery.FunctionalTests.Core/Helpers/ClientSdkHelper.cs b/tests/NuGetGallery.FunctionalTests.Core/Helpers/ClientSdkHelper.cs index daa50cec6d..f5e85a7198 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/Helpers/ClientSdkHelper.cs +++ b/tests/NuGetGallery.FunctionalTests.Core/Helpers/ClientSdkHelper.cs @@ -63,57 +63,64 @@ public bool CheckIfPackageExistsInSource(string packageId, string sourceUrl) /// public bool CheckIfPackageVersionExistsInSource(string packageId, string version, string sourceUrl) { - var found = false; var repo = PackageRepositoryFactory.Default.CreateRepository(sourceUrl); - SemanticVersion semVersion; - var success = SemanticVersion.TryParse(version, out semVersion); - const int interval = 30; - const int maxAttempts = 15; + SemanticVersion semVersion = SemanticVersion.Parse(version); - if (success) - { - try + return VerifyWithRetry( + $"Verifying that package {packageId} {version} exists on source {sourceUrl}", + () => { - WriteLine("Starting package verification checks ({0} attempts, interval {1} seconds).", maxAttempts, interval); - // Wait for the search service to kick in, so that the package can be found via FindPackage(packageId, SemanticVersion) - Thread.Sleep(5000); + var package = repo.FindPackage(packageId, semVersion); + + return package != null; + }); + } + + private bool VerifyWithRetry(string actionPhrase, Func action) + { + bool success = false; + const int intervalSec = 30; + const int maxAttempts = 30; + + try + { + WriteLine($"{actionPhrase} ({maxAttempts} attempts, interval {intervalSec} seconds)."); - for (var i = 0; ((i < maxAttempts) && (!found)); i++) + for (var i = 0; i < maxAttempts && !success; i++) + { + if (i != 0) { - WriteLine("[verification attempt {0}]: Waiting {1} seconds before next check...", i, interval); - - if (i != 0) - { - Thread.Sleep(interval * 1000); - } - - WriteLine("[verification attempt {0}]: Checking if package {1} with version {2} exists in source {3}... ", i, packageId, version, sourceUrl); - IPackage package = repo.FindPackage(packageId, semVersion); - found = (package != null); - if (found) - { - WriteLine("Found!"); - } - else - { - WriteLine("NOT found!"); - } + WriteLine($"[verification attempt {i}]: Waiting {intervalSec} seconds before next check..."); + Thread.Sleep(intervalSec * 1000); } - } - catch (Exception ex) - { - WriteLine("Exception thrown while checking the existence of package {0} with version {1}:\r\n {2}", packageId, version, ex.Message); + + WriteLine($"[verification attempt {i}]: Executing... "); + success = action(); + WriteLine(success ? "Successful!" : "NOT successful!"); } } + catch (Exception ex) + { + WriteLine($"{actionPhrase} threw an exception.{Environment.NewLine}{ex}"); + } - return found; + return success; } /// - /// Creates a package with the specified Id and Version and uploads it and checks if the upload has suceeded. - /// This will be used by test classes which tests scenarios on top of upload. + /// Creates a package with the specified Id and Version and uploads it and checks if the upload has succeeded. + /// Throws if the upload fails or cannot be verified in the source. /// public async Task UploadNewPackageAndVerify(string packageId, string version = "1.0.0", string minClientVersion = null, string title = null, string tags = null, string description = null, string licenseUrl = null, string dependencies = null) + { + await UploadNewPackage(packageId, version, minClientVersion, title, tags, description, licenseUrl, dependencies); + + VerifyPackageExistsInSource(packageId, version); + } + + public async Task UploadNewPackage(string packageId, string version = "1.0.0", string minClientVersion = null, + string title = null, string tags = null, string description = null, string licenseUrl = null, + string dependencies = null, string apiKey = null) { if (string.IsNullOrEmpty(packageId)) { @@ -125,21 +132,60 @@ public async Task UploadNewPackageAndVerify(string packageId, string version = " var packageCreationHelper = new PackageCreationHelper(TestOutputHelper); var packageFullPath = await packageCreationHelper.CreatePackage(packageId, version, minClientVersion, title, tags, description, licenseUrl, dependencies); + await UploadExistingPackage(packageFullPath); + + // Delete package from local disk once it gets uploaded + CleanCreatedPackage(packageFullPath); + } + + public async Task UploadExistingPackage(string packageFullPath, string apiKey = null) + { var commandlineHelper = new CommandlineHelper(TestOutputHelper); - var processResult = await commandlineHelper.UploadPackageAsync(packageFullPath, UrlHelper.V2FeedPushSourceUrl); + var processResult = await commandlineHelper.UploadPackageAsync(packageFullPath, UrlHelper.V2FeedPushSourceUrl, apiKey); - Assert.True(processResult.ExitCode == 0, "The package upload via Nuget.exe did not succeed properly. Check the logs to see the process error and output stream. Exit Code: " + processResult.ExitCode + ". Error message: \"" + processResult.StandardError + "\""); + Assert.True(processResult.ExitCode == 0, + "The package upload via Nuget.exe did not succeed properly. Check the logs to see the process error and output stream. Exit Code: " + + processResult.ExitCode + ". Error message: \"" + processResult.StandardError + "\""); + } - var packageExistsInSource = CheckIfPackageVersionExistsInSource(packageId, version, UrlHelper.V2FeedRootUrl); - var userMessage = string.Format("Package {0} with version {1} is not found in the site {2} after uploading.", packageId, version, UrlHelper.V2FeedRootUrl); - Assert.True(packageExistsInSource, userMessage); + /// + /// Unlists a package with the specified Id and Version and checks if the unlist has succeeded. + /// Throws if the unlist fails or cannot be verified in the source. + /// + public async Task UnlistPackageAndVerify(string packageId, string version = "1.0.0") + { + await UnlistPackage(packageId, version); - // Delete package from local disk so once it gets uploaded - if (File.Exists(packageFullPath)) + VerifyPackageExistsInSource(packageId, version); + } + + public async Task UnlistPackage(string packageId, string version = "1.0.0", string apiKey = null) + { + if (string.IsNullOrEmpty(packageId)) { - File.Delete(packageFullPath); - Directory.Delete(Path.GetFullPath(Path.GetDirectoryName(packageFullPath)), true); + throw new ArgumentException($"{nameof(packageId)} cannot be null or empty!"); } + + WriteLine("Unlisting package '{0}', version '{1}'", packageId, version); + + var commandlineHelper = new CommandlineHelper(TestOutputHelper); + var processResult = await commandlineHelper.DeletePackageAsync(packageId, version, UrlHelper.V2FeedPushSourceUrl, apiKey); + + Assert.True(processResult.ExitCode == 0, + "The package unlist via Nuget.exe did not succeed properly. Check the logs to see the process error and output stream. Exit Code: " + + processResult.ExitCode + ". Error message: \"" + processResult.StandardError + "\""); + } + + /// + /// Throws if the specified package cannot be found in the source. + /// + /// Id of the package. + /// Version of the package. + public void VerifyPackageExistsInSource(string packageId, string version = "1.0.0") + { + var packageExistsInSource = CheckIfPackageVersionExistsInSource(packageId, version, UrlHelper.V2FeedRootUrl); + Assert.True(packageExistsInSource, + $"Package {packageId} with version {version} is not found on the site {UrlHelper.V2FeedRootUrl}."); } /// @@ -155,17 +201,31 @@ public static string GetLatestStableVersion(string packageId) return version.ToString(); } - /// - /// Returns the count of versions available for the given package - /// - public int GetVersionCount(string packageId, bool allowPreRelease = true) + public void VerifyVersionCount(string packageId, int expectedVersionCount, bool allowPreRelease = true) { var repo = PackageRepositoryFactory.Default.CreateRepository(SourceUrl); - var packages = repo.FindPackagesById(packageId).ToList(); - if (!allowPreRelease) - packages = packages.Where(item => item.IsReleaseVersion()).ToList(); - return packages.Count; + + // To verify the count of package versions, the FindPackagesById() V2 OData endpoint is used. When the + // gallery handles this request, it delegates to the search service. Since the search service can lag being + // the gallery database (due to the time it takes for packages to make it through the V3 pipeline and into + // an active Lucene index), we retry the request for a while. + Assert.True(VerifyWithRetry( + $"Verifying count of {packageId} versions is {expectedVersionCount}", + () => + { + var packages = repo.FindPackagesById(packageId).ToList(); + if (!allowPreRelease) + { + packages = packages.Where(item => item.IsReleaseVersion()).ToList(); + } + var actualVersionCount = packages.Count; + + var versionsDisplay = string.Join(", ", packages.Select(p => p.Version)); + WriteLine($"{actualVersionCount} versions of {packageId} found: {versionsDisplay}"); + return actualVersionCount == expectedVersionCount; + })); } + /// /// Returns the download count of the given package as a formatted string as it would appear in the gallery UI. /// @@ -242,10 +302,9 @@ public bool IsPackageVersionUnListed(string packageId, string version) /// /// Clears the local package folder. /// - public void ClearLocalPackageFolder(string packageId) + public void ClearLocalPackageFolder(string packageId, string version = "1.0.0") { - string packageVersion = GetLatestStableVersion(packageId); - string expectedDownloadedNupkgFileName = packageId + "." + packageVersion; + string expectedDownloadedNupkgFileName = packageId + "." + version; string pathToNupkgFolder = Path.Combine(Environment.CurrentDirectory, expectedDownloadedNupkgFileName); WriteLine("Path to the downloaded Nupkg file for clearing local package folder is: " + pathToNupkgFolder); if (Directory.Exists(pathToNupkgFolder)) @@ -292,7 +351,7 @@ public bool CheckIfPackageVersionInstalled(string packageId, string packageVersi public void DownloadPackageAndVerify(string packageId, string version = "1.0.0") { ClearMachineCache(); - ClearLocalPackageFolder(packageId); + ClearLocalPackageFolder(packageId, version); var packageRepository = PackageRepositoryFactory.Default.CreateRepository(UrlHelper.V2FeedRootUrl); var packageManager = new PackageManager(packageRepository, Environment.CurrentDirectory); @@ -302,5 +361,14 @@ public void DownloadPackageAndVerify(string packageId, string version = "1.0.0") Assert.True(CheckIfPackageVersionInstalled(packageId, version), "Package install failed. Either the file is not present on disk or it is corrupted. Check logs for details"); } + + public void CleanCreatedPackage(string packageFullPath) + { + if (!string.IsNullOrEmpty(packageFullPath) && File.Exists(packageFullPath)) + { + File.Delete(packageFullPath); + Directory.Delete(Path.GetFullPath(Path.GetDirectoryName(packageFullPath)), true); + } + } } } \ No newline at end of file diff --git a/tests/NuGetGallery.FunctionalTests.Core/Helpers/CommandlineHelper.cs b/tests/NuGetGallery.FunctionalTests.Core/Helpers/CommandlineHelper.cs index c4008c26fe..2fd94ed769 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/Helpers/CommandlineHelper.cs +++ b/tests/NuGetGallery.FunctionalTests.Core/Helpers/CommandlineHelper.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.Diagnostics; using System.IO; using System.Threading.Tasks; @@ -15,17 +16,20 @@ namespace NuGetGallery.FunctionalTests public class CommandlineHelper : HelperBase { - internal static string AnalyzeCommandString = " analyze "; - internal static string SpecCommandString = " spec -f "; - internal static string PackCommandString = " pack "; - internal static string UpdateCommandString = " update "; - internal static string InstallCommandString = " install "; - internal static string PushCommandString = " push "; - internal static string OutputDirectorySwitchString = " -OutputDirectory "; - internal static string PreReleaseSwitchString = " -Prerelease "; - internal static string SourceSwitchString = " -Source "; - internal static string ApiKeySwitchString = " -ApiKey "; - internal static string ExcludeVersionSwitchString = " -ExcludeVersion "; + internal static string AnalyzeCommandString = "analyze"; + internal static string SpecCommandString = "spec -force"; + internal static string PackCommandString = "pack"; + internal static string UpdateCommandString = "update"; + internal static string InstallCommandString = "install"; + internal static string DeleteCommandString = "delete"; + internal static string PushCommandString = "push"; + internal static string OutputDirectorySwitchString = "-OutputDirectory"; + internal static string PreReleaseSwitchString = "-Prerelease"; + internal static string SourceSwitchString = "-Source"; + internal static string ApiKeySwitchString = "-ApiKey"; + internal static string SelfSwitch = "-self"; + internal static string NonInteractiveSwitchString = "-noninteractive"; + internal static string ExcludeVersionSwitchString = "-ExcludeVersion"; internal static string NugetExePath = @"NuGet.exe"; internal static string SampleDependency = "SampleDependency"; internal static string SampleDependencyVersion = "1.0"; @@ -41,16 +45,54 @@ public CommandlineHelper(ITestOutputHelper testOutputHelper) /// /// /// - public async Task UploadPackageAsync(string packageFullPath, string sourceName) + public async Task UploadPackageAsync(string packageFullPath, string sourceName, string apiKey = null) { - WriteLine("Uploading package " + packageFullPath + " to " + sourceName); + string message = $"Uploading package {packageFullPath} to {sourceName}."; - var arguments = string.Join(string.Empty, PushCommandString, @"""" + packageFullPath + @"""", SourceSwitchString, sourceName, ApiKeySwitchString, EnvironmentSettings.TestAccountApiKey); - WriteLine("nuget.exe " + arguments); + if (apiKey == null) + { + apiKey = EnvironmentSettings.TestAccountApiKey; + + message += " Using full access API key"; + } + + WriteLine(message); + + var arguments = new List + { + PushCommandString, packageFullPath, SourceSwitchString, sourceName, ApiKeySwitchString, apiKey + }; return await InvokeNugetProcess(arguments); } + /// + /// Delete the specified package using Nuget.exe + /// + /// package to be deleted + /// version of package to be deleted + /// source url + /// + public async Task DeletePackageAsync(string packageId, string version, string sourceName, string apiKey = null) + { + string message = $"Deleting package {packageId} with version {version} from {sourceName}."; + + if (apiKey == null) + { + apiKey = EnvironmentSettings.TestAccountApiKey; + + message += " Using full access API key"; + } + + WriteLine(message); + + var arguments = new List + { + DeleteCommandString, packageId, version, SourceSwitchString, sourceName, ApiKeySwitchString, apiKey + }; + return await InvokeNugetProcess(arguments); + } + /// /// Install the specified package using Nuget.exe /// @@ -59,7 +101,12 @@ public async Task UploadPackageAsync(string packageFullPath, stri /// public async Task InstallPackageAsync(string packageId, string sourceName) { - var arguments = string.Join(string.Empty, InstallCommandString, packageId, SourceSwitchString, sourceName); + WriteLine("Installing package " + packageId + " from " + sourceName); + + var arguments = new List + { + InstallCommandString, packageId, SourceSwitchString, sourceName + }; return await InvokeNugetProcess(arguments); } @@ -72,17 +119,44 @@ public async Task InstallPackageAsync(string packageId, string so /// public async Task InstallPackageAsync(string packageId, string sourceName, string outputDirectory) { - var arguments = string.Join(string.Empty, InstallCommandString, packageId, SourceSwitchString, sourceName, OutputDirectorySwitchString, outputDirectory); + WriteLine("Installing package " + packageId + " from " + sourceName + " to " + outputDirectory); + + var arguments = new List + { + InstallCommandString, packageId, SourceSwitchString, sourceName, OutputDirectorySwitchString, + outputDirectory + }; return await InvokeNugetProcess(arguments); } + public async Task SpecPackageAsync(string packageName, string packageDir) + { + var arguments = new List + { + SpecCommandString, packageName + }; + return await InvokeNugetProcess(arguments, packageDir); + } + + public async Task PackPackageAsync(string nuspecFileFullPath, string nuspecDir) + { + var arguments = new List + { + PackCommandString, nuspecFileFullPath, OutputDirectorySwitchString, nuspecDir + }; + return await InvokeNugetProcess(arguments, Path.GetFullPath(Path.GetDirectoryName(nuspecFileFullPath))); + } + /// /// Self update on nuget.exe /// /// public async Task UpdateNugetExeAsync() { - var arguments = string.Join(string.Empty, UpdateCommandString, "-self"); + var arguments = new List + { + UpdateCommandString, SelfSwitch + }; return await InvokeNugetProcess(arguments); } @@ -94,16 +168,25 @@ public async Task UpdateNugetExeAsync() /// working dir if any to be used /// Timeout in seconds (default = 6min). /// - public async Task InvokeNugetProcess(string arguments, string workingDir = null, int timeout = 360) + public async Task InvokeNugetProcess(List arguments, string workingDir = null, int timeout = 360) { var nugetProcess = new Process(); var pathToNugetExe = Path.Combine(Environment.CurrentDirectory, NugetExePath); - WriteLine("The NuGet.exe command to be executed is: " + pathToNugetExe + " " + arguments); + foreach (var trustedCertificate in EnvironmentSettings.TrustedHttpsCertificates) + { + arguments.AddRange(new[] { "-TrustedHttpsCertificate", trustedCertificate }); + } + + arguments.Add(NonInteractiveSwitchString); + + var argumentsString = string.Join(" ", arguments); + + WriteLine("The NuGet.exe command to be executed is: " + pathToNugetExe + " " + argumentsString); // During the actual test run, a script will copy the latest NuGet.exe and overwrite the existing one ProcessStartInfo nugetProcessStartInfo = new ProcessStartInfo(pathToNugetExe); - nugetProcessStartInfo.Arguments = arguments; + nugetProcessStartInfo.Arguments = argumentsString; nugetProcessStartInfo.RedirectStandardError = true; nugetProcessStartInfo.RedirectStandardOutput = true; nugetProcessStartInfo.RedirectStandardInput = true; diff --git a/tests/NuGetGallery.FunctionalTests.Core/Helpers/MetricsServiceHelper.cs b/tests/NuGetGallery.FunctionalTests.Core/Helpers/MetricsServiceHelper.cs deleted file mode 100644 index eeb00c180c..0000000000 --- a/tests/NuGetGallery.FunctionalTests.Core/Helpers/MetricsServiceHelper.cs +++ /dev/null @@ -1,89 +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.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using Xunit.Abstractions; - -namespace NuGetGallery.FunctionalTests -{ - public class MetricsServiceHelper - : HelperBase - { - public const string IdKey = "id"; - public const string VersionKey = "version"; - public const string IpAddressKey = "ipAddress"; - public const string UserAgentKey = "userAgent"; - public const string OperationKey = "operation"; - public const string DependentPackageKey = "dependentPackage"; - public const string ProjectGuidsKey = "projectGuids"; - public const string HttpPost = "POST"; - public const string MetricsDownloadEventMethod = "/DownloadEvent"; - public const string ContentTypeJson = "application/json"; - public const string MetricsServiceUri = "http://api-metrics.int.nugettest.org"; - - public MetricsServiceHelper() - : this(ConsoleTestOutputHelper.New) - { - } - - public MetricsServiceHelper(ITestOutputHelper testOutputHelper) - : base(testOutputHelper) - { - } - - public async Task TryHitMetricsEndPoint(string id, string version, string ipAddress, string userAgent, string operation, string dependentPackage, string projectGuids) - { - var jObject = GetJObject(id, version, ipAddress, userAgent, operation, dependentPackage, projectGuids); - var result = await TryHitMetricsEndPoint(jObject); - return result; - } - - public async Task TryHitMetricsEndPoint(JObject jObject) - { - try - { - using (var httpClient = new HttpClient()) - { - var requestUri = new Uri(MetricsServiceUri + MetricsDownloadEventMethod); - var content = new StringContent(jObject.ToString(), Encoding.UTF8, ContentTypeJson); - var response = await httpClient.PostAsync(requestUri, content); - - //print the header - WriteLine("HTTP status code : {0}", response.StatusCode); - if (response.StatusCode == HttpStatusCode.Accepted) - { - return true; - } - else - { - return false; - } - } - } - catch (HttpRequestException hre) - { - WriteLine("Exception : {0}", hre.Message); - return false; - } - } - - private static JObject GetJObject(string id, string version, string ipAddress, string userAgent, string operation, string dependentPackage, string projectGuids) - { - var jObject = new JObject(); - jObject.Add(IdKey, id); - jObject.Add(VersionKey, version); - if (!string.IsNullOrEmpty(ipAddress)) jObject.Add(IpAddressKey, ipAddress); - if (!string.IsNullOrEmpty(userAgent)) jObject.Add(UserAgentKey, userAgent); - if (!string.IsNullOrEmpty(operation)) jObject.Add(OperationKey, operation); - if (!string.IsNullOrEmpty(dependentPackage)) jObject.Add(DependentPackageKey, dependentPackage); - if (!string.IsNullOrEmpty(projectGuids)) jObject.Add(ProjectGuidsKey, projectGuids); - - return jObject; - } - } -} diff --git a/tests/NuGetGallery.FunctionalTests.Core/Helpers/NuspecHelper.cs b/tests/NuGetGallery.FunctionalTests.Core/Helpers/NuspecHelper.cs index d61a17a0f5..939defb937 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/Helpers/NuspecHelper.cs +++ b/tests/NuGetGallery.FunctionalTests.Core/Helpers/NuspecHelper.cs @@ -37,17 +37,16 @@ public NuspecHelper(ITestOutputHelper testOutputHelper) /// public async Task CreateDefaultNuspecFile(string packageName, string version = "1.0.0", string minClientVersion = null, string title = null, string tags = null, string description = null, string licenseUrl = null, string dependencies = null) { - string packageDir = Path.Combine(Environment.CurrentDirectory, packageName); + string packageDir = Path.Combine(Environment.CurrentDirectory, packageName, version); if (Directory.Exists(packageDir)) { Directory.Delete(packageDir, true); } Directory.CreateDirectory(packageDir); - - var arguments = string.Join(string.Empty, CommandlineHelper.SpecCommandString, packageName); + var commandlineHelper = new CommandlineHelper(TestOutputHelper); - await commandlineHelper.InvokeNugetProcess(arguments, packageDir); + await commandlineHelper.SpecPackageAsync(packageName, packageDir); string filePath = Path.Combine(packageDir, packageName + ".nuspec"); RemoveSampleNuspecValues(filePath); @@ -56,11 +55,11 @@ public async Task CreateDefaultNuspecFile(string packageName, string ver // Apply the minClientVersion to the spec only if it's defined. if (minClientVersion != null) { - UpdateNuspecFile(filePath, "", String.Format("", minClientVersion)); + UpdateNuspecFile(filePath, "", $""); } if (title != null) { - UpdateNuspecFile(filePath, "", String.Format("{0}", title)); + UpdateNuspecFile(filePath, "", $"{title}"); } if (tags != null) { @@ -72,11 +71,11 @@ public async Task CreateDefaultNuspecFile(string packageName, string ver } if (licenseUrl != null) { - UpdateNuspecFile(filePath, "", String.Format("{0}", licenseUrl)); + UpdateNuspecFile(filePath, "", $"{licenseUrl}"); } if (dependencies != null) { - UpdateNuspecFile(filePath, "", String.Format("{0}", dependencies)); + UpdateNuspecFile(filePath, "", $"{dependencies}"); } return filePath; } diff --git a/tests/NuGetGallery.FunctionalTests.Core/Helpers/ODataHelper.cs b/tests/NuGetGallery.FunctionalTests.Core/Helpers/ODataHelper.cs index c8b1f4ba12..8babf385c0 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/Helpers/ODataHelper.cs +++ b/tests/NuGetGallery.FunctionalTests.Core/Helpers/ODataHelper.cs @@ -59,17 +59,61 @@ public async Task TryDownloadPackageFromFeed(string packageId, string ve } } - public async Task ContainsResponseText(string url, params string[] expectedTexts) + public async Task GetTimestampOfPackageFromResponse(string url, string propertyName, string packageId, string version = "1.0.0") { - var request = WebRequest.Create(url); - var response = await request.GetResponseAsync().ConfigureAwait(false); + WriteLine($"Getting '{propertyName}' timestamp of package '{packageId}' with version '{version}'."); + + var packageResponse = await GetPackageDataInResponse(url, packageId, version); + if (string.IsNullOrEmpty(packageResponse)) + { + return null; + } + + var timestampStartTag = ""; + var timestampEndTag = ""; + + var timestampTagIndex = packageResponse.IndexOf(timestampStartTag); + if (timestampTagIndex < 0) + { + WriteLine($"Package data does not contain '{propertyName}' timestamp!"); + return null; + } - string responseText; - using (var sr = new StreamReader(response.GetResponseStream())) + var timestampStartIndex = timestampTagIndex + timestampStartTag.Length; + var timestampLength = packageResponse.Substring(timestampStartIndex).IndexOf(timestampEndTag); + + var timestamp = + DateTime.Parse(packageResponse.Substring(timestampStartIndex, timestampLength)); + WriteLine($"'{propertyName}' timestamp of package '{packageId}' with version '{version}' is '{timestamp}'"); + return timestamp; + } + + public async Task GetPackageDataInResponse(string url, string packageId, string version = "1.0.0") + { + WriteLine($"Getting data for package '{packageId}' with version '{version}'."); + + var responseText = await GetResponseText(url); + + var packageString = @"" + UrlHelper.V2FeedRootUrl + @"Packages(Id='" + packageId + @"',Version='" + (string.IsNullOrEmpty(version) ? "" : version + "')"); + var endEntryTag = ""; + + var startingIndex = responseText.IndexOf(packageString); + + if (startingIndex < 0) { - responseText = await sr.ReadToEndAsync().ConfigureAwait(false); + WriteLine("Package not found in response text!"); + return null; } + var endingIndex = responseText.IndexOf(endEntryTag, startingIndex); + + return responseText.Substring(startingIndex, endingIndex - startingIndex); + } + + public async Task ContainsResponseText(string url, params string[] expectedTexts) + { + var responseText = await GetResponseText(url); + foreach (string s in expectedTexts) { if (!responseText.Contains(s)) @@ -83,14 +127,7 @@ public async Task ContainsResponseText(string url, params string[] expecte public async Task ContainsResponseTextIgnoreCase(string url, params string[] expectedTexts) { - var request = WebRequest.Create(url); - var response = await request.GetResponseAsync(); - - string responseText; - using (var sr = new StreamReader(response.GetResponseStream())) - { - responseText = (await sr.ReadToEndAsync()).ToLowerInvariant(); - } + var responseText = (await GetResponseText(url)).ToLowerInvariant(); foreach (string s in expectedTexts) { @@ -103,15 +140,36 @@ public async Task ContainsResponseTextIgnoreCase(string url, params string return true; } + private async Task GetResponseText(string url) + { + var request = WebRequest.Create(url); + using (var response = await request.GetResponseAsync()) + { + string responseText; + using (var sr = new StreamReader(response.GetResponseStream())) + { + responseText = await sr.ReadToEndAsync(); + } + + return responseText; + } + } + + public async Task SendRequest(string url) + { + var request = WebRequest.Create(url); + return await request.GetResponseAsync().ConfigureAwait(false); + } + public async Task DownloadPackageFromV2FeedWithOperation(string packageId, string version, string operation) { string filename = await DownloadPackageFromFeed(packageId, version, operation); - //check if the file exists. + // Check if the file exists. Assert.True(File.Exists(filename), Constants.PackageDownloadFailureMessage); var clientSdkHelper = new ClientSdkHelper(TestOutputHelper); string downloadedPackageId = clientSdkHelper.GetPackageIdFromNupkgFile(filename); - //Check that the downloaded Nupkg file is not corrupt and it indeed corresponds to the package which we were trying to download. + // Check that the downloaded Nupkg file is not corrupt and it indeed corresponds to the package which we were trying to download. Assert.True(downloadedPackageId.Equals(packageId), Constants.UnableToZipError); } diff --git a/tests/NuGetGallery.FunctionalTests.Core/Helpers/PackageCreationHelper.cs b/tests/NuGetGallery.FunctionalTests.Core/Helpers/PackageCreationHelper.cs index 3ccf73a132..e4e794d5e0 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/Helpers/PackageCreationHelper.cs +++ b/tests/NuGetGallery.FunctionalTests.Core/Helpers/PackageCreationHelper.cs @@ -176,10 +176,8 @@ private async Task CreatePackageInternal(string nuspecFileFullPath) AddContent(nuspecDir); AddLib(nuspecDir); - var arguments = string.Join(string.Empty, CommandlineHelper.PackCommandString, @"""" + nuspecFileFullPath + @"""", CommandlineHelper.OutputDirectorySwitchString, @"""" + nuspecDir + @""""); - var commandlineHelper = new CommandlineHelper(TestOutputHelper); - await commandlineHelper.InvokeNugetProcess(arguments, Path.GetFullPath(Path.GetDirectoryName(nuspecFileFullPath))); + await commandlineHelper.PackPackageAsync(nuspecFileFullPath, nuspecDir); string[] nupkgFiles = Directory.GetFiles(nuspecDir, "*.nupkg").ToArray(); return nupkgFiles.Length == 0 ? null : nupkgFiles[0]; @@ -190,10 +188,9 @@ private async Task CreatePackageWithTargetFrameworkInternal(string nuspe string nuspecDir = Path.GetDirectoryName(nuspecFileFullPath); AddContent(nuspecDir, frameworkVersion); AddLib(nuspecDir, frameworkVersion); - var arguments = string.Join(string.Empty, CommandlineHelper.PackCommandString, @"""" + nuspecFileFullPath + @"""", CommandlineHelper.OutputDirectorySwitchString, @"""" + nuspecDir + @""""); var commandlineHelper = new CommandlineHelper(TestOutputHelper); - await commandlineHelper.InvokeNugetProcess(arguments, Path.GetFullPath(Path.GetDirectoryName(nuspecFileFullPath))); + await commandlineHelper.PackPackageAsync(nuspecFileFullPath, nuspecDir); string[] nupkgFiles = Directory.GetFiles(nuspecDir, "*.nupkg").ToArray(); return nupkgFiles.Length == 0 ? null : nupkgFiles[0]; diff --git a/tests/NuGetGallery.FunctionalTests.Core/Helpers/UrlHelper.cs b/tests/NuGetGallery.FunctionalTests.Core/Helpers/UrlHelper.cs index d9c7000bcb..668dae0cc4 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/Helpers/UrlHelper.cs +++ b/tests/NuGetGallery.FunctionalTests.Core/Helpers/UrlHelper.cs @@ -12,7 +12,7 @@ public class UrlHelper { private const string _logonPageUrlSuffix = "/users/account/LogOn"; private const string _editUrlSuffix = "/packages/{0}/{1}/Edit"; - private const string _cancelUrlSuffix = "packages/cancel-upload"; + private const string _cancelUrlSuffix = "packages/manage/cancel-upload"; private const string _signInPageUrlSuffix = "/users/account/SignIn"; private const string _logOffPageUrlSuffix = "/users/account/LogOff?returnUrl=%2F"; private const string _logonPageUrlOnPackageUploadSuffix = "Users/Account/LogOn?ReturnUrl=%2fpackages%2fupload"; @@ -21,8 +21,8 @@ public class UrlHelper private const string _registrationPendingPageUrlSuffix = "account/Thanks"; private const string _statsPageUrlSuffix = "stats"; private const string _aggregateStatsPageUrlSuffix = "/stats/totals"; - private const string _uploadPageUrlSuffix = "/packages/Upload"; - private const string _verifyUploadPageUrlSuffix = "/packages/verify-upload"; + private const string _uploadPageUrlSuffix = "/packages/manage/Upload"; + private const string _verifyUploadPageUrlSuffix = "/packages/manage/verify-upload"; private const string _windows8CuratedFeedUrlSuffix = "curated-feeds/windows8-packages/"; private const string _webMatrixCuratedFeedUrlSuffix = "curated-feeds/webmatrix/"; private const string _dotnetCuratedFeedUrlSuffix = "curated-feeds/microsoftdotnet/"; @@ -40,6 +40,14 @@ public static string BaseUrl } } + public static string SearchServiceBaseUrl + { + get + { + return EnsureTrailingSlash(EnvironmentSettings.SearchServiceBaseUrl); + } + } + public static string V1FeedRootUrl { get diff --git a/tests/NuGetGallery.FunctionalTests.Core/NuGetGallery.FunctionalTests.Core.csproj b/tests/NuGetGallery.FunctionalTests.Core/NuGetGallery.FunctionalTests.Core.csproj index a6895e6b34..6a93865033 100644 --- a/tests/NuGetGallery.FunctionalTests.Core/NuGetGallery.FunctionalTests.Core.csproj +++ b/tests/NuGetGallery.FunctionalTests.Core/NuGetGallery.FunctionalTests.Core.csproj @@ -1,6 +1,6 @@  - + Debug @@ -12,7 +12,8 @@ NuGetGallery.FunctionalTests.Core v4.5 512 - 2bf4eb2c + + true @@ -51,13 +52,17 @@ False ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll - - False - ..\..\packages\xunit.assert.2.1.0-beta1-build2945\lib\portable-net45+aspnetcore50+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + + ..\..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll + True - - False - ..\..\packages\xunit.extensibility.core.2.1.0-beta1-build2945\lib\portable-net45+aspnetcore50+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + + ..\..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll + True + + + ..\..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll + True @@ -73,7 +78,6 @@ - @@ -91,7 +95,7 @@ This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - +