diff --git a/NuGetGallery.sln b/NuGetGallery.sln index cbf88d09bd..d9cb388b81 100644 --- a/NuGetGallery.sln +++ b/NuGetGallery.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 -VisualStudioVersion = 12.0.20827.3 +VisualStudioVersion = 12.0.21005.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{96E4AFF8-D3A1-4102-ADCF-05F186F916A9}" ProjectSection(SolutionItems) = preProject @@ -36,10 +36,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetGallery.FunctionalTest EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetGallery.FunctionalTests.Helpers", "tests\NuGetGallery.FunctionalTests.Helpers\NuGetGallery.FunctionalTests.Helpers.csproj", "{8FB56455-C688-44AE-95F1-48FFCB199BFE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetGallery.Monitoring", "src\NuGetGallery.Monitoring\NuGetGallery.Monitoring.csproj", "{DFF0089E-4918-4A12-992B-B9DD2C070B0F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetGallery.Monitoring.Azure", "src\NuGetGallery.Monitoring.Azure\NuGetGallery.Monitoring.Azure.csproj", "{6569F5ED-1DB6-433C-8C7F-E96C7B8E54BB}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -136,42 +132,6 @@ Global {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Release|x64.Build.0 = Release|Any CPU {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Release|x86.ActiveCfg = Release|Any CPU {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Release|x86.Build.0 = Release|Any CPU - {DFF0089E-4918-4A12-992B-B9DD2C070B0F}.Debug|x64.ActiveCfg = Debug|Any CPU - {DFF0089E-4918-4A12-992B-B9DD2C070B0F}.Debug|x64.Build.0 = Debug|Any CPU - {DFF0089E-4918-4A12-992B-B9DD2C070B0F}.Debug|x86.ActiveCfg = Debug|Any CPU - {DFF0089E-4918-4A12-992B-B9DD2C070B0F}.Debug|x86.Build.0 = Debug|Any CPU - {DFF0089E-4918-4A12-992B-B9DD2C070B0F}.Release|x64.ActiveCfg = Release|Any CPU - {DFF0089E-4918-4A12-992B-B9DD2C070B0F}.Release|x64.Build.0 = Release|Any CPU - {DFF0089E-4918-4A12-992B-B9DD2C070B0F}.Release|x86.ActiveCfg = Release|Any CPU - {DFF0089E-4918-4A12-992B-B9DD2C070B0F}.Release|x86.Build.0 = Release|Any CPU - {6569F5ED-1DB6-433C-8C7F-E96C7B8E54BB}.Debug|x64.ActiveCfg = Debug|Any CPU - {6569F5ED-1DB6-433C-8C7F-E96C7B8E54BB}.Debug|x64.Build.0 = Debug|Any CPU - {6569F5ED-1DB6-433C-8C7F-E96C7B8E54BB}.Debug|x86.ActiveCfg = Debug|Any CPU - {6569F5ED-1DB6-433C-8C7F-E96C7B8E54BB}.Debug|x86.Build.0 = Debug|Any CPU - {6569F5ED-1DB6-433C-8C7F-E96C7B8E54BB}.Release|x64.ActiveCfg = Release|Any CPU - {6569F5ED-1DB6-433C-8C7F-E96C7B8E54BB}.Release|x64.Build.0 = Release|Any CPU - {6569F5ED-1DB6-433C-8C7F-E96C7B8E54BB}.Release|x86.ActiveCfg = Release|Any CPU - {6569F5ED-1DB6-433C-8C7F-E96C7B8E54BB}.Release|x86.Build.0 = Release|Any CPU - {0A6B1A52-4D26-4946-9DDD-416D01A1ADBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0A6B1A52-4D26-4946-9DDD-416D01A1ADBF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0A6B1A52-4D26-4946-9DDD-416D01A1ADBF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {0A6B1A52-4D26-4946-9DDD-416D01A1ADBF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {0A6B1A52-4D26-4946-9DDD-416D01A1ADBF}.Debug|x86.ActiveCfg = Debug|Any CPU - {0A6B1A52-4D26-4946-9DDD-416D01A1ADBF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0A6B1A52-4D26-4946-9DDD-416D01A1ADBF}.Release|Any CPU.Build.0 = Release|Any CPU - {0A6B1A52-4D26-4946-9DDD-416D01A1ADBF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {0A6B1A52-4D26-4946-9DDD-416D01A1ADBF}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {0A6B1A52-4D26-4946-9DDD-416D01A1ADBF}.Release|x86.ActiveCfg = Release|Any CPU - {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Debug|x86.ActiveCfg = Debug|Any CPU - {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Release|Any CPU.Build.0 = Release|Any CPU - {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {8FB56455-C688-44AE-95F1-48FFCB199BFE}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -184,7 +144,5 @@ Global {4405C24C-7F57-4826-831F-D5D7E139F02E} = {B9B19787-DCC0-489E-9173-36A32C6B6848} {DBECF66B-8F2F-4B32-9143-E243BAFF12DF} = {2ECA1159-9B9D-4D65-95AF-F14337FD3DA6} {F240D1BC-BBFB-4F22-9DF8-3FDE36BFD665} = {2ECA1159-9B9D-4D65-95AF-F14337FD3DA6} - {DFF0089E-4918-4A12-992B-B9DD2C070B0F} = {2ECA1159-9B9D-4D65-95AF-F14337FD3DA6} - {6569F5ED-1DB6-433C-8C7F-E96C7B8E54BB} = {2ECA1159-9B9D-4D65-95AF-F14337FD3DA6} EndGlobalSection EndGlobal diff --git a/build/Enable-LocalTestMe.ps1 b/build/Enable-LocalTestMe.ps1 index f99598adda..92f890b37b 100644 --- a/build/Enable-LocalTestMe.ps1 +++ b/build/Enable-LocalTestMe.ps1 @@ -1,31 +1,90 @@ -param([switch]$Force, [string]$Subdomain="nuget") +param([string]$Subdomain="nuget", [string]$SiteName = "NuGet Gallery", [string]$SitePhysicalPath, [string]$MakeCertPath, [string]$AppCmdPath) if(!(([Security.Principal.WindowsPrincipal]([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]"Administrator"))) { throw "This script must be run as an admin." } -$WebSite = Resolve-Path (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\Website") +if(!$SitePhysicalPath) { + $ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path; + $SitePhysicalPath = Join-Path $ScriptRoot "..\src\NuGetGallery" +} +if(!(Test-Path $SitePhysicalPath)) { + throw "Could not find site at $SitePhysicalPath. Use -SitePhysicalPath argument to specify the path." +} +$SitePhysicalPath = Convert-Path $SitePhysicalPath -# Enable access to the necessary URLs -netsh http add urlacl url=http://nuget.localtest.me:80/ user=Everyone -netsh http add urlacl url=https://nuget.localtest.me:443/ user=Everyone +# Find Windows SDK +if(!$MakeCertPath) { + $SDKVersion = dir 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows' | + where { $_.PSChildName -match "v(?\d+\.\d+)" } | + foreach { New-Object System.Version $($matches["ver"]) } | + sort -desc | + select -first 1 + if(!$SDKVersion) { + throw "Could not find Windows SDK. Please install the Windows SDK before running this script, or use -MakeCertPath to specify the path to makecert.exe" + } + $SDKRegKey = (Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows\v$SDKVersion") + $WinSDKDir = $SDKRegKey.InstallationFolder + $xArch = "x86" + if($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { + $xArch = "x64" + } + $MakeCertPath = Join-Path $WinSDKDir "bin\$xArch\makecert.exe" +} -$IISExpressDir = "$env:ProgramFiles\IIS Express" -if(!(Test-Path $IISExpressDir)) { - throw "Can't find IIS Express in $IISExpressDir" +if(!(Test-Path $MakeCertPath)) { + throw "Could not find makecert.exe in $MakeCertPath!" } -$AppCmd = "$IISExpressDir\appcmd.exe" -$sites = @(&$AppCmd list site "NuGet Gallery ($Subdomain.localtest.me)") -if($sites.Length -gt 0) { - if($Force) { - &$AppCmd delete site "NuGet Gallery ($Subdomain.localtest.me)" +# Find IIS Express +if(!$AppCmdPath) { + $IISXVersion = dir 'HKLM:\Software\Microsoft\IISExpress' | + foreach { New-Object System.Version ($_.PSChildName) } | + sort -desc | + select -first 1 + if(!$IISXVersion) { + throw "Could not find IIS Express. Please install IIS Express before running this script, or use -AppCmdPath to specify the path to appcmd.exe for your IIS environment" + } + $IISRegKey = (Get-ItemProperty "HKLM:\Software\Microsoft\IISExpress\$IISXVersion") + $IISExpressDir = $IISRegKey.InstallPath + if(!(Test-Path $IISExpressDir)) { + throw "Can't find IIS Express in $IISExpressDir. Please install IIS Express" + } + $AppCmdPath = "$IISExpressDir\appcmd.exe" +} + +if(!(Test-Path $AppCmdPath)) { + throw "Could not find appcmd.exe in $AppCmdPath!" +} + +function Invoke-Netsh() { + $argStr = $([String]::Join(" ", $args)) + Write-Verbose "netsh $argStr" + $result = netsh @args + $parsed = [Regex]::Match($result, ".*Error: (\d+).*") + if($parsed.Success) { + $err = $parsed.Groups[1].Value + if($err -ne "183") { + throw $result + } } else { - throw "You already have a site named `"NuGet Gallery ($Subdomain.localtest.me)`". Remove it manually or use -Force to have this command auto-remove it" + Write-Host $result } } -&$AppCmd add site /name:"NuGet Gallery ($Subdomain.localtest.me)" /bindings:"http://$Subdomain.localtest.me:80,https://$Subdomain.localtest.me:443" /physicalPath:$WebSite +# Enable access to the necessary URLs +Invoke-Netsh http add urlacl "url=http://$Subdomain.localtest.me:80/" user=Everyone +Invoke-Netsh http add urlacl "url=https://$Subdomain.localtest.me:443/" user=Everyone + + +$SiteFullName = "$SiteName ($Subdomain.localtest.me)" +$sites = @(&$AppCmdPath list site $SiteFullName) +if($sites.Length -gt 0) { + Write-Warning "Site '$SiteFullName' already exists. Deleting and recreating." + &$AppCmdPath delete site "$SiteFullName" +} + +&$AppCmdPath add site /name:"$SiteFullName" /bindings:"http://$Subdomain.localtest.me:80,https://$Subdomain.localtest.me:443" /physicalPath:$SitePhysicalPath # Check for a cert $cert = @(dir -l "Cert:\CurrentUser\Root" | where {$_.Subject -eq "CN=$Subdomain.localtest.me"}) @@ -36,7 +95,7 @@ if($cert.Length -eq 0) { if($cert.Length -eq 0) { Write-Host "Generating a Self-Signed SSL Certificate for $Subdomain.localtest.me" # Generate one - & makecert -r -pe -n "CN=$Subdomain.localtest.me" -b `"$([DateTime]::Now.ToString("MM/dd/yyy"))`" -e `"$([DateTime]::Now.AddYears(10).ToString("MM/dd/yyy"))`" -eku 1.3.6.1.5.5.7.3.1 -ss root -sr localMachine -sky exchange -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12 + & $MakeCertPath -r -pe -n "CN=$Subdomain.localtest.me" -b `"$([DateTime]::Now.ToString("MM/dd/yyy"))`" -e `"$([DateTime]::Now.AddYears(10).ToString("MM/dd/yyy"))`" -eku 1.3.6.1.5.5.7.3.1 -ss root -sr localMachine -sky exchange -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12 $cert = @(dir -l "Cert:\LocalMachine\Root" | where {$_.Subject -eq "CN=$Subdomain.localtest.me"}) } @@ -47,7 +106,6 @@ if($cert.Length -eq 0) { Write-Host "Using SSL Certificate: $($cert.Thumbprint)" # Set the Certificate -netsh http add sslcert hostnameport="$Subdomain.localtest.me:443" certhash="$($cert.Thumbprint)" certstorename=Root appid="{$([Guid]::NewGuid().ToString())}" +Invoke-Netsh http add sslcert hostnameport="$Subdomain.localtest.me:443" certhash="$($cert.Thumbprint)" certstorename=Root appid="{$([Guid]::NewGuid().ToString())}" -Write-Host "Ready! All you have to do now is go to your Website project properties and set 'http://$Subdomain.localtest.me' as your Project URL" -Write-Host "To use SSL, set the IISExpressSSLPort MSBuild property in your Website.csproj.user to 443" \ No newline at end of file +Write-Host "Ready! All you have to do now is go to your website project properties and set 'http://$Subdomain.localtest.me' as your Project URL!" \ No newline at end of file diff --git a/src/NuGetGallery.Core/Entities/EntitiesContext.cs b/src/NuGetGallery.Core/Entities/EntitiesContext.cs index 60abfab867..1978ceebc1 100644 --- a/src/NuGetGallery.Core/Entities/EntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/EntitiesContext.cs @@ -30,6 +30,7 @@ public EntitiesContext() public IDbSet CuratedFeeds { get; set; } public IDbSet CuratedPackages { get; set; } public IDbSet PackageRegistrations { get; set; } + public IDbSet Credentials { get; set; } public IDbSet Users { get; set; } IDbSet IEntitiesContext.Set() diff --git a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs index 3a18c9b9c5..065f24b924 100644 --- a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs @@ -7,6 +7,7 @@ public interface IEntitiesContext IDbSet CuratedFeeds { get; set; } IDbSet CuratedPackages { get; set; } IDbSet PackageRegistrations { get; set; } + IDbSet Credentials { get; set; } IDbSet Users { get; set; } int SaveChanges(); [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/User.cs b/src/NuGetGallery.Core/Entities/User.cs index d57613fbb9..0dce5df133 100644 --- a/src/NuGetGallery.Core/Entities/User.cs +++ b/src/NuGetGallery.Core/Entities/User.cs @@ -13,6 +13,7 @@ public User() : this(null) public User(string username) { Credentials = new List(); + Roles = new List(); Username = username; } diff --git a/src/NuGetGallery/ApiController.generated.cs b/src/NuGetGallery/ApiController.generated.cs index e1eff9b642..df9ced635b 100644 --- a/src/NuGetGallery/ApiController.generated.cs +++ b/src/NuGetGallery/ApiController.generated.cs @@ -89,25 +89,22 @@ public class ViewNames { public class T4MVC_ApiController: NuGetGallery.ApiController { public T4MVC_ApiController() : base(Dummy.Instance) { } - public override System.Web.Mvc.ActionResult VerifyPackageKey(string apiKey, string id, string version) { + public override System.Web.Mvc.ActionResult VerifyPackageKey(string id, string version) { var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.VerifyPackageKey); - callInfo.RouteValueDictionary.Add("apiKey", apiKey); callInfo.RouteValueDictionary.Add("id", id); callInfo.RouteValueDictionary.Add("version", version); return callInfo; } - public override System.Web.Mvc.ActionResult DeletePackage(string apiKey, string id, string version) { + public override System.Web.Mvc.ActionResult DeletePackage(string id, string version) { var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.DeletePackage); - callInfo.RouteValueDictionary.Add("apiKey", apiKey); callInfo.RouteValueDictionary.Add("id", id); callInfo.RouteValueDictionary.Add("version", version); return callInfo; } - public override System.Web.Mvc.ActionResult PublishPackage(string apiKey, string id, string version) { + public override System.Web.Mvc.ActionResult PublishPackage(string id, string version) { var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.PublishPackage); - callInfo.RouteValueDictionary.Add("apiKey", apiKey); callInfo.RouteValueDictionary.Add("id", id); callInfo.RouteValueDictionary.Add("version", version); return callInfo; diff --git a/src/NuGetGallery/App_Start/AppActivator.cs b/src/NuGetGallery/App_Start/AppActivator.cs index a51b4f83f0..21d66280c8 100644 --- a/src/NuGetGallery/App_Start/AppActivator.cs +++ b/src/NuGetGallery/App_Start/AppActivator.cs @@ -1,5 +1,7 @@ using System; using System.Data.Entity; +using System.Security.Claims; +using System.Web.Helpers; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; @@ -30,6 +32,8 @@ public static class AppActivator public static void PreStart() { + AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier; + NinjectPreStart(); ElmahPreStart(); GlimpsePreStart(); @@ -101,7 +105,6 @@ private static void AppPostStart() GlobalFilters.Filters.Add(new ElmahHandleErrorAttribute()); GlobalFilters.Filters.Add(new ReadOnlyModeErrorFilter()); GlobalFilters.Filters.Add(new AntiForgeryErrorFilter()); - GlobalFilters.Filters.Add(new RequireRemoteHttpsAttribute() { OnlyWhenAuthenticated = true }); ValueProviderFactories.Factories.Add(new HttpHeaderValueProviderFactory()); } diff --git a/src/NuGetGallery/App_Start/AuthenticationModule.cs b/src/NuGetGallery/App_Start/AuthenticationModule.cs deleted file mode 100644 index 9def43507d..0000000000 --- a/src/NuGetGallery/App_Start/AuthenticationModule.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Security.Principal; -using System.Threading; -using System.Web; -using System.Web.Security; -using Microsoft.Web.Infrastructure.DynamicModuleHelper; -using NuGetGallery; - -[assembly: WebActivator.PreApplicationStartMethod(typeof(AuthenticationModule), "Start")] - -namespace NuGetGallery -{ - public class AuthenticationModule : IHttpModule - { - public void Init(HttpApplication context) - { - context.AuthenticateRequest += OnAuthenticateRequest; - } - - public void Dispose() - { - } - - public static void Start() - { - DynamicModuleUtility.RegisterModule(typeof(AuthenticationModule)); - } - - private void OnAuthenticateRequest(object sender, EventArgs e) - { - var context = HttpContext.Current; - var request = HttpContext.Current.Request; - if (request.IsAuthenticated) - { - HttpCookie authCookie = request.Cookies[FormsAuthentication.FormsCookieName]; - if (authCookie != null) - { - FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value); - var roles = authTicket.UserData.Split('|'); - var user = new GenericPrincipal(context.User.Identity, roles); - context.User = Thread.CurrentPrincipal = user; - } - } - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery/App_Start/ContainerBindings.cs b/src/NuGetGallery/App_Start/ContainerBindings.cs index 089cfd7085..1167f3f0a9 100644 --- a/src/NuGetGallery/App_Start/ContainerBindings.cs +++ b/src/NuGetGallery/App_Start/ContainerBindings.cs @@ -222,6 +222,12 @@ private void ConfigureForLocalFileSystem() Bind() .To() .InSingletonScope(); + + // Ninject is doing some weird things with constructor selection without these. + // Anyone requesting an IReportService or IStatisticsService should be prepared + // to receive null anyway. + Bind().ToConstant(NullReportService.Instance); + Bind().ToConstant(NullStatisticsService.Instance); } private void ConfigureForAzureStorage(ConfigurationService configuration) diff --git a/src/NuGetGallery/App_Start/OwinStartup.cs b/src/NuGetGallery/App_Start/OwinStartup.cs new file mode 100644 index 0000000000..be07b9a465 --- /dev/null +++ b/src/NuGetGallery/App_Start/OwinStartup.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Owin; +using Ninject; +using Microsoft.Owin; +using Microsoft.Owin.Logging; +using Microsoft.Owin.Extensions; +using Microsoft.Owin.Diagnostics; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; +using NuGetGallery.Authentication; +using NuGetGallery.Configuration; + +[assembly: OwinStartup(typeof(NuGetGallery.OwinStartup))] + +namespace NuGetGallery +{ + public class OwinStartup + { + // This method is auto-detected by the OWIN pipeline. DO NOT RENAME IT! + public static void Configuration(IAppBuilder app) + { + // Get config + var config = Container.Kernel.Get(); + var cookieSecurity = config.Current.RequireSSL ? CookieSecureOption.Always : CookieSecureOption.Never; + + // Configure logging + app.SetLoggerFactory(new DiagnosticsLoggerFactory()); + + if (config.Current.RequireSSL) + { + // Put a middleware at the top of the stack to force the user over to SSL + // if authenticated. + app.UseForceSslWhenAuthenticated(config.Current.SSLPort); + } + + app.UseCookieAuthentication(new CookieAuthenticationOptions() + { + AuthenticationType = AuthenticationTypes.Password, + AuthenticationMode = AuthenticationMode.Active, + CookieHttpOnly = true, + CookieSecure = cookieSecurity, + LoginPath = "/users/account/LogOn" + }); + app.UseApiKeyAuthentication(); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/ApiKeyAuthenticationExtensions.cs b/src/NuGetGallery/Authentication/ApiKeyAuthenticationExtensions.cs new file mode 100644 index 0000000000..02ebd41e41 --- /dev/null +++ b/src/NuGetGallery/Authentication/ApiKeyAuthenticationExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using NuGetGallery.Authentication; +using Microsoft.Owin.Extensions; + +namespace Owin +{ + public static class ApiKeyAuthenticationExtensions + { + public static IAppBuilder UseApiKeyAuthentication(this IAppBuilder self) + { + return UseApiKeyAuthentication(self, new ApiKeyAuthenticationOptions()); + } + + public static IAppBuilder UseApiKeyAuthentication(this IAppBuilder self, ApiKeyAuthenticationOptions options) + { + if (self == null) + { + throw new ArgumentNullException("self"); + } + + self.Use(typeof(ApiKeyAuthenticationMiddleware), self, options); + self.UseStageMarker(PipelineStage.Authenticate); + return self; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/ApiKeyAuthenticationHandler.cs b/src/NuGetGallery/Authentication/ApiKeyAuthenticationHandler.cs new file mode 100644 index 0000000000..bf35e9708d --- /dev/null +++ b/src/NuGetGallery/Authentication/ApiKeyAuthenticationHandler.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Owin.Logging; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Infrastructure; + +namespace NuGetGallery.Authentication +{ + public class ApiKeyAuthenticationHandler : AuthenticationHandler + { + protected ILogger Logger { get; set; } + protected AuthenticationService Auth { get; set; } + + internal ApiKeyAuthenticationHandler() { } + public ApiKeyAuthenticationHandler(ILogger logger, AuthenticationService auth) + { + Logger = logger; + Auth = auth; + } + + protected override async Task ApplyResponseChallengeAsync() + { + string message = GetChallengeMessage(); + + if (message != null) + { + Response.ReasonPhrase = message; + Response.Write(message); + } + else + { + await base.ApplyResponseChallengeAsync(); + } + } + + internal string GetChallengeMessage() + { + string message = null; + if (Response.StatusCode == 401 && (Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode) != null)) + { + var apiKey = Request.Headers[Options.ApiKeyHeaderName]; + message = Strings.ApiKeyRequired; + if (!String.IsNullOrEmpty(apiKey)) + { + // Had an API key, but it wasn't valid + message = Strings.ApiKeyNotAuthorized; + } + + } + return message; + } + + protected override Task AuthenticateCoreAsync() + { + var apiKey = Request.Headers[Options.ApiKeyHeaderName]; + if (!String.IsNullOrEmpty(apiKey)) + { + // Get the user + var authUser = Auth.Authenticate(CredentialBuilder.CreateV1ApiKey(apiKey)); + if (authUser != null) + { + // Set the current user + Context.Set(Constants.CurrentUserOwinEnvironmentKey, authUser); + + return Task.FromResult( + new AuthenticationTicket( + AuthenticationService.CreateIdentity( + authUser.User, + AuthenticationTypes.ApiKey, + new Claim(NuGetClaims.ApiKey, apiKey)), + new AuthenticationProperties())); + } + else + { + Logger.WriteWarning("No match for API Key!"); + } + } + else + { + Logger.WriteVerbose("No API Key Header found in request."); + } + return Task.FromResult(null); + } + } +} diff --git a/src/NuGetGallery/Authentication/ApiKeyAuthenticationMiddleware.cs b/src/NuGetGallery/Authentication/ApiKeyAuthenticationMiddleware.cs new file mode 100644 index 0000000000..681a72756b --- /dev/null +++ b/src/NuGetGallery/Authentication/ApiKeyAuthenticationMiddleware.cs @@ -0,0 +1,30 @@ +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 Owin; + +namespace NuGetGallery.Authentication +{ + public class ApiKeyAuthenticationMiddleware : AuthenticationMiddleware + { + private ILogger _logger; + + public ApiKeyAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, ApiKeyAuthenticationOptions options) + : base(next, options) + { + _logger = app.CreateLogger(); + } + + protected override AuthenticationHandler CreateHandler() + { + return new ApiKeyAuthenticationHandler( + _logger, + DependencyResolver.Current.GetService()); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/ApiKeyAuthenticationOptions.cs b/src/NuGetGallery/Authentication/ApiKeyAuthenticationOptions.cs new file mode 100644 index 0000000000..99803f9c27 --- /dev/null +++ b/src/NuGetGallery/Authentication/ApiKeyAuthenticationOptions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Microsoft.Owin.Security; + +namespace NuGetGallery.Authentication +{ + public class ApiKeyAuthenticationOptions : AuthenticationOptions + { + public string ApiKeyHeaderName { get; set; } + public string ApiKeyClaim { get; set; } + public string RootPath { get; set; } + + public ApiKeyAuthenticationOptions() : base(AuthenticationTypes.ApiKey) { + ApiKeyHeaderName = Constants.ApiKeyHeaderName; + ApiKeyClaim = NuGetClaims.ApiKey; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/AuthenticatedUser.cs b/src/NuGetGallery/Authentication/AuthenticatedUser.cs new file mode 100644 index 0000000000..4930d3e144 --- /dev/null +++ b/src/NuGetGallery/Authentication/AuthenticatedUser.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NuGetGallery.Authentication +{ + public class AuthenticatedUser + { + public User User { get; private set; } + public Credential CredentialUsed { get; private set; } + + public AuthenticatedUser(User user, Credential cred) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (cred == null) + { + throw new ArgumentNullException("cred"); + } + + User = user; + CredentialUsed = cred; + } + } +} diff --git a/src/NuGetGallery/Authentication/AuthenticationService.cs b/src/NuGetGallery/Authentication/AuthenticationService.cs new file mode 100644 index 0000000000..e2df02c538 --- /dev/null +++ b/src/NuGetGallery/Authentication/AuthenticationService.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Web; +using NuGetGallery.Diagnostics; +using System.Data.Entity; +using System.Globalization; +using Microsoft.Owin; +using System.Security.Claims; +using NuGetGallery.Configuration; +using Microsoft.Owin.Security; + +namespace NuGetGallery.Authentication +{ + public class AuthenticationService + { + public IEntitiesContext Entities { get; private set; } + public IAppConfiguration Config { get; private set; } + private IDiagnosticsSource Trace { get; set; } + + protected AuthenticationService() { } + + public AuthenticationService(IEntitiesContext entities, IAppConfiguration config, IDiagnosticsService diagnostics) + { + Entities = entities; + Config = config; + Trace = diagnostics.SafeGetSource("AuthenticationService"); + } + + public virtual AuthenticatedUser Authenticate(string userNameOrEmail, string password) + { + using (Trace.Activity("Authenticate:" + userNameOrEmail)) + { + var user = FindByUserNameOrEmail(userNameOrEmail); + + // Check if the user exists + if (user == null) + { + Trace.Information("No such user: " + userNameOrEmail); + return null; + } + + // Validate the password + Credential matched; + if (!ValidatePasswordCredential(user.Credentials, password, out matched)) + { + Trace.Information("Password validation failed: " + userNameOrEmail); + return null; + } + + var passwordCredentials = user + .Credentials + .Where(c => c.Type.StartsWith(CredentialTypes.Password.Prefix, StringComparison.OrdinalIgnoreCase)) + .ToList(); + if (passwordCredentials.Count > 1 || !passwordCredentials.Any(c => String.Equals(c.Type, CredentialTypes.Password.Pbkdf2, StringComparison.OrdinalIgnoreCase))) + { + MigrateCredentials(user, passwordCredentials, password); + } + + // Return the result + Trace.Verbose("Successfully authenticated '" + user.Username + "' with '" + matched.Type + "' credential"); + return new AuthenticatedUser(user, matched); + } + } + + public virtual AuthenticatedUser Authenticate(Credential credential) + { + if (credential.Type.StartsWith(CredentialTypes.Password.Prefix, StringComparison.OrdinalIgnoreCase)) + { + // Password credentials cannot be used this way. + throw new ArgumentException(Strings.PasswordCredentialsCannotBeUsedHere, "credential"); + } + + using (Trace.Activity("Authenticate Credential: " + credential.Type)) + { + var matched = FindMatchingCredential(credential); + + if (matched == null) + { + Trace.Information("No user matches credential of type: " + credential.Type); + return null; + } + + 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, string authenticationType) + { + // Create a claims identity for the session + ClaimsIdentity identity = CreateIdentity(user, authenticationType); + + // Issue the session token + owinContext.Authentication.SignIn(identity); + } + + public virtual AuthenticatedUser Register(string username, string password, string emailAddress) + { + var existingUser = Entities.Users + .FirstOrDefault(u => u.Username == username || u.EmailAddress == emailAddress); + if (existingUser != null) + { + if (String.Equals(existingUser.Username, username, StringComparison.OrdinalIgnoreCase)) + { + throw new EntityException(Strings.UsernameNotAvailable, username); + } + else + { + throw new EntityException(Strings.EmailAddressBeingUsed, emailAddress); + } + } + + var hashedPassword = CryptographyService.GenerateSaltedHash(password, Constants.PBKDF2HashAlgorithmId); + + var apiKey = Guid.NewGuid(); + var newUser = new User(username) + { + ApiKey = apiKey, + EmailAllowed = true, + UnconfirmedEmailAddress = emailAddress, + EmailConfirmationToken = CryptographyService.GenerateToken(), + HashedPassword = hashedPassword, + PasswordHashAlgorithm = Constants.PBKDF2HashAlgorithmId, + CreatedUtc = DateTime.UtcNow + }; + + // Add a credential for the password and the API Key + var passCred = new Credential(CredentialTypes.Password.Pbkdf2, newUser.HashedPassword); + newUser.Credentials.Add(CredentialBuilder.CreateV1ApiKey(apiKey)); + newUser.Credentials.Add(passCred); + + if (!Config.ConfirmEmailAddresses) + { + newUser.ConfirmEmailAddress(); + } + + Entities.Users.Add(newUser); + Entities.SaveChanges(); + + return new AuthenticatedUser(newUser, passCred); + } + + public virtual void ReplaceCredential(string username, Credential credential) + { + var user = Entities + .Users + .Include(u => u.Credentials) + .SingleOrDefault(u => u.Username == username); + if (user == null) + { + throw new InvalidOperationException(Strings.UserNotFound); + } + ReplaceCredential(user, credential); + } + + public virtual void ReplaceCredential(User user, Credential credential) + { + ReplaceCredentialInternal(user, credential); + Entities.SaveChanges(); + } + + public virtual bool ResetPasswordWithToken(string username, string token, string newPassword) + { + if (String.IsNullOrEmpty(newPassword)) + { + throw new ArgumentNullException("newPassword"); + } + + var user = Entities + .Users + .Include(u => u.Credentials) + .SingleOrDefault(u => u.Username == username); + + if (user != null && String.Equals(user.PasswordResetToken, token, StringComparison.Ordinal) && !user.PasswordResetTokenExpirationDate.IsInThePast()) + { + if (!user.Confirmed) + { + throw new InvalidOperationException(Strings.UserIsNotYetConfirmed); + } + + ReplaceCredentialInternal(user, CredentialBuilder.CreatePbkdf2Password(newPassword)); + user.PasswordResetToken = null; + user.PasswordResetTokenExpirationDate = null; + Entities.SaveChanges(); + return true; + } + + return false; + } + + public virtual bool ChangePassword(string username, string oldPassword, string newPassword) + { + // Review: If the old password is hashed using something other than PBKDF2, we end up making an extra db call that changes the old hash password. + // This operation is rare enough that I'm not inclined to change it. + var authUser = Authenticate(username, oldPassword); + if (authUser == null) + { + return false; + } + + var cred = CredentialBuilder.CreatePbkdf2Password(newPassword); + ReplaceCredentialInternal(authUser.User, cred); + Entities.SaveChanges(); + return true; + } + + public virtual User GeneratePasswordResetToken(string usernameOrEmail, int expirationInMinutes) + { + if (String.IsNullOrEmpty(usernameOrEmail)) + { + throw new ArgumentNullException("usernameOrEmail"); + } + if (expirationInMinutes < 1) + { + throw new ArgumentException( + "Token expiration should give the user at least a minute to change their password", "expirationInMinutes"); + } + + var user = FindByUserNameOrEmail(usernameOrEmail); + if (user == null) + { + return null; + } + + if (!user.Confirmed) + { + throw new InvalidOperationException(Strings.UserIsNotYetConfirmed); + } + + if (!String.IsNullOrEmpty(user.PasswordResetToken) && !user.PasswordResetTokenExpirationDate.IsInThePast()) + { + return user; + } + + user.PasswordResetToken = CryptographyService.GenerateToken(); + user.PasswordResetTokenExpirationDate = DateTime.UtcNow.AddMinutes(expirationInMinutes); + + Entities.SaveChanges(); + return user; + } + + public static ClaimsIdentity CreateIdentity(User user, string authenticationType, params Claim[] additionalClaims) + { + var claims = Enumerable.Concat(new[] { + new Claim(ClaimsIdentity.DefaultNameClaimType, user.Username), + new Claim(ClaimTypes.AuthenticationMethod, authenticationType), + + // Needed for anti-forgery token, also good practice to have a unique identifier claim + new Claim(ClaimTypes.NameIdentifier, user.Username) + }, user.Roles.Select(r => new Claim(ClaimsIdentity.DefaultRoleClaimType, r.Name))); + + if (additionalClaims.Length > 0) + { + claims = Enumerable.Concat(claims, additionalClaims); + } + + ClaimsIdentity identity = new ClaimsIdentity( + claims, + authenticationType, + nameType: ClaimsIdentity.DefaultNameClaimType, + roleType: ClaimsIdentity.DefaultRoleClaimType); + return identity; + } + + private void ReplaceCredentialInternal(User user, Credential credential) + { + // Find the credentials we're replacing, if any + var creds = user.Credentials + .Where(cred => + // If we're replacing a password credential, remove ALL password credentials + (credential.Type.StartsWith(CredentialTypes.Password.Prefix, StringComparison.OrdinalIgnoreCase) && + cred.Type.StartsWith(CredentialTypes.Password.Prefix, StringComparison.OrdinalIgnoreCase)) || + cred.Type == credential.Type) + .ToList(); + foreach (var cred in creds) + { + user.Credentials.Remove(cred); + Entities.DeleteOnCommit(cred); + } + + user.Credentials.Add(credential); + } + + private Credential FindMatchingCredential(Credential credential) + { + var results = Entities + .Set() + .Include(u => u.User) + .Include(u => u.User.Roles) + .Where(c => c.Type == credential.Type && c.Value == credential.Value) + .ToList(); + + if (results.Count == 0) + { + return null; + } + else if (results.Count == 1) + { + return results[0]; + } + else + { + // 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, + results.First().Key); + Trace.Error(message); + throw new InvalidOperationException(message); + } + } + + private User FindByUserNameOrEmail(string userNameOrEmail) + { + var users = Entities + .Users + .Include(u => u.Credentials) + .Include(u => u.Roles); + + var user = users.SingleOrDefault(u => u.Username == userNameOrEmail); + if (user == null) + { + var allMatches = users + .Where(u => u.EmailAddress == userNameOrEmail) + .Take(2) + .ToList(); + + if (allMatches.Count == 1) + { + user = allMatches[0]; + } + else + { + // If multiple matches, leave it null to signal no unique email address + Trace.Warning("Multiple user accounts with email address: " + userNameOrEmail + " found: " + String.Join(", ", allMatches.Select(u => u.Username))); + } + } + return user; + } + + public static bool ValidatePasswordCredential(IEnumerable creds, string password, out Credential matched) + { + matched = creds.FirstOrDefault(c => ValidatePasswordCredential(c, password)); + return matched != null; + } + + 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) } + }; + + public static bool ValidatePasswordCredential(Credential cred, string password) + { + Func validator; + if (!_validators.TryGetValue(cred.Type, out validator)) + { + return false; + } + return validator(password, cred); + } + + private void MigrateCredentials(User user, List creds, string password) + { + var toRemove = creds.Where(c => + !String.Equals( + c.Type, + CredentialTypes.Password.Pbkdf2, + StringComparison.OrdinalIgnoreCase)) + .ToList(); + + // Remove any non PBKDF2 credentials + foreach (var cred in toRemove) + { + creds.Remove(cred); + user.Credentials.Remove(cred); + } + + // Now add one if there are no credentials left + if (creds.Count == 0) + { + user.Credentials.Add(CredentialBuilder.CreatePbkdf2Password(password)); + } + + // Save changes, if any + Entities.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/AuthenticationTypes.cs b/src/NuGetGallery/Authentication/AuthenticationTypes.cs new file mode 100644 index 0000000000..7caa64e046 --- /dev/null +++ b/src/NuGetGallery/Authentication/AuthenticationTypes.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace NuGetGallery.Authentication +{ + public static class AuthenticationTypes + { + public static readonly string Password = "password"; + public static readonly string ApiKey = "apikey"; + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/ForceSslWhenAuthenticatedMiddleware.cs b/src/NuGetGallery/Authentication/ForceSslWhenAuthenticatedMiddleware.cs new file mode 100644 index 0000000000..19ac1fd4d3 --- /dev/null +++ b/src/NuGetGallery/Authentication/ForceSslWhenAuthenticatedMiddleware.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Web; +using Microsoft.Owin; +using Microsoft.Owin.Logging; +using NuGetGallery.Configuration; +using Owin; + +namespace NuGetGallery.Authentication +{ + public class ForceSslWhenAuthenticatedMiddleware : OwinMiddleware + { + public static readonly string DefaultCookieName = ".ForceSsl"; + + private readonly ILogger _logger; + public string CookieName { get; private set; } + public int SslPort { get; private set; } + + public ForceSslWhenAuthenticatedMiddleware(OwinMiddleware next, IAppBuilder app, string cookieName, int sslPort) + : base(next) + { + CookieName = cookieName; + SslPort = sslPort; + _logger = app.CreateLogger(); + } + + public override async Task Invoke(IOwinContext context) + { + // Check for the SSL Cookie + var forceSSL = context.Request.Cookies[CookieName]; + if (!String.IsNullOrEmpty(forceSSL) && !context.Request.IsSecure) + { + _logger.WriteVerbose("Force SSL Cookie found. Redirecting to SSL"); + // Presence of the cookie is all we care about, value is ignored + context.Response.Redirect(new UriBuilder(context.Request.Uri) + { + Scheme = Uri.UriSchemeHttps, + Port = SslPort + }.Uri.AbsoluteUri); + } + else + { + // Invoke the rest of the pipeline + await Next.Invoke(context); + + var cookieOptions = new CookieOptions() { HttpOnly = true }; + if (context.Authentication.AuthenticationResponseGrant != null) + { + _logger.WriteVerbose("Auth Grant found, writing Force SSL cookie"); + // We're granting new authentication, so drop a force ssl cookie + // for later. + context.Response.Cookies.Append(CookieName, "true", cookieOptions); + } + else if (context.Authentication.AuthenticationResponseRevoke != null) + { + _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 + }); + } + } + } + } +} + +namespace Owin { + using NuGetGallery.Authentication; + + public static class ForceSslWhenAuthenticatedExtensions + { + public static IAppBuilder UseForceSslWhenAuthenticated(this IAppBuilder self) + { + return UseForceSslWhenAuthenticated( + self, + ForceSslWhenAuthenticatedMiddleware.DefaultCookieName, + 443); + } + + public static IAppBuilder UseForceSslWhenAuthenticated(this IAppBuilder self, int sslPort) + { + return UseForceSslWhenAuthenticated( + self, + ForceSslWhenAuthenticatedMiddleware.DefaultCookieName, + sslPort); + } + + public static IAppBuilder UseForceSslWhenAuthenticated(this IAppBuilder self, string cookieName) + { + return UseForceSslWhenAuthenticated( + self, + cookieName, + 443); + } + + public static IAppBuilder UseForceSslWhenAuthenticated(this IAppBuilder self, string cookieName, int sslPort) + { + return self.Use(typeof(ForceSslWhenAuthenticatedMiddleware), self, cookieName, sslPort); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Authentication/NuGetClaims.cs b/src/NuGetGallery/Authentication/NuGetClaims.cs new file mode 100644 index 0000000000..75bd67339a --- /dev/null +++ b/src/NuGetGallery/Authentication/NuGetClaims.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace NuGetGallery.Authentication +{ + public static class NuGetClaims + { + public static readonly string ApiKey = "https://claims.nuget.org/apikey"; + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Constants.cs b/src/NuGetGallery/Constants.cs index 4ed65b3b45..2700cfbf96 100644 --- a/src/NuGetGallery/Constants.cs +++ b/src/NuGetGallery/Constants.cs @@ -35,6 +35,10 @@ public static class Constants public const string UrlValidationRegEx = @"(https?):\/\/[^ ""]+$"; public const string UrlValidationErrorMessage = "This doesn't appear to be a valid HTTP/HTTPS URL"; + public static readonly string ApiKeyHeaderName = "X-NuGet-ApiKey"; + public static readonly string ReturnUrlParameterName = "ReturnUrl"; + public static readonly string CurrentUserOwinEnvironmentKey = "nuget.user"; + public static class ContentNames { public static readonly string FrontPageAnnouncement = "FrontPage-Announcement"; diff --git a/src/NuGetGallery/Controllers/ApiController.cs b/src/NuGetGallery/Controllers/ApiController.cs index 779b5d77b0..993568f9c3 100644 --- a/src/NuGetGallery/Controllers/ApiController.cs +++ b/src/NuGetGallery/Controllers/ApiController.cs @@ -7,10 +7,12 @@ using System.Linq; using System.Net; using System.Threading.Tasks; +using System.Web; using System.Web.Mvc; using System.Web.UI; using Newtonsoft.Json.Linq; using NuGet; +using NuGetGallery.Authentication; using NuGetGallery.Filters; using NuGetGallery.Packaging; @@ -26,7 +28,7 @@ public partial class ApiController : AppController public IStatisticsService StatisticsService { get; set; } public IContentService ContentService { get; set; } public IIndexingService IndexingService { get; set; } - + protected ApiController() { } public ApiController( @@ -62,8 +64,8 @@ public ApiController( StatisticsService = statisticsService; } - [ActionName("GetPackageApi")] [HttpGet] + [ActionName("GetPackageApi")] public virtual async Task GetPackage(string id, string version) { // some security paranoia about URL hacking somehow creating e.g. open redirects @@ -122,21 +124,21 @@ public virtual async Task GetPackage(string id, string version) catch (SqlException e) { // Log the error and continue - QuietlyLogException(e); + QuietLog.LogHandledException(e); } catch (DataException e) { // Log the error and continue - QuietlyLogException(e); + QuietLog.LogHandledException(e); } } catch (SqlException e) { - QuietlyLogException(e); + QuietLog.LogHandledException(e); } catch (DataException e) { - QuietlyLogException(e); + QuietLog.LogHandledException(e); } // Fall back to constructing the URL based on the package version and ID. @@ -160,9 +162,10 @@ public virtual Task GetNuGetExe() } [HttpGet] + [RequireSsl] + [ApiAuthorize] [ActionName("VerifyPackageKeyApi")] - [ApiKeyAuthorize] - public virtual ActionResult VerifyPackageKey(string apiKey, string id, string version) + public virtual ActionResult VerifyPackageKey(string id, string version) { if (!String.IsNullOrEmpty(id)) { @@ -174,10 +177,10 @@ public virtual ActionResult VerifyPackageKey(string apiKey, string id, string ve HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version)); } - var user = GetUserByApiKey(apiKey); - if (user == null || !package.IsOwner(user)) + var user = GetCurrentUser(); + if (!package.IsOwner(user)) { - return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, String.Format(CultureInfo.CurrentCulture, Strings.ApiKeyNotAuthorized, "push")); + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); } } @@ -185,27 +188,28 @@ public virtual ActionResult VerifyPackageKey(string apiKey, string id, string ve } [HttpPut] + [RequireSsl] + [ApiAuthorize] [ActionName("PushPackageApi")] - [RequireRemoteHttps(OnlyWhenAuthenticated = false)] - [ApiKeyAuthorize] - public virtual Task CreatePackagePut(string apiKey) + public virtual Task CreatePackagePut() { - var user = GetUserByApiKey(apiKey); - return CreatePackageInternal(user); + return CreatePackageInternal(); } [HttpPost] + [RequireSsl] + [ApiAuthorize] [ActionName("PushPackageApi")] - [RequireRemoteHttps(OnlyWhenAuthenticated = false)] - [ApiKeyAuthorize] - public virtual Task CreatePackagePost(string apiKey) + public virtual Task CreatePackagePost() { - var user = GetUserByApiKey(apiKey); - return CreatePackageInternal(user); + return CreatePackageInternal(); } - private async Task CreatePackageInternal(User user) + private async Task CreatePackageInternal() { + // Get the user + var user = GetCurrentUser(); + using (var packageToPush = ReadPackageFromRequest()) { // Ensure that the user can push packages for this partialId. @@ -254,10 +258,10 @@ private async Task CreatePackageInternal(User user) } [HttpDelete] + [RequireSsl] + [ApiAuthorize] [ActionName("DeletePackageApi")] - [RequireRemoteHttps(OnlyWhenAuthenticated = false)] - [ApiKeyAuthorize] - public virtual ActionResult DeletePackage(string apiKey, string id, string version) + public virtual ActionResult DeletePackage(string id, string version) { var package = PackageService.FindPackageByIdAndVersion(id, version); if (package == null) @@ -266,17 +270,11 @@ public virtual ActionResult DeletePackage(string apiKey, string id, string versi HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version)); } - User user = GetUserByApiKey(apiKey); - if (user == null) - { - return new HttpStatusCodeWithBodyResult( - HttpStatusCode.Forbidden, String.Format(CultureInfo.CurrentCulture, Strings.ApiKeyNotAuthorized, "delete")); - } - + var user = GetCurrentUser(); if (!package.IsOwner(user)) { return new HttpStatusCodeWithBodyResult( - HttpStatusCode.Forbidden, String.Format(CultureInfo.CurrentCulture, Strings.ApiKeyNotAuthorized, "delete")); + HttpStatusCode.Forbidden, Strings.ApiKeyNotAuthorized); } PackageService.MarkPackageUnlisted(package); @@ -285,10 +283,10 @@ public virtual ActionResult DeletePackage(string apiKey, string id, string versi } [HttpPost] + [RequireSsl] + [ApiAuthorize] [ActionName("PublishPackageApi")] - [RequireRemoteHttps(OnlyWhenAuthenticated = false)] - [ApiKeyAuthorize] - public virtual ActionResult PublishPackage(string apiKey, string id, string version) + public virtual ActionResult PublishPackage(string id, string version) { var package = PackageService.FindPackageByIdAndVersion(id, version); if (package == null) @@ -297,13 +295,7 @@ public virtual ActionResult PublishPackage(string apiKey, string id, string vers HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version)); } - User user = GetUserByApiKey(apiKey); - if (user == null) - { - return new HttpStatusCodeWithBodyResult( - HttpStatusCode.Forbidden, String.Format(CultureInfo.CurrentCulture, Strings.ApiKeyNotAuthorized, "publish")); - } - + User user = GetCurrentUser(); if (!package.IsOwner(user)) { return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, String.Format(CultureInfo.CurrentCulture, Strings.ApiKeyNotAuthorized, "publish")); @@ -354,8 +346,8 @@ protected internal virtual INupkg ReadPackageFromRequest() return new Nupkg(stream, leaveOpen: false); } - [ActionName("PackageIDs")] [HttpGet] + [ActionName("PackageIDs")] public virtual ActionResult GetPackageIds(string partialId, bool? includePrerelease) { var query = GetService(); @@ -366,8 +358,8 @@ public virtual ActionResult GetPackageIds(string partialId, bool? includePrerele }; } - [ActionName("PackageVersions")] [HttpGet] + [ActionName("PackageVersions")] public virtual ActionResult GetPackageVersions(string id, bool? includePrerelease) { var query = GetService(); @@ -378,79 +370,47 @@ public virtual ActionResult GetPackageVersions(string id, bool? includePrereleas }; } - [ActionName("StatisticsDownloadsApi")] [HttpGet] + [ActionName("StatisticsDownloadsApi")] public virtual async Task GetStatsDownloads(int? count) { - if (StatisticsService != null) + var result = await StatisticsService.LoadDownloadPackageVersions(); + + if (result.Loaded) { - var result = await StatisticsService.LoadDownloadPackageVersions(); + int i = 0; - if (result.Loaded) + JArray content = new JArray(); + foreach (StatisticsPackagesItemViewModel row in StatisticsService.DownloadPackageVersionsAll) { - int i = 0; - - JArray content = new JArray(); - foreach (StatisticsPackagesItemViewModel row in StatisticsService.DownloadPackageVersionsAll) - { - JObject item = new JObject(); - - item.Add("PackageId", row.PackageId); - item.Add("PackageVersion", row.PackageVersion); - item.Add("Gallery", Url.PackageGallery(row.PackageId, row.PackageVersion)); - item.Add("PackageTitle", row.PackageTitle ?? row.PackageId); - item.Add("PackageDescription", row.PackageDescription); - item.Add("PackageIconUrl", row.PackageIconUrl ?? Url.PackageDefaultIcon()); - item.Add("Downloads", row.Downloads); + JObject item = new JObject(); - content.Add(item); + item.Add("PackageId", row.PackageId); + item.Add("PackageVersion", row.PackageVersion); + item.Add("Gallery", Url.PackageGallery(row.PackageId, row.PackageVersion)); + item.Add("PackageTitle", row.PackageTitle ?? row.PackageId); + item.Add("PackageDescription", row.PackageDescription); + item.Add("PackageIconUrl", row.PackageIconUrl ?? Url.PackageDefaultIcon()); + item.Add("Downloads", row.Downloads); - i++; + content.Add(item); - if (count.HasValue && count.Value == i) - { - break; - } - } + i++; - return new ContentResult + if (count.HasValue && count.Value == i) { - Content = content.ToString(), - ContentType = "application/json" - }; + break; + } } - } - return new HttpStatusCodeResult(HttpStatusCode.NotFound); - } - - private User GetUserByApiKey(string apiKey) - { - var cred = UserService.AuthenticateCredential(CredentialTypes.ApiKeyV1, apiKey.ToLowerInvariant()); - User user; - if (cred == null) - { -#pragma warning disable 0618 - user = UserService.FindByApiKey(Guid.Parse(apiKey)); -#pragma warning restore 0618 - } - else - { - user = cred.User; + return new ContentResult + { + Content = content.ToString(), + ContentType = "application/json" + }; } - return user; - } - private static void QuietlyLogException(Exception e) - { - try - { - Elmah.ErrorSignal.FromCurrentContext().Raise(e); - } - catch - { - // logging failed, don't allow exception to escape - } + return new HttpStatusCodeResult(HttpStatusCode.NotFound); } } } diff --git a/src/NuGetGallery/Controllers/AppController.cs b/src/NuGetGallery/Controllers/AppController.cs index b470aa8f43..6e83e3f31a 100644 --- a/src/NuGetGallery/Controllers/AppController.cs +++ b/src/NuGetGallery/Controllers/AppController.cs @@ -1,18 +1,90 @@ -using System.Security.Principal; +using System; +using System.Security.Claims; +using System.Security.Principal; +using System.Linq; +using System.Web; using System.Web.Mvc; +using Microsoft.Owin; +using NuGetGallery.Authentication; +using System.Net; namespace NuGetGallery { public abstract partial class AppController : Controller { - public virtual IIdentity Identity + private IOwinContext _overrideContext; + + public IOwinContext OwinContext + { + get { return _overrideContext ?? HttpContext.GetOwinContext(); } + set { _overrideContext = value; } + } + + public new ClaimsPrincipal User { - get { return User.Identity; } + get { return base.User as ClaimsPrincipal; } } protected internal virtual T GetService() { return DependencyResolver.Current.GetService(); } + + // 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 + /// retrieved it, from memory. This will NEVER return null. It will throw an exception + /// that will yield an HTTP 401 if it would return null. As a result, it should only + /// be called in actions with the Authorize attribute or a Request.IsAuthenticated check + /// + /// The current user + protected internal User GetCurrentUser() + { + if (OwinContext.Request.User == null) + { + return null; + } + + User user = null; + object obj; + if (OwinContext.Environment.TryGetValue(Constants.CurrentUserOwinEnvironmentKey, out obj)) + { + user = obj as User; + } + + if (user == null) + { + user = LoadUser(); + OwinContext.Environment[Constants.CurrentUserOwinEnvironmentKey] = user; + } + + if (user == null) + { + // Unauthorized! If we get here it's because a valid session token was presented, but the + // user doesn't exist any more. So we just have a generic error. + throw new HttpException(401, Strings.Unauthorized); + } + + return user; + } + + private User LoadUser() + { + var principal = OwinContext.Authentication.User; + if (principal != null) + { + // Try to authenticate with the user name + string userName = principal.GetClaimOrDefault(ClaimTypes.Name); + + if (!String.IsNullOrEmpty(userName)) + { + return DependencyResolver + .Current + .GetService() + .FindByUsername(userName); + } + } + return null; // No user logged in, or credentials could not be resolved + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Controllers/AuthenticationController.cs b/src/NuGetGallery/Controllers/AuthenticationController.cs index 1ff0a5acf9..632af9584d 100644 --- a/src/NuGetGallery/Controllers/AuthenticationController.cs +++ b/src/NuGetGallery/Controllers/AuthenticationController.cs @@ -1,14 +1,16 @@ using System; +using System.Web; using System.Collections.Generic; using System.Linq; using System.Web.Mvc; +using NuGetGallery.Authentication; +using NuGetGallery.Filters; namespace NuGetGallery { - public partial class AuthenticationController : Controller + public partial class AuthenticationController : AppController { - public IFormsAuthenticationService FormsAuth { get; protected set; } - public IUserService UserService { get; protected set; } + public AuthenticationService AuthService { get; protected set; } // For sub-classes to initialize services themselves protected AuthenticationController() @@ -16,20 +18,18 @@ protected AuthenticationController() } public AuthenticationController( - IFormsAuthenticationService formsAuthService, - IUserService userService) + AuthenticationService authService) { - FormsAuth = formsAuthService; - UserService = userService; + AuthService = authService; } - [RequireRemoteHttps(OnlyWhenAuthenticated = false)] + [RequireSsl] public virtual ActionResult LogOn(string returnUrl) { // I think it should be obvious why we don't want the current URL to be the return URL here ;) ViewData[Constants.ReturnUrlViewDataKey] = returnUrl; - if (User != null && User.Identity != null && User.Identity.IsAuthenticated) + if (Request.IsAuthenticated) { TempData["Message"] = "You are already logged in!"; return Redirect(returnUrl); @@ -40,13 +40,13 @@ public virtual ActionResult LogOn(string returnUrl) [HttpPost] [ValidateAntiForgeryToken] - [RequireRemoteHttps(OnlyWhenAuthenticated = false)] + [RequireSsl] public virtual ActionResult SignIn(SignInRequest request, string returnUrl) { // I think it should be obvious why we don't want the current URL to be the return URL here ;) ViewData[Constants.ReturnUrlViewDataKey] = returnUrl; - if (User != null && User.Identity != null && User.Identity.IsAuthenticated) + if (Request.IsAuthenticated) { ModelState.AddModelError(String.Empty, "You are already logged in!"); return View(); @@ -57,9 +57,7 @@ public virtual ActionResult SignIn(SignInRequest request, string returnUrl) return View(); } - var user = UserService.FindByUsernameOrEmailAddressAndPassword( - request.UserNameOrEmail, - request.Password); + var user = AuthService.Authenticate(request.UserNameOrEmail, request.Password); if (user == null) { @@ -70,19 +68,19 @@ public virtual ActionResult SignIn(SignInRequest request, string returnUrl) return View(); } - SetAuthenticationCookie(user); + AuthService.CreateSession(OwinContext, user.User, AuthenticationTypes.Password); return SafeRedirect(returnUrl); } [HttpPost] [ValidateAntiForgeryToken] - [RequireRemoteHttps(OnlyWhenAuthenticated = false)] + [RequireSsl] public virtual ActionResult Register(RegisterRequest request, string returnUrl) { // I think it should be obvious why we don't want the current URL to be the return URL here ;) ViewData[Constants.ReturnUrlViewDataKey] = returnUrl; - if (User != null && User.Identity != null && User.Identity.IsAuthenticated) + if (Request.IsAuthenticated) { ModelState.AddModelError(String.Empty, "You are already logged in!"); return View(); @@ -93,10 +91,10 @@ public virtual ActionResult Register(RegisterRequest request, string returnUrl) return View(); } - User user; + AuthenticatedUser user; try { - user = UserService.Create( + user = AuthService.Register( request.Username, request.Password, request.EmailAddress); @@ -107,7 +105,7 @@ public virtual ActionResult Register(RegisterRequest request, string returnUrl) return View(); } - SetAuthenticationCookie(user); + AuthService.CreateSession(OwinContext, user.User, AuthenticationTypes.Password); if (RedirectHelper.SafeRedirectUrl(Url, returnUrl) != RedirectHelper.SafeRedirectUrl(Url, null)) { @@ -122,10 +120,7 @@ public virtual ActionResult Register(RegisterRequest request, string returnUrl) public virtual ActionResult LogOff(string returnUrl) { - // TODO: this should really be a POST - - FormsAuth.SignOut(); - + OwinContext.Authentication.SignOut(); return SafeRedirect(returnUrl); } @@ -134,20 +129,5 @@ protected virtual ActionResult SafeRedirect(string returnUrl) { return Redirect(RedirectHelper.SafeRedirectUrl(Url, returnUrl)); } - - [NonAction] - protected virtual void SetAuthenticationCookie(User user) - { - IEnumerable roles = null; - if (user.Roles.AnySafe()) - { - roles = user.Roles.Select(r => r.Name); - } - - FormsAuth.SetAuthCookie( - user.Username, - true, - roles); - } } } diff --git a/src/NuGetGallery/Controllers/CuratedFeedsController.cs b/src/NuGetGallery/Controllers/CuratedFeedsController.cs index 30745f89bb..0134c90c30 100644 --- a/src/NuGetGallery/Controllers/CuratedFeedsController.cs +++ b/src/NuGetGallery/Controllers/CuratedFeedsController.cs @@ -33,7 +33,7 @@ public virtual ActionResult CuratedFeed(string name) return HttpNotFound(); } - if (curatedFeed.Managers.All(manager => manager.Username != Identity.Name)) + if (curatedFeed.Managers.All(manager => manager.Username != User.Identity.Name)) { return new HttpStatusCodeResult(403); } diff --git a/src/NuGetGallery/Controllers/CuratedPackagesController.cs b/src/NuGetGallery/Controllers/CuratedPackagesController.cs index 4b58e6df64..4d9d647a69 100644 --- a/src/NuGetGallery/Controllers/CuratedPackagesController.cs +++ b/src/NuGetGallery/Controllers/CuratedPackagesController.cs @@ -31,7 +31,7 @@ public virtual ActionResult GetCreateCuratedPackageForm(string curatedFeedName) return HttpNotFound(); } - if (curatedFeed.Managers.All(manager => manager.Username != Identity.Name)) + if (curatedFeed.Managers.All(manager => manager.Username != User.Identity.Name)) { return new HttpStatusCodeResult(403); } @@ -58,7 +58,7 @@ public virtual ActionResult DeleteCuratedPackage( return HttpNotFound(); } - if (curatedFeed.Managers.All(manager => manager.Username != Identity.Name)) + if (curatedFeed.Managers.All(manager => manager.Username != User.Identity.Name)) { return new HttpStatusCodeResult(403); } @@ -89,7 +89,7 @@ public virtual ActionResult PatchCuratedPackage( return HttpNotFound(); } - if (curatedFeed.Managers.All(manager => manager.Username != Identity.Name)) + if (curatedFeed.Managers.All(manager => manager.Username != User.Identity.Name)) { return new HttpStatusCodeResult(403); } @@ -119,7 +119,7 @@ public virtual ActionResult PostCuratedPackages( return HttpNotFound(); } - if (curatedFeed.Managers.All(manager => manager.Username != Identity.Name)) + if (curatedFeed.Managers.All(manager => manager.Username != User.Identity.Name)) { return new HttpStatusCodeResult(403); } diff --git a/src/NuGetGallery/Controllers/PackagesController.cs b/src/NuGetGallery/Controllers/PackagesController.cs index d1f1689323..f39166e081 100644 --- a/src/NuGetGallery/Controllers/PackagesController.cs +++ b/src/NuGetGallery/Controllers/PackagesController.cs @@ -34,17 +34,14 @@ public partial class PackagesController : AppController private readonly IPackageFileService _packageFileService; private readonly ISearchService _searchService; private readonly IUploadFileService _uploadFileService; - private readonly IUserService _userService; private readonly IEntitiesContext _entitiesContext; private readonly IIndexingService _indexingService; private readonly ICacheService _cacheService; private readonly EditPackageService _editPackageService; - private readonly IPrincipal _currentUser; public PackagesController( IPackageService packageService, IUploadFileService uploadFileService, - IUserService userService, IMessageService messageService, ISearchService searchService, IAutomaticallyCuratePackageCommand autoCuratedPackageCmd, @@ -54,12 +51,10 @@ public PackagesController( IAppConfiguration config, IIndexingService indexingService, ICacheService cacheService, - EditPackageService editPackageService, - IPrincipal currentUser) + EditPackageService editPackageService) { _packageService = packageService; _uploadFileService = uploadFileService; - _userService = userService; _messageService = messageService; _searchService = searchService; _autoCuratedPackageCmd = autoCuratedPackageCmd; @@ -70,14 +65,13 @@ public PackagesController( _indexingService = indexingService; _cacheService = cacheService; _editPackageService = editPackageService; - _currentUser = currentUser; } [Authorize] [OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")] public virtual ActionResult UploadPackageProgress() { - string username = _currentUser.Identity.Name; + string username = User.Identity.Name; AsyncFileUploadProgress progress = _cacheService.GetProgress(username); if (progress == null) @@ -99,7 +93,7 @@ public virtual ActionResult UndoPendingEdits(string id, string version) return HttpNotFound(); } - if (!package.IsOwner(HttpContext.User)) + if (!package.IsOwner(User)) { return new HttpStatusCodeResult(403, "Forbidden"); } @@ -147,7 +141,7 @@ public virtual ActionResult UndoPendingEdits(string id, string version) [RequiresAccountConfirmation("upload a package")] public async virtual Task UploadPackage() { - var currentUser = _userService.FindByUsername(_currentUser.Identity.Name); + var currentUser = GetCurrentUser(); using (var existingUploadFile = await _uploadFileService.GetUploadFileAsync(currentUser.Key)) { @@ -162,11 +156,11 @@ public async virtual Task UploadPackage() [HttpPost] [Authorize] - [RequiresAccountConfirmation("upload a package")] [ValidateAntiForgeryToken] + [RequiresAccountConfirmation("upload a package")] public virtual async Task UploadPackage(HttpPostedFileBase uploadFile) { - var currentUser = _userService.FindByUsername(_currentUser.Identity.Name); + var currentUser = GetCurrentUser(); using (var existingUploadFile = await _uploadFileService.GetUploadFileAsync(currentUser.Key)) { @@ -246,7 +240,7 @@ public virtual ActionResult DisplayPackage(string id, string version) } var model = new DisplayPackageViewModel(package); - if (package.IsOwner(_currentUser)) + if (package.IsOwner(User)) { // Tell logged-in package owners not to cache the package page, so they won't be confused about the state of pending edits. Response.Cache.SetCacheability(HttpCacheability.NoCache); @@ -332,7 +326,7 @@ public virtual ActionResult ReportAbuse(string id, string version) if (Request.IsAuthenticated) { - var user = _userService.FindByUsername(HttpContext.User.Identity.Name); + var user = GetCurrentUser(); // If user logged on in as owner a different tab, then clicked the link, we can redirect them to ReportMyPackage if (package.IsOwner(user)) @@ -362,7 +356,7 @@ public virtual ActionResult ReportAbuse(string id, string version) [RequiresAccountConfirmation("contact support about your package")] public virtual ActionResult ReportMyPackage(string id, string version) { - var user = _userService.FindByUsername(HttpContext.User.Identity.Name); + var user = GetCurrentUser(); var package = _packageService.FindPackageByIdAndVersion(id, version); @@ -372,7 +366,7 @@ public virtual ActionResult ReportMyPackage(string id, string version) } // If user hit this url by constructing it manually but is not the owner, redirect them to ReportAbuse - if (!(HttpContext.User.IsInRole(Constants.AdminRoleName) || package.IsOwner(user))) + if (!(User.IsInRole(Constants.AdminRoleName) || package.IsOwner(user))) { return RedirectToAction(ActionNames.ReportAbuse, new { id, version }); } @@ -411,7 +405,7 @@ public virtual ActionResult ReportAbuse(string id, string version, ReportAbuseVi MailAddress from; if (Request.IsAuthenticated) { - user = _userService.FindByUsername(HttpContext.User.Identity.Name); + user = GetCurrentUser(); from = user.ToMailAddress(); } else @@ -457,7 +451,7 @@ public virtual ActionResult ReportMyPackage(string id, string version, ReportAbu return HttpNotFound(); } - var user = _userService.FindByUsername(HttpContext.User.Identity.Name); + var user = GetCurrentUser(); MailAddress from = user.ToMailAddress(); _messageService.ReportMyPackage( @@ -515,7 +509,7 @@ public virtual ActionResult ContactOwners(string id, ContactOwnersViewModel cont return HttpNotFound(); } - var user = _userService.FindByUsername(User.Identity.Name); + var user = GetCurrentUser(); var fromAddress = new MailAddress(user.EmailAddress, user.Username); _messageService.SendContactOwnersMessage( fromAddress, package, contactForm.Message, Url.Action(MVC.Users.Edit(), protocol: Request.Url.Scheme)); @@ -539,12 +533,12 @@ public virtual ActionResult ManagePackageOwners(string id, string version) { return HttpNotFound(); } - if (!package.IsOwner(HttpContext.User)) + if (!package.IsOwner(User)) { return new HttpStatusCodeResult(401, "Unauthorized"); } - var model = new ManagePackageOwnersViewModel(package, HttpContext.User); + var model = new ManagePackageOwnersViewModel(package, User); return View(model); } @@ -576,7 +570,7 @@ public virtual ActionResult Edit(string id, string version) return HttpNotFound(); } - if (!package.IsOwner(HttpContext.User)) + if (!package.IsOwner(User)) { return new HttpStatusCodeResult(403, "Forbidden"); } @@ -599,9 +593,9 @@ public virtual ActionResult Edit(string id, string version) [Authorize] [HttpPost] - [RequiresAccountConfirmation("edit a package")] - [ValidateAntiForgeryToken] [ValidateInput(false)] // Security note: Disabling ASP.Net input validation which does things like disallow angle brackets in submissions. See http://go.microsoft.com/fwlink/?LinkID=212874 + [ValidateAntiForgeryToken] + [RequiresAccountConfirmation("edit a package")] public virtual ActionResult Edit(string id, string version, EditPackageRequest formData, string returnUrl) { var package = _packageService.FindPackageByIdAndVersion(id, version); @@ -610,12 +604,12 @@ public virtual ActionResult Edit(string id, string version, EditPackageRequest f return HttpNotFound(); } - var user = _userService.FindByUsername(HttpContext.User.Identity.Name); - if (user == null || !package.IsOwner(HttpContext.User)) + if (!package.IsOwner(User)) { return new HttpStatusCodeResult(403, "Forbidden"); } + var user = GetCurrentUser(); if (!ModelState.IsValid) { formData.PackageId = package.PackageRegistration.Id; @@ -664,17 +658,7 @@ public virtual ActionResult ConfirmOwner(string id, string username, string toke return HttpNotFound(); } - var user = _userService.FindByUsername(username); - if (user == null) - { - return HttpNotFound(); - } - - if (!String.Equals(user.Username, User.Identity.Name, StringComparison.OrdinalIgnoreCase)) - { - return new HttpStatusCodeResult(403); - } - + var user = GetCurrentUser(); ConfirmOwnershipResult result = _packageService.ConfirmPackageOwner(package, user, token); var model = new PackageOwnerConfirmationModel @@ -693,7 +677,7 @@ internal virtual ActionResult Edit(string id, string version, bool? listed, Func { return HttpNotFound(); } - if (!package.IsOwner(HttpContext.User)) + if (!package.IsOwner(User)) { return new HttpStatusCodeResult(401, "Unauthorized"); } @@ -726,7 +710,7 @@ private ActionResult GetPackageOwnerActionFormResult(string id, string version) { return HttpNotFound(); } - if (!package.IsOwner(HttpContext.User)) + if (!package.IsOwner(User)) { return new HttpStatusCodeResult(401, "Unauthorized"); } @@ -739,7 +723,7 @@ private ActionResult GetPackageOwnerActionFormResult(string id, string version) [RequiresAccountConfirmation("upload a package")] public virtual async Task VerifyPackage() { - var currentUser = _userService.FindByUsername(_currentUser.Identity.Name); + var currentUser = GetCurrentUser(); IPackageMetadata packageMetadata; using (Stream uploadFile = await _uploadFileService.GetUploadFileAsync(currentUser.Key)) @@ -795,7 +779,7 @@ public virtual async Task VerifyPackage() [ValidateInput(false)] // Security note: Disabling ASP.Net input validation which does things like disallow angle brackets in submissions. See http://go.microsoft.com/fwlink/?LinkID=212874 public virtual async Task VerifyPackage(VerifyPackageRequest formData) { - var currentUser = _userService.FindByUsername(_currentUser.Identity.Name); + var currentUser = GetCurrentUser(); Package package; using (Stream uploadFile = await _uploadFileService.GetUploadFileAsync(currentUser.Key)) @@ -888,7 +872,7 @@ public virtual async Task VerifyPackage(VerifyPackageRequest formD [ValidateAntiForgeryToken] public virtual async Task CancelUpload() { - var currentUser = _userService.FindByUsername(_currentUser.Identity.Name); + var currentUser = GetCurrentUser(); await _uploadFileService.DeleteUploadFileAsync(currentUser.Key); return RedirectToAction("UploadPackage"); @@ -909,7 +893,7 @@ internal virtual ActionResult SetLicenseReportVisibility(string id, string versi { return HttpNotFound(); } - if (!package.IsOwner(HttpContext.User)) + if (!package.IsOwner(User)) { return new HttpStatusCodeResult(401, "Unauthorized"); } diff --git a/src/NuGetGallery/Controllers/StatisticsController.cs b/src/NuGetGallery/Controllers/StatisticsController.cs index 696c5404bc..06fc2e2a91 100644 --- a/src/NuGetGallery/Controllers/StatisticsController.cs +++ b/src/NuGetGallery/Controllers/StatisticsController.cs @@ -99,7 +99,7 @@ private CultureInfo DetermineClientLocale() public virtual async Task Index() { - if (_statisticsService == null) + if (_statisticsService == NullStatisticsService.Instance) { return new HttpStatusCodeResult(HttpStatusCode.NotFound); } @@ -141,7 +141,7 @@ public virtual async Task Index() public virtual async Task Packages() { - if (_statisticsService == null) + if (_statisticsService == NullStatisticsService.Instance) { return new HttpStatusCodeResult(HttpStatusCode.NotFound); } @@ -164,7 +164,7 @@ public virtual async Task Packages() public virtual async Task PackageVersions() { - if (_statisticsService == null) + if (_statisticsService == NullStatisticsService.Instance) { return new HttpStatusCodeResult(HttpStatusCode.NotFound); } @@ -187,7 +187,7 @@ public virtual async Task PackageVersions() public virtual async Task PackageDownloadsByVersion(string id, string[] groupby) { - if (_statisticsService == null) + if (_statisticsService == NullStatisticsService.Instance) { return new HttpStatusCodeResult(HttpStatusCode.NotFound); } @@ -215,7 +215,7 @@ public virtual async Task PackageDownloadsByVersion(string id, str public virtual async Task PackageDownloadsDetail(string id, string version, string[] groupby) { - if (_statisticsService == null) + if (_statisticsService == NullStatisticsService.Instance) { return new HttpStatusCodeResult(HttpStatusCode.NotFound); } diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index 8359154261..dbedf6d208 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -4,6 +4,7 @@ using System.Net.Mail; using System.Security.Principal; using System.Web.Mvc; +using NuGetGallery.Authentication; using NuGetGallery.Configuration; namespace NuGetGallery @@ -11,11 +12,11 @@ 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 IUserService UserService { get; protected set; } - public IPrincipal CurrentUser { get; protected set; } + public AuthenticationService AuthService { get; protected set; } protected UsersController() { } @@ -25,20 +26,20 @@ public UsersController( IPackageService packageService, IMessageService messageService, IAppConfiguration config, - IPrincipal currentUser) : this() + AuthenticationService authService) : this() { CuratedFeedService = feedsQuery; UserService = userService; PackageService = packageService; MessageService = messageService; Config = config; - CurrentUser = currentUser; + AuthService = authService; } [Authorize] public virtual ActionResult Account() { - var user = UserService.FindByUsername(Identity.Name); + var user = GetCurrentUser(); var curatedFeeds = CuratedFeedService.GetFeedsForManager(user.Key); var apiCredential = user .Credentials @@ -54,11 +55,11 @@ public virtual ActionResult Account() }); } - [Authorize] [HttpGet] + [Authorize] public virtual ActionResult ConfirmationRequired() { - User user = UserService.FindByUsername(User.Identity.Name); + User user = GetCurrentUser(); var model = new ConfirmationViewModel { ConfirmingNewAccount = !(user.Confirmed), @@ -72,7 +73,7 @@ public virtual ActionResult ConfirmationRequired() [ActionName("ConfirmationRequired")] public virtual ActionResult ConfirmationRequiredPost() { - User user = UserService.FindByUsername(User.Identity.Name); + User user = GetCurrentUser(); var confirmationUrl = Url.ConfirmationUrl( MVC.Users.Confirm(), user.Username, user.EmailConfirmationToken, protocol: Request.Url.Scheme); @@ -90,7 +91,7 @@ public virtual ActionResult ConfirmationRequiredPost() [Authorize] public virtual ActionResult Edit() { - var user = UserService.FindByUsername(Identity.Name); + var user = GetCurrentUser(); var model = new EditProfileViewModel { Username = user.Username, @@ -106,7 +107,7 @@ public virtual ActionResult Edit() [ValidateAntiForgeryToken] public virtual ActionResult Edit(EditProfileViewModel profile) { - var user = UserService.FindByUsername(Identity.Name); + var user = GetCurrentUser(); if (user == null) { return HttpNotFound(); @@ -130,7 +131,7 @@ public virtual ActionResult Thanks() [Authorize] public virtual ActionResult Packages() { - var user = UserService.FindByUsername(Identity.Name); + var user = GetCurrentUser(); var packages = PackageService.FindPackagesByOwner(user, includeUnlisted: true) .Select(p => new PackageViewModel(p) { @@ -151,7 +152,7 @@ public virtual ActionResult Packages() public virtual ActionResult GenerateApiKey() { // Get the user - var user = UserService.FindByUsername(User.Identity.Name); + var user = GetCurrentUser(); // Generate an API Key var apiKey = Guid.NewGuid(); @@ -160,7 +161,7 @@ public virtual ActionResult GenerateApiKey() user.ApiKey = apiKey; // Add/Replace the API Key credential, and save to the database - UserService.ReplaceCredential(user, CredentialBuilder.CreateV1ApiKey(apiKey)); + AuthService.ReplaceCredential(user, CredentialBuilder.CreateV1ApiKey(apiKey)); return RedirectToAction(MVC.Users.Account()); } @@ -183,7 +184,7 @@ public virtual ActionResult ForgotPassword(ForgotPasswordViewModel model) if (ModelState.IsValid) { - var user = UserService.GeneratePasswordResetToken(model.Email, Constants.DefaultPasswordResetTokenExpirationHours * 60); + var user = AuthService.GeneratePasswordResetToken(model.Email, Constants.DefaultPasswordResetTokenExpirationHours * 60); if (user != null) { var resetPasswordUrl = Url.ConfirmationUrl( @@ -229,7 +230,7 @@ public virtual ActionResult ResetPassword(string username, string token, Passwor // 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; - ViewBag.ResetTokenValid = UserService.ResetPasswordWithToken(username, token, model.NewPassword); + ViewBag.ResetTokenValid = AuthService.ResetPasswordWithToken(username, token, model.NewPassword); if (!ViewBag.ResetTokenValid) { @@ -246,7 +247,7 @@ public virtual ActionResult Confirm(string username, string token) // 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 (!String.Equals(username, Identity.Name, StringComparison.OrdinalIgnoreCase)) + if (!String.Equals(username, User.Identity.Name, StringComparison.OrdinalIgnoreCase)) { return View(new ConfirmationViewModel { @@ -255,12 +256,8 @@ public virtual ActionResult Confirm(string username, string token) }); } - var user = UserService.FindByUsername(username); - if (user == null) - { - return HttpNotFound(); - } - + var user = GetCurrentUser(); + string existingEmail = user.EmailAddress; var model = new ConfirmationViewModel { @@ -337,14 +334,14 @@ public virtual ActionResult ChangeEmail(ChangeEmailRequestModel model) return View(model); } - User user = UserService.FindByUsernameAndPassword(Identity.Name, model.Password); - if (user == null) + var authUser = AuthService.Authenticate(User.Identity.Name, model.Password); + if (authUser == null) { ModelState.AddModelError("Password", Strings.CurrentPasswordIncorrect); return View(model); } - if (String.Equals(model.NewEmail, user.LastSavedEmailAddress, StringComparison.OrdinalIgnoreCase)) + if (String.Equals(model.NewEmail, authUser.User.LastSavedEmailAddress, StringComparison.OrdinalIgnoreCase)) { // email address unchanged - accept return RedirectToAction(MVC.Users.Edit()); @@ -352,7 +349,7 @@ public virtual ActionResult ChangeEmail(ChangeEmailRequestModel model) try { - UserService.ChangeEmailAddress(user, model.NewEmail); + UserService.ChangeEmailAddress(authUser.User, model.NewEmail); } catch (EntityException e) { @@ -360,11 +357,11 @@ public virtual ActionResult ChangeEmail(ChangeEmailRequestModel model) return View(model); } - if (user.Confirmed) + if (authUser.User.Confirmed) { var confirmationUrl = Url.ConfirmationUrl( - MVC.Users.Confirm(), user.Username, user.EmailConfirmationToken, protocol: Request.Url.Scheme); - MessageService.SendEmailChangeConfirmationNotice(new MailAddress(user.UnconfirmedEmailAddress, user.Username), confirmationUrl); + MVC.Users.Confirm(), authUser.User.Username, authUser.User.EmailConfirmationToken, protocol: Request.Url.Scheme); + MessageService.SendEmailChangeConfirmationNotice(new MailAddress(authUser.User.UnconfirmedEmailAddress, authUser.User.Username), confirmationUrl); TempData["Message"] = "Your email address has been changed! We sent a confirmation email to verify your new email. When you confirm the new email address, it will take effect and we will forget the old one."; @@ -384,8 +381,8 @@ public virtual ActionResult ChangePassword() } [HttpPost] - [ValidateAntiForgeryToken] [Authorize] + [ValidateAntiForgeryToken] public virtual ActionResult ChangePassword(PasswordChangeViewModel model) { if (!ModelState.IsValid) @@ -393,7 +390,7 @@ public virtual ActionResult ChangePassword(PasswordChangeViewModel model) return View(model); } - if (!UserService.ChangePassword(Identity.Name, model.OldPassword, model.NewPassword)) + if (!AuthService.ChangePassword(User.Identity.Name, model.OldPassword, model.NewPassword)) { ModelState.AddModelError( "OldPassword", diff --git a/src/NuGetGallery/Diagnostics/AuthenticationGlimpseTab.cs b/src/NuGetGallery/Diagnostics/AuthenticationGlimpseTab.cs new file mode 100644 index 0000000000..a883ac684e --- /dev/null +++ b/src/NuGetGallery/Diagnostics/AuthenticationGlimpseTab.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Glimpse.AspNet.Extensibility; +using Glimpse.Core.Extensibility; + +namespace NuGetGallery.Diagnostics +{ + public class AuthenticationGlimpseTab : AspNetTab + { + public override object GetData(ITabContext context) + { + return context.GetRequestContext().User; + } + + public override string Name + { + get { return "Auth"; } + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Diagnostics/IDiagnosticsService.cs b/src/NuGetGallery/Diagnostics/IDiagnosticsService.cs index 554c428daa..2cb2a6a10a 100644 --- a/src/NuGetGallery/Diagnostics/IDiagnosticsService.cs +++ b/src/NuGetGallery/Diagnostics/IDiagnosticsService.cs @@ -1,4 +1,5 @@ -namespace NuGetGallery.Diagnostics +using System; +namespace NuGetGallery.Diagnostics { public interface IDiagnosticsService { @@ -14,7 +15,19 @@ public static class DiagnosticsServiceExtensions { public static IDiagnosticsSource SafeGetSource(this IDiagnosticsService self, string name) { - return self == null ? new NullDiagnosticsSource() : self.GetSource(name); + // Hyper-defensive code to get a diagnostics source when self could be null AND self.GetSource(name) could return null. + // Designed to support all kinds of mocking scenarios and basically just never fail :) + try + { + return self == null ? + NullDiagnosticsSource.Instance : + (self.GetSource(name) ?? NullDiagnosticsSource.Instance); + } + catch(Exception ex) + { + System.Diagnostics.Trace.WriteLine("Error getting trace source: " + ex.ToString()); + return NullDiagnosticsSource.Instance; + } } } } diff --git a/src/NuGetGallery/Diagnostics/NullDiagnosticsSource.cs b/src/NuGetGallery/Diagnostics/NullDiagnosticsSource.cs index f2f7c1d787..1f65bd6e72 100644 --- a/src/NuGetGallery/Diagnostics/NullDiagnosticsSource.cs +++ b/src/NuGetGallery/Diagnostics/NullDiagnosticsSource.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; @@ -7,6 +8,11 @@ namespace NuGetGallery.Diagnostics { public class NullDiagnosticsSource : IDiagnosticsSource { + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Type is immutable")] + public static readonly NullDiagnosticsSource Instance = new NullDiagnosticsSource(); + + private NullDiagnosticsSource() { } + public void TraceEvent(System.Diagnostics.TraceEventType type, int id, string message, string member = null, string file = null, int line = 0) { // No-op! diff --git a/src/NuGetGallery/ExtensionMethods.cs b/src/NuGetGallery/ExtensionMethods.cs index b34268c4a0..7d5b7ce726 100644 --- a/src/NuGetGallery/ExtensionMethods.cs +++ b/src/NuGetGallery/ExtensionMethods.cs @@ -8,6 +8,7 @@ using System.Net.Mail; using System.Runtime.Versioning; using System.Security; +using System.Security.Claims; using System.Security.Principal; using System.ServiceModel.Activation; using System.Text; @@ -306,5 +307,23 @@ public static string ToFriendlyName(this FrameworkName frameworkName) } return sb.ToString(); } + + public static string GetClaimOrDefault(this ClaimsPrincipal self, string claimType) + { + return self.Claims.GetClaimOrDefault(claimType); + } + + public static string GetClaimOrDefault(this ClaimsIdentity self, string claimType) + { + return self.Claims.GetClaimOrDefault(claimType); + } + + public static string GetClaimOrDefault(this IEnumerable self, string claimType) + { + return self + .Where(c => String.Equals(c.Type, claimType, StringComparison.OrdinalIgnoreCase)) + .Select(c => c.Value) + .FirstOrDefault(); + } } } \ No newline at end of file diff --git a/src/NuGetGallery/ExtensionMethods.cs.rej b/src/NuGetGallery/ExtensionMethods.cs.rej new file mode 100644 index 0000000000..0f5a587e29 --- /dev/null +++ b/src/NuGetGallery/ExtensionMethods.cs.rej @@ -0,0 +1,63 @@ +diff a/src/NuGetGallery/ExtensionMethods.cs b/src/NuGetGallery/ExtensionMethods.cs (rejected hunks) +@@ -8,6 +8,7 @@ + using System.Net.Mail; + using System.Runtime.Versioning; + using System.Security; ++using System.Security.Claims; + using System.Security.Principal; + using System.ServiceModel.Activation; + using System.Text; +@@ -15,6 +16,7 @@ + using System.Web.Routing; + using System.Web.WebPages; + using NuGet; ++using NuGetGallery.Authentication; + + namespace NuGetGallery + { +@@ -40,6 +42,16 @@ + self.AddOrUpdate(key, val, (_, __) => val); + } + ++ public static UserSession AsUserSession(this IPrincipal self) ++ { ++ if (self == null) ++ { ++ return null; ++ } ++ // Direct cast because a non-ClaimsPrincipal is an error here. ++ return new UserSession((ClaimsPrincipal)self); ++ } ++ + public static SecureString ToSecureString(this string str) + { + SecureString output = new SecureString(); +@@ -175,25 +187,17 @@ + return package.PackageRegistration.IsOwner(user); + } + +- public static bool IsOwner(this Package package, User user) ++ public static bool IsOwner(this Package package, UserSession user) + { + return package.PackageRegistration.IsOwner(user); + } + + public static bool IsOwner(this PackageRegistration package, IPrincipal user) + { +- if (package == null) +- { +- throw new ArgumentNullException("package"); +- } +- if (user == null || user.Identity == null) +- { +- return false; +- } +- return user.IsAdministrator() || package.Owners.Any(u => u.Username == user.Identity.Name); ++ return IsOwner(package, user.AsUserSession()); + } + +- public static bool IsOwner(this PackageRegistration package, User user) ++ public static bool IsOwner(this PackageRegistration package, UserSession user) + { + if (package == null) + { diff --git a/src/NuGetGallery/Filters/ApiAuthorizeAttribute.cs b/src/NuGetGallery/Filters/ApiAuthorizeAttribute.cs new file mode 100644 index 0000000000..5604246831 --- /dev/null +++ b/src/NuGetGallery/Filters/ApiAuthorizeAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using NuGetGallery.Authentication; + +namespace NuGetGallery.Filters +{ + public sealed class ApiAuthorizeAttribute : AuthorizeAttribute + { + protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) + { + var owinContext = filterContext.HttpContext.GetOwinContext(); + owinContext.Authentication.Challenge(AuthenticationTypes.ApiKey); + owinContext.Response.StatusCode = 401; + filterContext.Result = new HttpUnauthorizedResult(); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Filters/ApiKeyAuthorizeAttribute.cs b/src/NuGetGallery/Filters/ApiKeyAuthorizeAttribute.cs deleted file mode 100644 index 1fa1c096ea..0000000000 --- a/src/NuGetGallery/Filters/ApiKeyAuthorizeAttribute.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Globalization; -using System.Net; -using System.Web.Mvc; -using Ninject; - -namespace NuGetGallery.Filters -{ - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] - public sealed class ApiKeyAuthorizeAttribute : ActionFilterAttribute - { - private IUserService _userService; // for tests - - public IUserService UserService - { - get { return _userService ?? Container.Kernel.TryGet(); } - set { _userService = value; } - } - - public override void OnActionExecuting(ActionExecutingContext filterContext) - { - if (filterContext == null) - { - throw new ArgumentNullException("filterContext"); - } - - var controller = filterContext.Controller; - string apiKeyStr = (string)((Controller)controller).RouteData.Values["apiKey"]; - filterContext.Result = CheckForResult(apiKeyStr); - } - - public ActionResult CheckForResult(string apiKeyStr) - { - if (String.IsNullOrEmpty(apiKeyStr)) - { - return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, String.Format(CultureInfo.CurrentCulture, Strings.InvalidApiKey, apiKeyStr)); - } - - Guid _; - if (!Guid.TryParse(apiKeyStr, out _)) - { - return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, String.Format(CultureInfo.CurrentCulture, Strings.InvalidApiKey, apiKeyStr)); - } - - User user = GetUserByApiKey(apiKeyStr); - if (user == null) - { - return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, String.Format(CultureInfo.CurrentCulture, Strings.ApiKeyNotAuthorized, "push")); - } - - if (!user.Confirmed) - { - return new HttpStatusCodeWithBodyResult(HttpStatusCode.Forbidden, Strings.ApiKeyUserAccountIsUnconfirmed); - } - - return null; - } - - // Temporary helper, not necessary after removing the old credential storage - private User GetUserByApiKey(string apiKey) - { - var cred = UserService.AuthenticateCredential(CredentialTypes.ApiKeyV1, apiKey.ToLowerInvariant()); - User user; - if (cred == null) - { -#pragma warning disable 0618 - user = UserService.FindByApiKey(Guid.Parse(apiKey)); -#pragma warning restore 0618 - } - else - { - user = cred.User; - } - return user; - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery/RequireRemoteHttpsAttribute.cs b/src/NuGetGallery/Filters/RequireSslAttribute.cs similarity index 69% rename from src/NuGetGallery/RequireRemoteHttpsAttribute.cs rename to src/NuGetGallery/Filters/RequireSslAttribute.cs index fe33220f56..e05b5a9866 100644 --- a/src/NuGetGallery/RequireRemoteHttpsAttribute.cs +++ b/src/NuGetGallery/Filters/RequireSslAttribute.cs @@ -7,29 +7,20 @@ using Ninject.Web.Mvc.Filter; using NuGetGallery.Configuration; -namespace NuGetGallery +namespace NuGetGallery.Filters { // This code is identical to System.Web.Mvc except that we allow for working in localhost environment without https and we force authenticated users to use SSL [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] - public sealed class RequireRemoteHttpsAttribute : FilterAttribute, IAuthorizationFilter + public sealed class RequireSslAttribute : FilterAttribute, IAuthorizationFilter { private IAppConfiguration _configuration; - private IFormsAuthenticationService _formsAuth; - + public IAppConfiguration Configuration { get { return _configuration ?? (_configuration = Container.Kernel.Get()); } set { _configuration = value; } } - public IFormsAuthenticationService FormsAuthentication - { - get { return _formsAuth ?? (_formsAuth = Container.Kernel.Get()); } - set { _formsAuth = value; } - } - - public bool OnlyWhenAuthenticated { get; set; } - public void OnAuthorization(AuthorizationContext filterContext) { if (filterContext == null) @@ -38,19 +29,12 @@ public void OnAuthorization(AuthorizationContext filterContext) } var request = filterContext.HttpContext.Request; - if (Configuration.RequireSSL && !request.IsSecureConnection && ShouldForceSSL(filterContext.HttpContext)) + if (Configuration.RequireSSL && !request.IsSecureConnection) { HandleNonHttpsRequest(filterContext); } } - private bool ShouldForceSSL(HttpContextBase context) - { - return !OnlyWhenAuthenticated || // If OnlyWhenAuthenticated == false, then we should force SSL - context.Request.IsAuthenticated || // If Authenticated, force SSL (we should already be on SSL, since the cookie is secure, but just in case...) - FormsAuthentication.ShouldForceSSL(context); // If the "ForceSSL" cookie is present. - } - private void HandleNonHttpsRequest(AuthorizationContext filterContext) { // only redirect for GET requests, otherwise the browser might not propagate the verb and request diff --git a/src/NuGetGallery/Filters/RequiresAccountConfirmationAttribute.cs b/src/NuGetGallery/Filters/RequiresAccountConfirmationAttribute.cs index 3ef883c307..b71496af5b 100644 --- a/src/NuGetGallery/Filters/RequiresAccountConfirmationAttribute.cs +++ b/src/NuGetGallery/Filters/RequiresAccountConfirmationAttribute.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Security.Principal; using System.Web.Mvc; +using NuGetGallery.Authentication; namespace NuGetGallery.Filters { @@ -22,16 +23,15 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) throw new ArgumentNullException("filterContext"); } - var controller = ((AppController)filterContext.Controller); - IPrincipal user = controller.User; - if (!user.Identity.IsAuthenticated) + if (!filterContext.HttpContext.Request.IsAuthenticated) { throw new InvalidOperationException("Requires account confirmation attribute is only valid on authenticated actions."); } - - var userService = controller.GetService(); - var currentUser = userService.FindByUsername(user.Identity.Name); - if (!currentUser.Confirmed) + + var controller = ((AppController)filterContext.Controller); + var user = controller.GetCurrentUser(); + + if (!user.Confirmed) { controller.TempData["ConfirmationRequiredMessage"] = String.Format( CultureInfo.CurrentCulture, diff --git a/src/NuGetGallery/Helpers/StatisticsHelper.cs b/src/NuGetGallery/Helpers/StatisticsHelper.cs index 754fcf2658..39c9049bb6 100644 --- a/src/NuGetGallery/Helpers/StatisticsHelper.cs +++ b/src/NuGetGallery/Helpers/StatisticsHelper.cs @@ -9,7 +9,7 @@ public static bool IsStatisticsPageAvailable get { var statistics = DependencyResolver.Current.GetService(); - return (statistics != null); + return (statistics != NullStatisticsService.Instance); } } } diff --git a/src/NuGetGallery/Infrastructure/CredentialBuilder.cs b/src/NuGetGallery/Infrastructure/CredentialBuilder.cs index 5b21ce2a5b..aed8395560 100644 --- a/src/NuGetGallery/Infrastructure/CredentialBuilder.cs +++ b/src/NuGetGallery/Infrastructure/CredentialBuilder.cs @@ -17,10 +17,7 @@ public static Credential CreateV1ApiKey() public static Credential CreateV1ApiKey(Guid apiKey) { - var value = apiKey - .ToString() - .ToLowerInvariant(); - return new Credential(CredentialTypes.ApiKeyV1, value); + return CreateV1ApiKey(apiKey.ToString()); } public static Credential CreatePbkdf2Password(string plaintextPassword) @@ -36,5 +33,10 @@ public static Credential CreateSha1Password(string plaintextPassword) CredentialTypes.Password.Sha1, CryptographyService.GenerateSaltedHash(plaintextPassword, Constants.Sha1HashAlgorithmId)); } + + internal static Credential CreateV1ApiKey(string apiKey) + { + return new Credential(CredentialTypes.ApiKeyV1, apiKey.ToLowerInvariant()); + } } } \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index eb767dd37c..e811d29908 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -28,8 +28,7 @@ 4.0 - - + 443 true enabled disabled @@ -117,6 +116,9 @@ ..\..\packages\WindowsAzure.Caching.1.7.0.0\lib\net35-full\Microsoft.ApplicationServer.Caching.Core.dll + + ..\..\packages\Microsoft.AspNet.Identity.Core.1.0.0-rc1\lib\net45\Microsoft.AspNet.Identity.Core.dll + @@ -135,6 +137,21 @@ False ..\..\packages\Microsoft.Data.Services.Client.5.5.0\lib\net40\Microsoft.Data.Services.Client.dll + + ..\..\packages\Microsoft.Owin.2.0.0-rc1\lib\net45\Microsoft.Owin.dll + + + ..\..\packages\Microsoft.Owin.Diagnostics.2.0.0-rc1\lib\net40\Microsoft.Owin.Diagnostics.dll + + + ..\..\packages\Microsoft.Owin.Host.SystemWeb.2.0.0-rc1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + ..\..\packages\Microsoft.Owin.Security.2.0.0-rc1\lib\net45\Microsoft.Owin.Security.dll + + + ..\..\packages\Microsoft.Owin.Security.Cookies.2.0.0-rc1\lib\net45\Microsoft.Owin.Security.Cookies.dll + ..\..\packages\WindowsAzure.Caching.1.7.0.0\lib\net35-full\Microsoft.Web.DistributedCache.dll @@ -186,6 +203,9 @@ ..\..\packages\ODataNullPropagationVisitor.0.5.4237.2641\lib\net40\ODataNullPropagationVisitor.dll + + ..\..\packages\Owin.1.0\lib\net40\Owin.dll + False ..\..\packages\PoliteCaptcha.0.4.0.0\lib\net40\PoliteCaptcha.dll @@ -290,11 +310,21 @@ T4MVC.tt + + + + + + + + + + @@ -303,6 +333,7 @@ + @@ -587,7 +618,7 @@ - + @@ -877,7 +908,7 @@ - + @@ -916,7 +947,6 @@ - @@ -1368,9 +1398,9 @@ - False + True True - 8085 + 80 / http://nuget.localtest.me False diff --git a/src/NuGetGallery/RouteNames.cs b/src/NuGetGallery/RouteNames.cs index 299503d661..f167a9d523 100644 --- a/src/NuGetGallery/RouteNames.cs +++ b/src/NuGetGallery/RouteNames.cs @@ -48,5 +48,6 @@ public static class RouteName public const string LegacyRegister = "LegacyRegister"; public const string PackageEnableLicenseReport = "EnableLicenseReport"; public const string PackageDisableLicenseReport = "DisableLicenseReport"; + public const string OwinRoute = "OwinRoute"; } } diff --git a/src/NuGetGallery/Services/ContentService.cs b/src/NuGetGallery/Services/ContentService.cs index 302cafee0d..23de4d1032 100644 --- a/src/NuGetGallery/Services/ContentService.cs +++ b/src/NuGetGallery/Services/ContentService.cs @@ -28,7 +28,7 @@ public class ContentService : IContentService protected ConcurrentDictionary ContentCache { get { return _contentCache; } } protected ContentService() { - Trace = new NullDiagnosticsSource(); + Trace = NullDiagnosticsSource.Instance; } public ContentService(IFileStorageService fileStorage, IDiagnosticsService diagnosticsService) diff --git a/src/NuGetGallery/Services/IReportService.cs b/src/NuGetGallery/Services/IReportService.cs index f90cc03f49..8816d66bb4 100644 --- a/src/NuGetGallery/Services/IReportService.cs +++ b/src/NuGetGallery/Services/IReportService.cs @@ -1,4 +1,5 @@  +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using NuGetGallery.Infrastructure; @@ -8,4 +9,17 @@ public interface IReportService { Task Load(string name); } + + public class NullReportService : IReportService + { + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Type is immutable")] + public static readonly NullReportService Instance = new NullReportService(); + + private NullReportService() { } + + public Task Load(string name) + { + return Task.FromResult(null); + } + } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/IStatisticsService.cs b/src/NuGetGallery/Services/IStatisticsService.cs index f1bdef2b7a..1d44c5b0ce 100644 --- a/src/NuGetGallery/Services/IStatisticsService.cs +++ b/src/NuGetGallery/Services/IStatisticsService.cs @@ -1,6 +1,8 @@  using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading.Tasks; namespace NuGetGallery @@ -22,4 +24,72 @@ public interface IStatisticsService Task GetPackageDownloadsByVersion(string packageId); Task GetPackageVersionDownloadsByClient(string packageId, string packageVersion); } + + public class NullStatisticsService : IStatisticsService + { + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Type is immutable")] + public static readonly NullStatisticsService Instance = new NullStatisticsService(); + + private NullStatisticsService() { } + + public IEnumerable DownloadPackagesSummary + { + get { return Enumerable.Empty(); } + } + + public IEnumerable DownloadPackageVersionsSummary + { + get { return Enumerable.Empty(); } + } + + public IEnumerable DownloadPackagesAll + { + get { return Enumerable.Empty(); } + } + + public IEnumerable DownloadPackageVersionsAll + { + get { return Enumerable.Empty(); } + } + + public IEnumerable NuGetClientVersion + { + get { return Enumerable.Empty(); } + } + + public IEnumerable Last6Months + { + get { return Enumerable.Empty(); } + } + + public Task LoadDownloadPackages() + { + return Task.FromResult(StatisticsReportResult.Failed); + } + + public Task LoadDownloadPackageVersions() + { + return Task.FromResult(StatisticsReportResult.Failed); + } + + public Task LoadNuGetClientVersion() + { + return Task.FromResult(StatisticsReportResult.Failed); + } + + public Task LoadLast6Months() + { + return Task.FromResult(StatisticsReportResult.Failed); + } + + public Task GetPackageDownloadsByVersion(string packageId) + { + return Task.FromResult(new StatisticsPackagesReport()); + } + + public Task GetPackageVersionDownloadsByClient(string packageId, string packageVersion) + { + return Task.FromResult(new StatisticsPackagesReport()); + } + } } diff --git a/src/NuGetGallery/Services/IUserService.cs b/src/NuGetGallery/Services/IUserService.cs index 147a57a576..60b5689b89 100644 --- a/src/NuGetGallery/Services/IUserService.cs +++ b/src/NuGetGallery/Services/IUserService.cs @@ -5,13 +5,8 @@ namespace NuGetGallery { public interface IUserService { - User Create(string username, string password, string emailAddress); - void UpdateProfile(User user, bool emailAllowed); - [Obsolete("Use AuthenticateCredential instead")] - User FindByApiKey(Guid apiKey); - User FindByEmailAddress(string emailAddress); IList FindAllByEmailAddress(string emailAddress); @@ -20,52 +15,8 @@ public interface IUserService User FindByUsername(string username); - User FindByUsernameAndPassword(string username, string password); - - User FindByUsernameOrEmailAddressAndPassword(string usernameOrEmail, string password); - - [Obsolete("Use ReplaceCredential instead")] - string GenerateApiKey(string username); - bool ConfirmEmailAddress(User user, string token); void ChangeEmailAddress(User user, string newEmailAddress); - - bool ChangePassword(string username, string oldPassword, string newPassword); - - User GeneratePasswordResetToken(string usernameOrEmail, int tokenExpirationMinutes); - - bool ResetPasswordWithToken(string username, string token, string newPassword); - - /// - /// Gets an authenticated credential, that is it returns a credential IF AND ONLY IF - /// one exists with exactly the specified type and value. - /// - /// The type of the credential, see - /// The value of the credential (such as an OAuth ID, API Key, etc.) - /// - /// null if there is no credential matching the request, or a - /// object WITH the associated object eagerly loaded if there is - /// a matching credential - /// - Credential AuthenticateCredential(string type, string value); - - /// - /// Creates a new credential for the specified user, overwriting the - /// previous credential of the same type, if any. Immediately saves - /// changes to the database. - /// - /// The name of the user to create a credential for - /// The credential to create - void ReplaceCredential(string userName, Credential credential); - - /// - /// Creates a new credential for the specified user, overwriting the - /// previous credential of the same type, if any. Immediately saves - /// changes to the database. - /// - /// The user object to create a credential for - /// The credential to create - void ReplaceCredential(User user, Credential credential); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index 727e687ffa..b4f382f07f 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -26,55 +26,6 @@ public UserService( CredentialRepository = credentialRepository; } - public virtual User Create( - string username, - string password, - string emailAddress) - { - // TODO: validate input - // TODO: consider encrypting email address with a public key, and having the background process that send messages have the private key to decrypt - - var existingUser = FindByUsername(username); - if (existingUser != null) - { - throw new EntityException(Strings.UsernameNotAvailable, username); - } - - var existingUsers = FindAllByEmailAddress(emailAddress); - if (existingUsers.AnySafe()) - { - throw new EntityException(Strings.EmailAddressBeingUsed, emailAddress); - } - - var hashedPassword = Crypto.GenerateSaltedHash(password, Constants.PBKDF2HashAlgorithmId); - - var apiKey = Guid.NewGuid(); - var newUser = new User(username) - { - ApiKey = apiKey, - EmailAllowed = true, - UnconfirmedEmailAddress = emailAddress, - EmailConfirmationToken = Crypto.GenerateToken(), - HashedPassword = hashedPassword, - PasswordHashAlgorithm = Constants.PBKDF2HashAlgorithmId, - CreatedUtc = DateTime.UtcNow - }; - - // Add a credential for the password and the API Key - newUser.Credentials.Add(CredentialBuilder.CreateV1ApiKey(apiKey)); - newUser.Credentials.Add(new Credential(CredentialTypes.Password.Pbkdf2, newUser.HashedPassword)); - - if (!Config.ConfirmEmailAddresses) - { - newUser.ConfirmEmailAddress(); - } - - UserRepository.InsertOnCommit(newUser); - UserRepository.CommitChanges(); - - return newUser; - } - public void UpdateProfile(User user, bool emailAllowed) { if (user == null) @@ -86,12 +37,6 @@ public void UpdateProfile(User user, bool emailAllowed) UserRepository.CommitChanges(); } - [Obsolete("Use AuthenticateCredential instead")] - public User FindByApiKey(Guid apiKey) - { - return UserRepository.GetAll().SingleOrDefault(u => u.ApiKey == apiKey); - } - public virtual User FindByEmailAddress(string emailAddress) { var allMatches = UserRepository.GetAll() @@ -134,35 +79,6 @@ public virtual User FindByUsername(string username) .SingleOrDefault(u => u.Username == username); } - public virtual User FindByUsernameAndPassword(string username, string password) - { - var user = FindByUsername(username); - - return AuthenticatePassword(password, user); - } - - public virtual User FindByUsernameOrEmailAddressAndPassword(string usernameOrEmail, string password) - { - var user = FindByUsername(usernameOrEmail) ?? FindByEmailAddress(usernameOrEmail); - - return AuthenticatePassword(password, user); - } - - [Obsolete("Use ReplaceCredential instead")] - public string GenerateApiKey(string username) - { - var user = FindByUsername(username); - if (user == null) - { - return null; - } - - var newApiKey = Guid.NewGuid(); - user.ApiKey = newApiKey; - UserRepository.CommitChanges(); - return newApiKey.ToString(); - } - public void ChangeEmailAddress(User user, string newEmailAddress) { var existingUsers = FindAllByEmailAddress(newEmailAddress); @@ -175,21 +91,6 @@ public void ChangeEmailAddress(User user, string newEmailAddress) UserRepository.CommitChanges(); } - public bool ChangePassword(string username, string oldPassword, string newPassword) - { - // Review: If the old password is hashed using something other than PBKDF2, we end up making an extra db call that changes the old hash password. - // This operation is rare enough that I'm not inclined to change it. - var user = FindByUsernameAndPassword(username, oldPassword); - if (user == null) - { - return false; - } - - ChangePasswordInternal(user, newPassword); - UserRepository.CommitChanges(); - return true; - } - public bool ConfirmEmailAddress(User user, string token) { if (user == null) @@ -218,201 +119,5 @@ public bool ConfirmEmailAddress(User user, string token) UserRepository.CommitChanges(); return true; } - - public virtual User GeneratePasswordResetToken(string usernameOrEmail, int tokenExpirationMinutes) - { - if (String.IsNullOrEmpty(usernameOrEmail)) - { - throw new ArgumentNullException("usernameOrEmail"); - } - if (tokenExpirationMinutes < 1) - { - throw new ArgumentException( - "Token expiration should give the user at least a minute to change their password", "tokenExpirationMinutes"); - } - - var user = FindByEmailAddress(usernameOrEmail); - if (user == null) - { - return null; - } - - if (!user.Confirmed) - { - throw new InvalidOperationException(Strings.UserIsNotYetConfirmed); - } - - if (!String.IsNullOrEmpty(user.PasswordResetToken) && !user.PasswordResetTokenExpirationDate.IsInThePast()) - { - return user; - } - - user.PasswordResetToken = Crypto.GenerateToken(); - user.PasswordResetTokenExpirationDate = DateTime.UtcNow.AddMinutes(tokenExpirationMinutes); - - UserRepository.CommitChanges(); - return user; - } - - public bool ResetPasswordWithToken(string username, string token, string newPassword) - { - if (String.IsNullOrEmpty(newPassword)) - { - throw new ArgumentNullException("newPassword"); - } - - var user = FindByUsername(username); - - if (user != null && user.PasswordResetToken == token && !user.PasswordResetTokenExpirationDate.IsInThePast()) - { - if (!user.Confirmed) - { - throw new InvalidOperationException(Strings.UserIsNotYetConfirmed); - } - - ChangePasswordInternal(user, newPassword); - user.PasswordResetToken = null; - user.PasswordResetTokenExpirationDate = null; - UserRepository.CommitChanges(); - return true; - } - - return false; - } - - public Credential AuthenticateCredential(string type, string value) - { - // Search for the cred - return CredentialRepository - .GetAll() - .Include(c => c.User) - .SingleOrDefault(c => c.Type == type && c.Value == value); - } - - public void ReplaceCredential(string userName, Credential credential) - { - var user = UserRepository - .GetAll() - .Include(u => u.Credentials) - .SingleOrDefault(u => u.Username == userName); - if (user == null) - { - throw new InvalidOperationException(Strings.UserNotFound); - } - ReplaceCredential(user, credential); - } - - public void ReplaceCredential(User user, Credential credential) - { - ReplaceCredentialInternal(user, credential); - UserRepository.CommitChanges(); - } - - private User AuthenticatePassword(string password, User user) - { - if (user == null) - { - return null; - } - - // Check for a credential - var creds = user.Credentials - .Where(c => c.Type.StartsWith( - CredentialTypes.Password.Prefix, - StringComparison.OrdinalIgnoreCase)).ToList(); - - bool valid; - if (creds.Count > 0) - { - valid = ValidatePasswordCredential(creds, password); - - if (valid && - (creds.Count > 1 || - !creds.Any(c => String.Equals( - c.Type, - CredentialTypes.Password.Pbkdf2, - StringComparison.OrdinalIgnoreCase)))) - { - MigrateCredentials(user, creds, password); - } - } - else - { - valid = Crypto.ValidateSaltedHash( - user.HashedPassword, - password, - user.PasswordHashAlgorithm); - } - - return valid ? user : null; - } - - private void MigrateCredentials(User user, List creds, string password) - { - var toRemove = creds.Where(c => - !String.Equals( - c.Type, - CredentialTypes.Password.Pbkdf2, - StringComparison.OrdinalIgnoreCase)) - .ToList(); - - // Remove any non PBKDF2 credentials - foreach (var cred in toRemove) - { - creds.Remove(cred); - user.Credentials.Remove(cred); - } - - // Now add one if there are no credentials left - if (creds.Count == 0) - { - user.Credentials.Add(CredentialBuilder.CreatePbkdf2Password(password)); - } - - // Save changes, if any - UserRepository.CommitChanges(); - } - - private static bool ValidatePasswordCredential(IEnumerable creds, string password) - { - return creds.Any(c => ValidatePasswordCredential(c, password)); - } - - private static readonly Dictionary> _validators = new Dictionary>(StringComparer.OrdinalIgnoreCase) { - { CredentialTypes.Password.Pbkdf2, (password, cred) => Crypto.ValidateSaltedHash(cred.Value, password, Constants.PBKDF2HashAlgorithmId) }, - { CredentialTypes.Password.Sha1, (password, cred) => Crypto.ValidateSaltedHash(cred.Value, password, Constants.Sha1HashAlgorithmId) } - }; - private static bool ValidatePasswordCredential(Credential cred, string password) - { - Func validator; - if (!_validators.TryGetValue(cred.Type, out validator)) - { - return false; - } - return validator(password, cred); - } - - private void ChangePasswordInternal(User user, string newPassword) - { - var cred = CredentialBuilder.CreatePbkdf2Password(newPassword); - user.PasswordHashAlgorithm = Constants.PBKDF2HashAlgorithmId; - user.HashedPassword = cred.Value; - ReplaceCredentialInternal(user, cred); - } - - private void ReplaceCredentialInternal(User user, Credential credential) - { - // Find the credentials we're replacing, if any - var creds = user.Credentials - .Where(cred => cred.Type == credential.Type) - .ToList(); - foreach (var cred in creds) - { - user.Credentials.Remove(cred); - CredentialRepository.DeleteOnCommit(cred); - } - - user.Credentials.Add(credential); - } } } diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index c3627b7e59..7e42a1e890 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18052 +// Runtime Version:4.0.30319.34003 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -61,7 +61,7 @@ internal Strings() { } /// - /// Looks up a localized string similar to The specified API key does not provide the authority to {0} packages.. + /// Looks up a localized string similar to The specified API key is invalid or does not have permission to access the specified package.. /// public static string ApiKeyNotAuthorized { get { @@ -69,6 +69,15 @@ public static string ApiKeyNotAuthorized { } } + /// + /// 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 ApiKeyRequired { + get { + return ResourceManager.GetString("ApiKeyRequired", resourceCulture); + } + } + /// /// Looks up a localized string similar to You must confirm the email address for the account in order to use the API key.. /// @@ -123,6 +132,15 @@ public static string InvalidApiKey { } } + /// + /// Looks up a localized string similar to Multiple Credentials match '{0}' credential with Key {1}. + /// + public static string MultipleMatchingCredentials { + get { + return ResourceManager.GetString("MultipleMatchingCredentials", resourceCulture); + } + } + /// /// Looks up a localized string similar to A nuget package's {0} property may not be more than {1} characters long.. /// @@ -186,6 +204,15 @@ public static string ParameterCannotBeNullOrEmpty { } } + /// + /// Looks up a localized string similar to Password credentials cannot be used with Authenticate(Credential). Use Authenticate(string, string) instead.. + /// + public static string PasswordCredentialsCannotBeUsedHere { + get { + return ResourceManager.GetString("PasswordCredentialsCannotBeUsedHere", resourceCulture); + } + } + /// /// Looks up a localized string similar to The requested resource can only be accessed via SSL.. /// @@ -204,6 +231,15 @@ public static string SuccessfullyUploadedPackage { } } + /// + /// Looks up a localized string similar to User is not authorized. + /// + public static string Unauthorized { + get { + return ResourceManager.GetString("Unauthorized", resourceCulture); + } + } + /// /// Looks up a localized string similar to A package file is required.. /// @@ -232,7 +268,7 @@ public static string UserIsNotYetConfirmed { } /// - /// Looks up a localized string similar to A user with the provided user name and password does not exist. Try logging on with your username if you were using an email address to log on.. + /// Looks up a localized string similar to A unique user with that username or email address and password does not exist. Try logging on with your username if you were using an email address to log on.. /// public static string UsernameAndPasswordNotFound { get { diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index 1a78062a04..0cb76301fd 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -136,7 +136,7 @@ A nuget package's {0} property may not be more than {1} characters long. - The specified API key does not provide the authority to {0} packages. + The specified API key is invalid or does not have permission to access the specified package. A package with id '{0}' and version '{1}' already exists and cannot be modified. @@ -183,4 +183,16 @@ You must confirm the email address for the account in order to use the API key. - + + Multiple Credentials match '{0}' credential with Key {1} + + + Password credentials cannot be used with Authenticate(Credential). Use Authenticate(string, string) instead. + + + An API key must be provided in the 'X-NuGet-ApiKey' header to use this service + + + User is not authorized + + \ No newline at end of file diff --git a/src/NuGetGallery/ViewModels/StatisticsPackagesReport.cs b/src/NuGetGallery/ViewModels/StatisticsPackagesReport.cs index e15485985b..1c8a92878a 100644 --- a/src/NuGetGallery/ViewModels/StatisticsPackagesReport.cs +++ b/src/NuGetGallery/ViewModels/StatisticsPackagesReport.cs @@ -1,18 +1,16 @@  using System; using System.Collections.Generic; +using System.Linq; namespace NuGetGallery { public class StatisticsPackagesReport { - private List _rows = new List(); - private List _dimensions = new List(); - - public IList Rows { get { return _rows; } } + public IList Rows { get; private set; } public string Total { get; set; } - public IList Dimensions { get { return _dimensions; } } + public IList Dimensions { get; private set; } public IList Facts { get; set; } public ICollection Table { get; set; } @@ -21,5 +19,16 @@ public class StatisticsPackagesReport public DateTime? LastUpdatedUtc { get; set; } public string Id { get; set; } + + public StatisticsPackagesReport() + { + Total = String.Empty; + Id = String.Empty; + Columns = Enumerable.Empty(); + Facts = new List(); + Table = new List(); + Rows = new List(); + Dimensions = new List(); + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Web.config b/src/NuGetGallery/Web.config index f8c2f11076..f6b296a77a 100644 --- a/src/NuGetGallery/Web.config +++ b/src/NuGetGallery/Web.config @@ -39,10 +39,6 @@ - - -