From d355abbc1da0591a67778bb867a8275525bb2e34 Mon Sep 17 00:00:00 2001 From: anurse Date: Tue, 17 Sep 2013 15:18:46 -0700 Subject: [PATCH 01/13] Added Credential entity --- src/NuGetGallery.Core/Entities/Credential.cs | 25 ++++ .../Entities/EntitiesContext.cs | 6 + src/NuGetGallery.Core/Entities/User.cs | 2 + .../NuGetGallery.Core.csproj | 1 + ...1309172217450_CredentialsTable.Designer.cs | 27 ++++ .../201309172217450_CredentialsTable.cs | 33 +++++ .../201309172217450_CredentialsTable.resx | 123 ++++++++++++++++++ src/NuGetGallery/NuGetGallery.csproj | 7 + 8 files changed, 224 insertions(+) create mode 100644 src/NuGetGallery.Core/Entities/Credential.cs create mode 100644 src/NuGetGallery/Migrations/201309172217450_CredentialsTable.Designer.cs create mode 100644 src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs create mode 100644 src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx diff --git a/src/NuGetGallery.Core/Entities/Credential.cs b/src/NuGetGallery.Core/Entities/Credential.cs new file mode 100644 index 0000000000..9111b2f9f0 --- /dev/null +++ b/src/NuGetGallery.Core/Entities/Credential.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; + +namespace NuGetGallery +{ + public class Credential : IEntity + { + public int Key { get; set; } + public int UserKey { get; set; } + + [StringLength(maximumLength: 64)] + public string Type { get; set; } + + [StringLength(maximumLength: 256)] + public string Identifier { get; set; } + + [StringLength(maximumLength: 256)] + public string Value { get; set; } + + public virtual User User { get; set; } + } +} diff --git a/src/NuGetGallery.Core/Entities/EntitiesContext.cs b/src/NuGetGallery.Core/Entities/EntitiesContext.cs index a07a773775..60abfab867 100644 --- a/src/NuGetGallery.Core/Entities/EntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/EntitiesContext.cs @@ -55,6 +55,12 @@ public void DeleteOnCommit(T entity) where T : class #pragma warning disable 618 // TODO: remove Package.Authors completely once prodution services definitely no longer need it protected override void OnModelCreating(DbModelBuilder modelBuilder) { + modelBuilder.Entity() + .HasKey(c => c.Key) + .HasRequired(c => c.User) + .WithMany(u => u.Credentials) + .HasForeignKey(c => c.UserKey); + modelBuilder.Entity() .HasKey(r => r.Key) .HasMany(r => r.Licenses) diff --git a/src/NuGetGallery.Core/Entities/User.cs b/src/NuGetGallery.Core/Entities/User.cs index a20bb470e9..2464da63d7 100644 --- a/src/NuGetGallery.Core/Entities/User.cs +++ b/src/NuGetGallery.Core/Entities/User.cs @@ -59,6 +59,8 @@ public bool Confirmed public DateTime? CreatedUtc { get; set; } + public virtual ICollection Credentials { get; set; } + public void ConfirmEmailAddress() { if (String.IsNullOrEmpty(UnconfirmedEmailAddress)) diff --git a/src/NuGetGallery.Core/NuGetGallery.Core.csproj b/src/NuGetGallery.Core/NuGetGallery.Core.csproj index 4c87fc786f..3e34de5abb 100644 --- a/src/NuGetGallery.Core/NuGetGallery.Core.csproj +++ b/src/NuGetGallery.Core/NuGetGallery.Core.csproj @@ -58,6 +58,7 @@ Properties\CommonAssemblyInfo.cs + diff --git a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.Designer.cs b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.Designer.cs new file mode 100644 index 0000000000..46059acde7 --- /dev/null +++ b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.Designer.cs @@ -0,0 +1,27 @@ +// +namespace NuGetGallery.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + public sealed partial class CredentialsTable : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CredentialsTable)); + + string IMigrationMetadata.Id + { + get { return "201309172217450_CredentialsTable"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs new file mode 100644 index 0000000000..6cf2a2177b --- /dev/null +++ b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs @@ -0,0 +1,33 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class CredentialsTable : DbMigration + { + public override void Up() + { + CreateTable( + "dbo.Credentials", + c => new + { + Key = c.Int(nullable: false, identity: true), + UserKey = c.Int(nullable: false), + Type = c.String(maxLength: 64), + Identifier = c.String(maxLength: 256), + Value = c.String(maxLength: 256), + }) + .PrimaryKey(t => t.Key) + .ForeignKey("dbo.Users", t => t.UserKey, cascadeDelete: true) + .Index(t => t.UserKey); + + } + + public override void Down() + { + DropIndex("dbo.Credentials", new[] { "UserKey" }); + DropForeignKey("dbo.Credentials", "UserKey", "dbo.Users"); + DropTable("dbo.Credentials"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx new file mode 100644 index 0000000000..8958816ed4 --- /dev/null +++ b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index 90641ef87c..acf70ef127 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -803,6 +803,10 @@ 201309092041546_AddPackageLicenseReportSproc.cs + + + 201309172217450_CredentialsTable.cs + @@ -1260,6 +1264,9 @@ 201309092041546_AddPackageLicenseReportSproc.cs + + 201309172217450_CredentialsTable.cs + PublicResXFileCodeGenerator Strings.Designer.cs From 398aeecab8f9039475f8f2e6b3f95efbc76d2860 Mon Sep 17 00:00:00 2001 From: anurse Date: Tue, 17 Sep 2013 16:09:13 -0700 Subject: [PATCH 02/13] Rewrote FindByUserNameAndPassword to use creds --- src/NuGetGallery.Core/Entities/Credential.cs | 13 +++ src/NuGetGallery.Core/Entities/User.cs | 1 + src/NuGetGallery/Constants.cs | 9 +- src/NuGetGallery/Services/UserService.cs | 56 +++++++++--- .../NuGetGallery.Core.Facts.csproj | 3 + .../NuGetGallery.Facts.csproj | 3 + .../Services/UserServiceFacts.cs | 86 +++++++++++++++++-- 7 files changed, 150 insertions(+), 21 deletions(-) diff --git a/src/NuGetGallery.Core/Entities/Credential.cs b/src/NuGetGallery.Core/Entities/Credential.cs index 9111b2f9f0..02d552138f 100644 --- a/src/NuGetGallery.Core/Entities/Credential.cs +++ b/src/NuGetGallery.Core/Entities/Credential.cs @@ -8,6 +8,19 @@ namespace NuGetGallery { public class Credential : IEntity { + public Credential() { } + + public Credential(string type, string value) + { + Type = type; + Value = value; + } + + public Credential(string type, string identifier, string value) : this(type, value) + { + Identifier = identifier; + } + public int Key { get; set; } public int UserKey { get; set; } diff --git a/src/NuGetGallery.Core/Entities/User.cs b/src/NuGetGallery.Core/Entities/User.cs index 2464da63d7..72ad48fa2a 100644 --- a/src/NuGetGallery.Core/Entities/User.cs +++ b/src/NuGetGallery.Core/Entities/User.cs @@ -17,6 +17,7 @@ public User( { HashedPassword = hashedPassword; Messages = new HashSet(); + Credentials = new List(); Username = username; } diff --git a/src/NuGetGallery/Constants.cs b/src/NuGetGallery/Constants.cs index 4377d8e20a..7635f77961 100644 --- a/src/NuGetGallery/Constants.cs +++ b/src/NuGetGallery/Constants.cs @@ -32,6 +32,9 @@ public static class Constants public static readonly string ReturnUrlViewDataKey = "ReturnUrl"; + public const string UrlValidationRegEx = @"(https?):\/\/[^ ""]+$"; + public const string UrlValidationErrorMessage = "This doesn't appear to be a valid HTTP/HTTPS URL"; + public static class ContentNames { public static readonly string FrontPageAnnouncement = "FrontPage-Announcement"; @@ -43,7 +46,9 @@ public static class ContentNames public static readonly string PrivacyPolicy = "Privacy-Policy"; } - public const string UrlValidationRegEx = @"(https?):\/\/[^ ""]+$"; - public const string UrlValidationErrorMessage = "This doesn't appear to be a valid HTTP/HTTPS URL"; + public static class CredentialTypes + { + public static readonly string PasswordPbkdf2 = "password.pbkdf2"; + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index 4a0d51db84..47495eb94d 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -11,15 +11,18 @@ public class UserService : IUserService { public IAppConfiguration Config { get; protected set; } public IEntityRepository UserRepository { get; protected set; } + public IEntityRepository CredentialRepository { get; protected set; } protected UserService() {} public UserService( IAppConfiguration config, - IEntityRepository userRepository) : this() + IEntityRepository userRepository, + IEntityRepository credentialRepository) : this() { Config = config; UserRepository = userRepository; + CredentialRepository = credentialRepository; } public virtual User Create( @@ -121,20 +124,12 @@ public virtual User FindByUsername(string username) public virtual User FindByUsernameAndPassword(string username, string password) { // TODO: validate input + var user = UserRepository.GetAll() + .Include(u => u.Roles) + .Include(u => u.Credentials) + .SingleOrDefault(u => u.Username == username); - var user = FindByUsername(username); - - if (user == null) - { - return null; - } - - if (!Crypto.ValidateSaltedHash(user.HashedPassword, password, user.PasswordHashAlgorithm)) - { - return null; - } - - return user; + return AuthenticateUser(password, user); } public virtual User FindByUsernameOrEmailAddressAndPassword(string usernameOrEmail, string password) @@ -276,6 +271,39 @@ public bool ResetPasswordWithToken(string username, string token, string newPass return false; } + private static User AuthenticateUser(string password, User user) + { + if (user == null) + { + return null; + } + + // Check for a credential + var cred = user.Credentials + .FirstOrDefault(c => String.Equals( + c.Type, + Constants.CredentialTypes.PasswordPbkdf2, + StringComparison.OrdinalIgnoreCase)); + + bool valid; + if (cred != null) + { + valid = Crypto.ValidateSaltedHash( + cred.Value, + password, + Constants.PBKDF2HashAlgorithmId); + } + else + { + valid = Crypto.ValidateSaltedHash( + user.HashedPassword, + password, + user.PasswordHashAlgorithm); + } + + return valid ? user : null; + } + private static void ChangePasswordInternal(User user, string newPassword) { var hashedPassword = Crypto.GenerateSaltedHash(newPassword, Constants.PBKDF2HashAlgorithmId); diff --git a/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj b/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj index c030a9e984..0331756346 100644 --- a/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj +++ b/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj @@ -94,6 +94,9 @@ Designer + + + diff --git a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj index acede600b9..9d60004ab6 100644 --- a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj +++ b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj @@ -251,6 +251,9 @@ NuGetGallery + + + diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 29fecb3739..fe22cffed3 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Moq; using NuGetGallery.Configuration; @@ -8,6 +9,13 @@ namespace NuGetGallery { public class UserServiceFacts { + public static User CreateAUser( + string username, + string emailAddress) + { + return CreateAUser(username, password: null, emailAddress: emailAddress); + } + public static User CreateAUser( string username, string password, @@ -16,8 +24,12 @@ public static User CreateAUser( return new User { Username = username, - HashedPassword = CryptographyService.GenerateSaltedHash(password, Constants.PBKDF2HashAlgorithmId), - PasswordHashAlgorithm = Constants.PBKDF2HashAlgorithmId, + HashedPassword = String.IsNullOrEmpty(password) ? + null : + CryptographyService.GenerateSaltedHash(password, Constants.PBKDF2HashAlgorithmId), + PasswordHashAlgorithm = String.IsNullOrEmpty(password) ? + null : + Constants.PBKDF2HashAlgorithmId, EmailAddress = emailAddress, }; } @@ -37,6 +49,15 @@ public static bool VerifyPasswordHash(User user, string password) return canAuthenticate && !sanity; } + public static Credential CreatePasswordCredential(string password) + { + return new Credential( + type: Constants.CredentialTypes.PasswordPbkdf2, + value: CryptographyService.GenerateSaltedHash( + password, + Constants.PBKDF2HashAlgorithmId)); + } + // Now only for things that actually need a MOCK UserService object. private static UserService CreateMockUserService(Action> setup, Mock> userRepo = null, Mock config = null) { @@ -47,10 +68,12 @@ private static UserService CreateMockUserService(Action> setup } userRepo = userRepo ?? new Mock>(); + var credRepo = new Mock>(); var userService = new Mock( config.Object, - userRepo.Object) + userRepo.Object, + credRepo.Object) { CallBase = true }; @@ -404,8 +427,7 @@ public void FindsUsersByUserName() [Fact] public void WillNotFindsUsersByEmailAddress() { - var hash = CryptographyService.GenerateSaltedHash("thePassword", Constants.PBKDF2HashAlgorithmId); - var user = new User { Username = "theUsername", HashedPassword = hash, EmailAddress = "test@example.com" }; + var user = CreateAUser("theUsername", "thePassword", "test@example.com"); var service = new TestableUserService(); service.MockUserRepository .Setup(r => r.GetAll()) @@ -415,6 +437,58 @@ public void WillNotFindsUsersByEmailAddress() Assert.Null(foundByEmailAddress); } + + [Fact] + public void DoesNotReturnUserIfPasswordIsInvalid() + { + var user = CreateAUser("theUsername", "thePassword", "test@example.com"); + var service = new TestableUserService(); + service.MockUserRepository + .Setup(r => r.GetAll()) + .Returns(new[] { user }.AsQueryable()); + + var foundByUserName = service.FindByUsernameAndPassword("theUsername", "theWrongPassword"); + + Assert.Null(foundByUserName); + } + + [Fact] + public void FindsUserBasedOnPasswordInCredentialsTable() + { + var user = CreateAUser("theUsername", "test@example.com"); + user.Credentials.Add(CreatePasswordCredential("thePassword")); + var service = new TestableUserService(); + service.MockUserRepository + .Setup(u => u.GetAll()) + .Returns(new[] { user }.AsQueryable()); + service.MockCredentialRepository + .Setup(c => c.GetAll()) + .Returns(user.Credentials.AsQueryable()); + + var foundByUserName = service.FindByUsernameAndPassword("theUsername", "thePassword"); + + Assert.NotNull(foundByUserName); + Assert.Same(user, foundByUserName); + } + + [Fact] + public void IfSomehowBothPasswordsExistItFindsUserBasedOnPasswordInCredentialsTable() + { + var user = CreateAUser("theUsername", "theWrongPassword", "test@example.com"); + user.Credentials.Add(CreatePasswordCredential("thePassword")); + var service = new TestableUserService(); + service.MockUserRepository + .Setup(u => u.GetAll()) + .Returns(new[] { user }.AsQueryable()); + service.MockCredentialRepository + .Setup(c => c.GetAll()) + .Returns(user.Credentials.AsQueryable()); + + var foundByUserName = service.FindByUsernameAndPassword("theUsername", "thePassword"); + + Assert.NotNull(foundByUserName); + Assert.Same(user, foundByUserName); + } } public class TheFindByUsernameOrEmailAddressAndPasswordMethod @@ -775,11 +849,13 @@ public class TestableUserService : UserService { public Mock MockConfig { get; protected set; } public Mock> MockUserRepository { get; protected set; } + public Mock> MockCredentialRepository { get; protected set; } public TestableUserService() { Config = (MockConfig = new Mock()).Object; UserRepository = (MockUserRepository = new Mock>()).Object; + CredentialRepository = (MockCredentialRepository = new Mock>()).Object; // Set ConfirmEmailAddress to a default of true MockConfig.Setup(c => c.ConfirmEmailAddresses).Returns(true); From 467ef53fb0f6c2775265d661fc4ceaff431ac2e3 Mon Sep 17 00:00:00 2001 From: anurse Date: Tue, 17 Sep 2013 16:13:42 -0700 Subject: [PATCH 03/13] Rewrote FindByUserNameOrEmailAndPassword --- src/NuGetGallery/Services/UserService.cs | 27 +++-------- .../Services/UserServiceFacts.cs | 45 ++++++++++++------- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index 47495eb94d..09b2a5d75b 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -123,7 +123,6 @@ public virtual User FindByUsername(string username) public virtual User FindByUsernameAndPassword(string username, string password) { - // TODO: validate input var user = UserRepository.GetAll() .Include(u => u.Roles) .Include(u => u.Credentials) @@ -134,28 +133,12 @@ public virtual User FindByUsernameAndPassword(string username, string password) public virtual User FindByUsernameOrEmailAddressAndPassword(string usernameOrEmail, string password) { - // TODO: validate input - - var user = FindByUsername(usernameOrEmail) - ?? FindByEmailAddress(usernameOrEmail); - - if (user == null) - { - return null; - } - - if (!Crypto.ValidateSaltedHash(user.HashedPassword, password, user.PasswordHashAlgorithm)) - { - return null; - } - else if (!user.PasswordHashAlgorithm.Equals(Constants.PBKDF2HashAlgorithmId, StringComparison.OrdinalIgnoreCase)) - { - // If the user can be authenticated and they are using an older password algorithm, migrate them to the current one. - ChangePasswordInternal(user, password); - UserRepository.CommitChanges(); - } + var user = UserRepository.GetAll() + .Include(u => u.Roles) + .Include(u => u.Credentials) + .SingleOrDefault(u => u.Username == usernameOrEmail || u.EmailAddress == usernameOrEmail); - return user; + return AuthenticateUser(password, user); } public string GenerateApiKey(string username) diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index fe22cffed3..9cedeff5a1 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -536,28 +536,41 @@ public void FindsUsersByEmailAddress() } [Fact] - public void FindsUsersUpdatesPasswordIfUsingLegacyHashAlgorithm() + public void FindsUserBasedOnPasswordInCredentialsTable() { - var user = new User - { - Username = "theUsername", - HashedPassword = CryptographyService.GenerateSaltedHash("thePassword", "SHA1"), - PasswordHashAlgorithm = "SHA1", - EmailAddress = "test@example.com", - }; - + var user = CreateAUser("theUsername", "test@example.com"); + user.Credentials.Add(CreatePasswordCredential("thePassword")); var service = new TestableUserService(); service.MockUserRepository - .Setup(r => r.GetAll()) + .Setup(u => u.GetAll()) .Returns(new[] { user }.AsQueryable()); + service.MockCredentialRepository + .Setup(c => c.GetAll()) + .Returns(user.Credentials.AsQueryable()); + + var foundByUserName = service.FindByUsernameOrEmailAddressAndPassword("test@example.com", "thePassword"); + + Assert.NotNull(foundByUserName); + Assert.Same(user, foundByUserName); + } + + [Fact] + public void IfSomehowBothPasswordsExistItFindsUserBasedOnPasswordInCredentialsTable() + { + var user = CreateAUser("theUsername", "theWrongPassword", "test@example.com"); + user.Credentials.Add(CreatePasswordCredential("thePassword")); + var service = new TestableUserService(); service.MockUserRepository - .Setup(r => r.CommitChanges()) - .Verifiable(); + .Setup(u => u.GetAll()) + .Returns(new[] { user }.AsQueryable()); + service.MockCredentialRepository + .Setup(c => c.GetAll()) + .Returns(user.Credentials.AsQueryable()); - service.FindByUsernameOrEmailAddressAndPassword("test@example.com", "thePassword"); - Assert.Equal("PBKDF2", user.PasswordHashAlgorithm); - Assert.True(VerifyPasswordHash(user, "thePassword")); - service.MockUserRepository.Verify(r => r.CommitChanges(), Times.Once()); + var foundByUserName = service.FindByUsernameOrEmailAddressAndPassword("test@example.com", "thePassword"); + + Assert.NotNull(foundByUserName); + Assert.Same(user, foundByUserName); } } From 37bd44d2100d55cfcfbf091d33158526cfc720f4 Mon Sep 17 00:00:00 2001 From: anurse Date: Wed, 18 Sep 2013 11:31:38 -0700 Subject: [PATCH 04/13] Added AuthenticateCredential service method --- src/NuGetGallery.Core/Entities/Credential.cs | 9 ++-- .../201309172217450_CredentialsTable.cs | 12 +++-- .../201309172217450_CredentialsTable.resx | 2 +- src/NuGetGallery/Services/IUserService.cs | 13 +++++ src/NuGetGallery/Services/UserService.cs | 15 +++++- .../Services/UserServiceFacts.cs | 52 ++++++++++--------- .../TestUtils/MockExtensions.cs | 17 +++++- 7 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/NuGetGallery.Core/Entities/Credential.cs b/src/NuGetGallery.Core/Entities/Credential.cs index 02d552138f..edeebfb766 100644 --- a/src/NuGetGallery.Core/Entities/Credential.cs +++ b/src/NuGetGallery.Core/Entities/Credential.cs @@ -16,20 +16,19 @@ public Credential(string type, string value) Value = value; } - public Credential(string type, string identifier, string value) : this(type, value) - { - Identifier = identifier; - } - public int Key { get; set; } + + [Required] public int UserKey { get; set; } + [Required] [StringLength(maximumLength: 64)] public string Type { get; set; } [StringLength(maximumLength: 256)] public string Identifier { get; set; } + [Required] [StringLength(maximumLength: 256)] public string Value { get; set; } diff --git a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs index 6cf2a2177b..f260c098d4 100644 --- a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs +++ b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs @@ -13,19 +13,25 @@ public override void Up() { Key = c.Int(nullable: false, identity: true), UserKey = c.Int(nullable: false), - Type = c.String(maxLength: 64), + Type = c.String(nullable: false, maxLength: 64), Identifier = c.String(maxLength: 256), - Value = c.String(maxLength: 256), + Value = c.String(nullable: false, maxLength: 256), }) .PrimaryKey(t => t.Key) .ForeignKey("dbo.Users", t => t.UserKey, cascadeDelete: true) .Index(t => t.UserKey); - + + CreateIndex( + "dbo.Credentials", + new[] { "Type", "Value" }, + unique: true, + name: "IX_Credentials_Type_Value"); } public override void Down() { DropIndex("dbo.Credentials", new[] { "UserKey" }); + DropIndex("dbo.Credentials", "IX_Credentials_Type_Value"); DropForeignKey("dbo.Credentials", "UserKey", "dbo.Users"); DropTable("dbo.Credentials"); } diff --git a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx index 8958816ed4..dabe134f2a 100644 --- a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx +++ b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx @@ -118,6 +118,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 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 \ No newline at end of file diff --git a/src/NuGetGallery/Services/IUserService.cs b/src/NuGetGallery/Services/IUserService.cs index c4105cab24..a9de91da9c 100644 --- a/src/NuGetGallery/Services/IUserService.cs +++ b/src/NuGetGallery/Services/IUserService.cs @@ -30,5 +30,18 @@ public interface IUserService 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); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index 09b2a5d75b..3eec59ecbe 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -13,12 +13,13 @@ public class UserService : IUserService public IEntityRepository UserRepository { get; protected set; } public IEntityRepository CredentialRepository { get; protected set; } - protected UserService() {} + protected UserService() { } public UserService( IAppConfiguration config, IEntityRepository userRepository, - IEntityRepository credentialRepository) : this() + IEntityRepository credentialRepository) + : this() { Config = config; UserRepository = userRepository; @@ -92,6 +93,7 @@ public void UpdateProfile(User user, string emailAddress, bool emailAllowed) UserRepository.CommitChanges(); } + [Obsolete("Use FindByCredential instead")] public User FindByApiKey(Guid apiKey) { return UserRepository.GetAll().SingleOrDefault(u => u.ApiKey == apiKey); @@ -254,6 +256,15 @@ public bool ResetPasswordWithToken(string username, string token, string newPass 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); + } + private static User AuthenticateUser(string password, User user) { if (user == null) diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 9cedeff5a1..426ddb6193 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -458,12 +458,8 @@ public void FindsUserBasedOnPasswordInCredentialsTable() var user = CreateAUser("theUsername", "test@example.com"); user.Credentials.Add(CreatePasswordCredential("thePassword")); var service = new TestableUserService(); - service.MockUserRepository - .Setup(u => u.GetAll()) - .Returns(new[] { user }.AsQueryable()); - service.MockCredentialRepository - .Setup(c => c.GetAll()) - .Returns(user.Credentials.AsQueryable()); + service.MockUserRepository.HasData(user); + service.MockCredentialRepository.HasData(user.Credentials); var foundByUserName = service.FindByUsernameAndPassword("theUsername", "thePassword"); @@ -477,12 +473,8 @@ public void IfSomehowBothPasswordsExistItFindsUserBasedOnPasswordInCredentialsTa var user = CreateAUser("theUsername", "theWrongPassword", "test@example.com"); user.Credentials.Add(CreatePasswordCredential("thePassword")); var service = new TestableUserService(); - service.MockUserRepository - .Setup(u => u.GetAll()) - .Returns(new[] { user }.AsQueryable()); - service.MockCredentialRepository - .Setup(c => c.GetAll()) - .Returns(user.Credentials.AsQueryable()); + service.MockUserRepository.HasData(user); + service.MockCredentialRepository.HasData(user.Credentials); var foundByUserName = service.FindByUsernameAndPassword("theUsername", "thePassword"); @@ -541,12 +533,8 @@ public void FindsUserBasedOnPasswordInCredentialsTable() var user = CreateAUser("theUsername", "test@example.com"); user.Credentials.Add(CreatePasswordCredential("thePassword")); var service = new TestableUserService(); - service.MockUserRepository - .Setup(u => u.GetAll()) - .Returns(new[] { user }.AsQueryable()); - service.MockCredentialRepository - .Setup(c => c.GetAll()) - .Returns(user.Credentials.AsQueryable()); + service.MockUserRepository.HasData(user); + service.MockCredentialRepository.HasData(user.Credentials); var foundByUserName = service.FindByUsernameOrEmailAddressAndPassword("test@example.com", "thePassword"); @@ -560,12 +548,8 @@ public void IfSomehowBothPasswordsExistItFindsUserBasedOnPasswordInCredentialsTa var user = CreateAUser("theUsername", "theWrongPassword", "test@example.com"); user.Credentials.Add(CreatePasswordCredential("thePassword")); var service = new TestableUserService(); - service.MockUserRepository - .Setup(u => u.GetAll()) - .Returns(new[] { user }.AsQueryable()); - service.MockCredentialRepository - .Setup(c => c.GetAll()) - .Returns(user.Credentials.AsQueryable()); + service.MockUserRepository.HasData(user); + service.MockCredentialRepository.HasData(user.Credentials); var foundByUserName = service.FindByUsernameOrEmailAddressAndPassword("test@example.com", "thePassword"); @@ -574,6 +558,26 @@ public void IfSomehowBothPasswordsExistItFindsUserBasedOnPasswordInCredentialsTa } } + public class TheAuthenticateCredentialMethod + { + [Fact] + public void ReturnsNullIfNoCredentialOfSpecifiedTypeExists() + { + // Arrange + var creds = new List() { + new Credential("foo", "bar") + }; + var service = new TestableUserService(); + service.MockCredentialRepository.HasData(creds); + + // Act + var result = service.AuthenticateCredential(type: "baz", value: "bar"); + + // Assert + Assert.Null(result); + } + } + public class TheGenerateApiKeyMethod { [Fact] diff --git a/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs b/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs index 24030b817b..8783679152 100644 --- a/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs +++ b/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs @@ -1,4 +1,7 @@ -using Moq.Language.Flow; +using System.Collections.Generic; +using System.Linq; +using Moq; +using Moq.Language.Flow; namespace NuGetGallery { @@ -9,5 +12,17 @@ public static IReturnsResult ReturnsNull(this ISetup> HasData(this Mock> self, params TType[] fakeData) + where TType : class, IEntity, new() + { + return HasData(self, (IEnumerable)fakeData); + } + + public static IReturnsResult> HasData(this Mock> self, IEnumerable fakeData) + where TType : class, IEntity, new() + { + return self.Setup(e => e.GetAll()).Returns(fakeData.AsQueryable()); + } } } From 8ca55baab0cc74760e9f75ded8a2c307eb36b3d6 Mon Sep 17 00:00:00 2001 From: anurse Date: Wed, 18 Sep 2013 12:46:16 -0700 Subject: [PATCH 05/13] Started on ReplaceCredential --- src/NuGetGallery/Constants.cs | 1 + src/NuGetGallery/Controllers/ApiController.cs | 25 ++++++++++--- .../Controllers/AuthenticationController.cs | 2 +- .../Controllers/UsersController.cs | 4 ++- .../Infrastructure/CredentialBuilder.cs | 21 +++++++++++ src/NuGetGallery/NuGetGallery.csproj | 1 + src/NuGetGallery/Services/IUserService.cs | 20 +++++++++++ src/NuGetGallery/Services/UserService.cs | 36 ++++++++++++++++--- src/NuGetGallery/Strings.Designer.cs | 13 +++++-- src/NuGetGallery/Strings.resx | 5 ++- .../AuthenticationControllerFacts.cs | 2 +- .../NuGetGallery.Facts.csproj | 1 + .../Services/UserServiceFacts.cs | 5 +++ 13 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 src/NuGetGallery/Infrastructure/CredentialBuilder.cs diff --git a/src/NuGetGallery/Constants.cs b/src/NuGetGallery/Constants.cs index 7635f77961..b0f53ac447 100644 --- a/src/NuGetGallery/Constants.cs +++ b/src/NuGetGallery/Constants.cs @@ -49,6 +49,7 @@ public static class ContentNames public static class CredentialTypes { public static readonly string PasswordPbkdf2 = "password.pbkdf2"; + public static readonly string ApiKeyV1 = "apikey.v1"; } } } \ No newline at end of file diff --git a/src/NuGetGallery/Controllers/ApiController.cs b/src/NuGetGallery/Controllers/ApiController.cs index c3db3b01c7..38435dce9a 100644 --- a/src/NuGetGallery/Controllers/ApiController.cs +++ b/src/NuGetGallery/Controllers/ApiController.cs @@ -158,7 +158,7 @@ public virtual ActionResult VerifyPackageKey(string apiKey, string id, string ve HttpStatusCode.BadRequest, String.Format(CultureInfo.CurrentCulture, Strings.InvalidApiKey, apiKey)); } - var user = UserService.FindByApiKey(parsedApiKey); + User user = GetUserByApiKey(apiKey); if (user == null) { return new HttpStatusCodeWithBodyResult( @@ -210,7 +210,7 @@ private async Task CreatePackageInternal(string apiKey) HttpStatusCode.BadRequest, String.Format(CultureInfo.CurrentCulture, Strings.InvalidApiKey, apiKey)); } - var user = UserService.FindByApiKey(parsedApiKey); + User user = GetUserByApiKey(apiKey); if (user == null) { return new HttpStatusCodeWithBodyResult( @@ -275,7 +275,7 @@ public virtual ActionResult DeletePackage(string apiKey, string id, string versi HttpStatusCode.BadRequest, String.Format(CultureInfo.CurrentCulture, Strings.InvalidApiKey, apiKey)); } - var user = UserService.FindByApiKey(parsedApiKey); + User user = GetUserByApiKey(apiKey); if (user == null) { return new HttpStatusCodeWithBodyResult( @@ -312,7 +312,7 @@ public virtual ActionResult PublishPackage(string apiKey, string id, string vers HttpStatusCode.BadRequest, String.Format(CultureInfo.CurrentCulture, Strings.InvalidApiKey, apiKey)); } - var user = UserService.FindByApiKey(parsedApiKey); + User user = GetUserByApiKey(apiKey); if (user == null) { return new HttpStatusCodeWithBodyResult( @@ -447,6 +447,23 @@ public virtual async Task GetStatsDownloads(int? count) return new HttpStatusCodeResult(HttpStatusCode.NotFound); } + private User GetUserByApiKey(string apiKey) + { + var cred = UserService.AuthenticateCredential(Constants.CredentialTypes.ApiKeyV1, apiKey); + 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; + } + private static void QuietlyLogException(Exception e) { try diff --git a/src/NuGetGallery/Controllers/AuthenticationController.cs b/src/NuGetGallery/Controllers/AuthenticationController.cs index f74c300984..376ae80c0c 100644 --- a/src/NuGetGallery/Controllers/AuthenticationController.cs +++ b/src/NuGetGallery/Controllers/AuthenticationController.cs @@ -55,7 +55,7 @@ public virtual ActionResult LogOn(SignInRequest request, string returnUrl) { ModelState.AddModelError( String.Empty, - Strings.UserNotFound); + Strings.UsernameAndPasswordNotFound); return View(); } diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index 7a92cc1971..31f9a11ca7 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -193,7 +193,9 @@ public virtual ActionResult Packages() [HttpPost] public virtual ActionResult GenerateApiKey() { - UserService.GenerateApiKey(CurrentUser.Identity.Name); + UserService.ReplaceCredential( + User.Identity.Name, + CredentialBuilder.CreateV1ApiKey()); return RedirectToAction(MVC.Users.Account()); } diff --git a/src/NuGetGallery/Infrastructure/CredentialBuilder.cs b/src/NuGetGallery/Infrastructure/CredentialBuilder.cs new file mode 100644 index 0000000000..94eb6afd7a --- /dev/null +++ b/src/NuGetGallery/Infrastructure/CredentialBuilder.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace NuGetGallery +{ + /// + /// Provides helper methods to generate credentials. + /// + public static class CredentialBuilder + { + public static Credential CreateV1ApiKey() + { + var value = Guid.NewGuid() + .ToString() + .ToLowerInvariant(); + return new Credential(Constants.CredentialTypes.ApiKeyV1, value); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index acf70ef127..6fc3a29464 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -592,6 +592,7 @@ + diff --git a/src/NuGetGallery/Services/IUserService.cs b/src/NuGetGallery/Services/IUserService.cs index a9de91da9c..82384eb438 100644 --- a/src/NuGetGallery/Services/IUserService.cs +++ b/src/NuGetGallery/Services/IUserService.cs @@ -9,6 +9,7 @@ public interface IUserService void UpdateProfile(User user, string emailAddress, bool emailAllowed); + [Obsolete("Use AuthenticateCredential instead")] User FindByApiKey(Guid apiKey); User FindByEmailAddress(string emailAddress); @@ -21,6 +22,7 @@ public interface IUserService User FindByUsernameOrEmailAddressAndPassword(string usernameOrEmail, string password); + [Obsolete("Use ReplaceCredential instead")] string GenerateApiKey(string username); bool ConfirmEmailAddress(User user, string token); @@ -43,5 +45,23 @@ public interface IUserService /// 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 3eec59ecbe..effb95af0d 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -93,7 +93,7 @@ public void UpdateProfile(User user, string emailAddress, bool emailAllowed) UserRepository.CommitChanges(); } - [Obsolete("Use FindByCredential instead")] + [Obsolete("Use AuthenticateCredential instead")] public User FindByApiKey(Guid apiKey) { return UserRepository.GetAll().SingleOrDefault(u => u.ApiKey == apiKey); @@ -130,7 +130,7 @@ public virtual User FindByUsernameAndPassword(string username, string password) .Include(u => u.Credentials) .SingleOrDefault(u => u.Username == username); - return AuthenticateUser(password, user); + return AuthenticatePassword(password, user); } public virtual User FindByUsernameOrEmailAddressAndPassword(string usernameOrEmail, string password) @@ -140,9 +140,10 @@ public virtual User FindByUsernameOrEmailAddressAndPassword(string usernameOrEma .Include(u => u.Credentials) .SingleOrDefault(u => u.Username == usernameOrEmail || u.EmailAddress == usernameOrEmail); - return AuthenticateUser(password, user); + return AuthenticatePassword(password, user); } + [Obsolete("Use ReplaceCredential instead")] public string GenerateApiKey(string username) { var user = FindByUsername(username); @@ -265,7 +266,34 @@ public Credential AuthenticateCredential(string type, string value) .SingleOrDefault(c => c.Type == type && c.Value == value); } - private static User AuthenticateUser(string password, User user) + 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) + { + // Find the credentials we're replacing, if any + var creds = user.Credentials + .Where(cred => cred.Type == credential.Type); + foreach(var cred in creds) + { + user.Credentials.Remove(cred); + } + + user.Credentials.Add(credential); + UserRepository.CommitChanges(); + } + + private static User AuthenticatePassword(string password, User user) { if (user == null) { diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index 4dfa7852a1..9981a8eb99 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.18033 +// Runtime Version:4.0.30319.33440 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -213,6 +213,15 @@ public static string UserIsNotYetConfirmed { } } + /// + /// Looks up a localized string similar to A user with the provided user name and password does not exist.. + /// + public static string UsernameAndPasswordNotFound { + get { + return ResourceManager.GetString("UsernameAndPasswordNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to The username '{0}' is not available.. /// @@ -223,7 +232,7 @@ public static string UsernameNotAvailable { } /// - /// Looks up a localized string similar to A user with the provided user name and password does not exist.. + /// Looks up a localized string similar to A user with the provided user name does not exist.. /// public static string UserNotFound { get { diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index af869c07bb..f2ff44bc46 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -123,7 +123,7 @@ The username '{0}' is not available. - + A user with the provided user name and password does not exist. @@ -174,4 +174,7 @@ '{0}' cannot be null or an empty string + + A user with the provided user name does not exist. + \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Controllers/AuthenticationControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/AuthenticationControllerFacts.cs index 5ee17a2573..95166ba9ee 100644 --- a/tests/NuGetGallery.Facts/Controllers/AuthenticationControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/AuthenticationControllerFacts.cs @@ -145,7 +145,7 @@ public void WillInvalidateModelStateAndShowTheViewWithErrorsWhenTheUsernameAndPa Assert.NotNull(result); Assert.Empty(result.ViewName); Assert.False(controller.ModelState.IsValid); - Assert.Equal(Strings.UserNotFound, controller.ModelState[String.Empty].Errors[0].ErrorMessage); + Assert.Equal(Strings.UsernameAndPasswordNotFound, controller.ModelState[String.Empty].Errors[0].ErrorMessage); } [Fact] diff --git a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj index 9d60004ab6..2498b30f0b 100644 --- a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj +++ b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj @@ -27,6 +27,7 @@ 4 true false + 0618 pdbonly diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 426ddb6193..9e1a763417 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -578,6 +578,11 @@ public void ReturnsNullIfNoCredentialOfSpecifiedTypeExists() } } + public class TheReplaceCredentialMethod + { + + } + public class TheGenerateApiKeyMethod { [Fact] From 0bdae2eb2f97b5b20d7c7627c1df0af4b327a796 Mon Sep 17 00:00:00 2001 From: anurse Date: Wed, 18 Sep 2013 13:29:19 -0700 Subject: [PATCH 06/13] Added tests for AuthenticateCredential --- .../Services/UserServiceFacts.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 9e1a763417..6337ae8c87 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -576,6 +576,40 @@ public void ReturnsNullIfNoCredentialOfSpecifiedTypeExists() // Assert Assert.Null(result); } + + [Fact] + public void ReturnsNullIfNoCredentialOfSpecifiedTypeWithSpecifiedValueExists() + { + // Arrange + var creds = new List() { + new Credential("foo", "bar") + }; + var service = new TestableUserService(); + service.MockCredentialRepository.HasData(creds); + + // Act + var result = service.AuthenticateCredential(type: "foo", value: "baz"); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ReturnsCredentialIfOneExistsWithSpecifiedTypeAndValue() + { + // Arrange + var creds = new List() { + new Credential("foo", "bar") + }; + var service = new TestableUserService(); + service.MockCredentialRepository.HasData(creds); + + // Act + var result = service.AuthenticateCredential(type: "foo", value: "bar"); + + // Assert + Assert.Same(creds[0], result); + } } public class TheReplaceCredentialMethod From 4f3b9a4d42cc9caa092e003d2f61ae444d8fa419 Mon Sep 17 00:00:00 2001 From: anurse Date: Wed, 18 Sep 2013 15:33:11 -0700 Subject: [PATCH 07/13] Fixed up Create/Delete/PublishPackage APIs --- src/NuGetGallery/Controllers/ApiController.cs | 4 +- src/NuGetGallery/Services/UserService.cs | 3 +- .../Controllers/ApiControllerFacts.cs | 321 +++++++++++++----- .../Services/UserServiceFacts.cs | 68 ++++ .../TestUtils/MockExtensions.cs | 21 +- .../TestUtils/ResultAssert.cs | 20 +- 6 files changed, 343 insertions(+), 94 deletions(-) diff --git a/src/NuGetGallery/Controllers/ApiController.cs b/src/NuGetGallery/Controllers/ApiController.cs index 38435dce9a..1bbde704fd 100644 --- a/src/NuGetGallery/Controllers/ApiController.cs +++ b/src/NuGetGallery/Controllers/ApiController.cs @@ -260,7 +260,7 @@ private async Task CreatePackageInternal(string apiKey) } } - return new HttpStatusCodeResult(201); + return new HttpStatusCodeResult(HttpStatusCode.Created); } [HttpDelete] @@ -449,7 +449,7 @@ public virtual async Task GetStatsDownloads(int? count) private User GetUserByApiKey(string apiKey) { - var cred = UserService.AuthenticateCredential(Constants.CredentialTypes.ApiKeyV1, apiKey); + var cred = UserService.AuthenticateCredential(Constants.CredentialTypes.ApiKeyV1, apiKey.ToLowerInvariant()); User user; if (cred == null) { diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index effb95af0d..791a3d37a8 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -283,7 +283,8 @@ public void ReplaceCredential(User user, Credential credential) { // Find the credentials we're replacing, if any var creds = user.Credentials - .Where(cred => cred.Type == credential.Type); + .Where(cred => cred.Type == credential.Type) + .ToList(); foreach(var cred in creds) { user.Credentials.Remove(cred); diff --git a/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs index 45bcdf46b1..db7ad6bd7c 100644 --- a/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs @@ -163,21 +163,82 @@ public async Task WillReturnConflictIfAPackageWithTheIdAndSemanticVersionAlready } [Fact] - public void WillFindTheUserThatMatchesTheApiKey() + public async Task WillFindUserUsingFindByApiKey() { var nuGetPackage = new Mock(); nuGetPackage.Setup(x => x.Metadata.Id).Returns("theId"); nuGetPackage.Setup(x => x.Metadata.Version).Returns(new SemanticVersion("1.0.42")); + var user = new User(); + var apiKey = Guid.NewGuid(); + var controller = new TestableApiController(); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns(new User()); + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(user); controller.SetupPackageFromInputStream(nuGetPackage); + ResultAssert.IsStatusCode( + await controller.CreatePackagePut(apiKey.ToString()), + HttpStatusCode.Created); + + controller.MockPackageService.Verify(p => + p.CreatePackage(nuGetPackage.Object, user, true)); + } + + [Fact] + public async Task WillFindUserUsingAuthenticateCredential() + { + var nuGetPackage = new Mock(); + nuGetPackage.Setup(x => x.Metadata.Id).Returns("theId"); + nuGetPackage.Setup(x => x.Metadata.Version).Returns(new SemanticVersion("1.0.42")); + + var user = new User(); var apiKey = Guid.NewGuid(); - controller.CreatePackagePut(apiKey.ToString()); + var controller = new TestableApiController(); + controller.MockUserService.Setup( + x => x.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + apiKey.ToString().ToLowerInvariant())) + .Returns(new Credential() { User = user }); + controller.SetupPackageFromInputStream(nuGetPackage); + + ResultAssert.IsStatusCode( + await controller.CreatePackagePut(apiKey.ToString().ToUpperInvariant()), + HttpStatusCode.Created); - controller.MockUserService.Verify(x => x.FindByApiKey(apiKey)); + controller.MockPackageService.Verify(p => + p.CreatePackage(nuGetPackage.Object, user, true)); + } + + [Fact] + public async Task WillUseUserFoundByAuthenticateCredentialOverFindByApiKey() + { + var nuGetPackage = new Mock(); + nuGetPackage.Setup(x => x.Metadata.Id).Returns("theId"); + nuGetPackage.Setup(x => x.Metadata.Version).Returns(new SemanticVersion("1.0.42")); + + var correctUser = new User(); + var incorrectUser = new User(); + var apiKey = Guid.NewGuid(); + + var controller = new TestableApiController(); + controller.MockUserService + .Setup(x => x.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + apiKey.ToString().ToLowerInvariant())) + .Returns(new Credential() { User = correctUser }); + controller.MockUserService + .Setup(x => x.FindByApiKey(apiKey)) + .Returns(incorrectUser); + + controller.SetupPackageFromInputStream(nuGetPackage); + + ResultAssert.IsStatusCode( + await controller.CreatePackagePut(apiKey.ToString().ToUpperInvariant()), + HttpStatusCode.Created); + + controller.MockPackageService.Verify(p => + p.CreatePackage(nuGetPackage.Object, correctUser, true)); } [Fact] @@ -266,8 +327,7 @@ public class TheDeletePackageAction public void WillThrowIfTheApiKeyIsAnInvalidGuid(string guidValue) { var controller = new TestableApiController(); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns((User)null); - + var result = controller.DeletePackage(guidValue, "theId", "1.0.42"); Assert.IsType(result); @@ -278,47 +338,31 @@ public void WillThrowIfTheApiKeyIsAnInvalidGuid(string guidValue) public void WillThrowIfTheApiKeyDoesNotExist() { var controller = new TestableApiController(); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns((User)null); - + var result = controller.DeletePackage(Guid.NewGuid().ToString(), "theId", "1.0.42"); Assert.IsType(result); var statusCodeResult = (HttpStatusCodeWithBodyResult)result; Assert.Equal(403, statusCodeResult.StatusCode); Assert.Equal(String.Format(Strings.ApiKeyNotAuthorized, "delete"), statusCodeResult.StatusDescription); + controller.MockPackageService.Verify(x => x.MarkPackageUnlisted(It.IsAny(), true), Times.Never()); } [Fact] public void WillThrowIfAPackageWithTheIdAndSemanticVersionDoesNotExist() { var controller = new TestableApiController(); + var apiKey = Guid.NewGuid(); controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns((Package)null); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns(new User()); + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(new User()); - var result = controller.DeletePackage(Guid.NewGuid().ToString(), "theId", "1.0.42"); + var result = controller.DeletePackage(apiKey.ToString(), "theId", "1.0.42"); Assert.IsType(result); var statusCodeResult = (HttpStatusCodeWithBodyResult)result; Assert.Equal(404, statusCodeResult.StatusCode); Assert.Equal(String.Format(Strings.PackageWithIdAndVersionNotFound, "theId", "1.0.42"), statusCodeResult.StatusDescription); - } - - [Fact] - public void WillFindTheUserThatMatchesTheApiKey() - { - var owner = new User { Key = 1, ApiKey = Guid.NewGuid() }; - var package = new Package - { - PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner } } - }; - - var controller = new TestableApiController(); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns(owner); - - controller.DeletePackage(owner.ApiKey.ToString(), "theId", "1.0.42"); - - controller.MockUserService.Verify(x => x.FindByApiKey(owner.ApiKey)); + controller.MockPackageService.Verify(x => x.MarkPackageUnlisted(It.IsAny(), true), Times.Never()); } [Fact] @@ -331,37 +375,111 @@ public void WillNotDeleteThePackageIfApiKeyDoesNotBelongToAnOwner() }; var apiKey = Guid.NewGuid(); var controller = new TestableApiController(); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns(owner); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); - controller.MockPackageService - .Setup(svc => svc.MarkPackageUnlisted(It.IsAny(), true)) - .Throws(new InvalidOperationException("Should not have unlisted the package!")); - + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(owner); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)).Returns(package); + var result = controller.DeletePackage(apiKey.ToString(), "theId", "1.0.42"); Assert.IsType(result); var statusCodeResult = (HttpStatusCodeWithBodyResult)result; Assert.Equal(String.Format(Strings.ApiKeyNotAuthorized, "delete"), statusCodeResult.StatusDescription); + + controller.MockPackageService.Verify(x => x.MarkPackageUnlisted(package, true), Times.Never()); } [Fact] - public void WillUnlistThePackageIfApiKeyBelongsToAnOwner() + public void WillUnlistThePackageIfApiKeyBelongsToAnOwnerUsingFindByApiKey() { - var owner = new User { Key = 1 }; + var apiKey = Guid.NewGuid(); + var owner = new User { Key = 1, ApiKey = apiKey }; var package = new Package { PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner } } }; var controller = new TestableApiController(); controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns(owner); + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(owner); + + ResultAssert.IsEmpty(controller.DeletePackage(apiKey.ToString(), "theId", "1.0.42")); + + controller.MockPackageService.Verify(x => x.MarkPackageUnlisted(package, true)); + controller.MockIndexingService.Verify(i => i.UpdatePackage(package)); + } + + [Fact] + public void WillUnlistThePackageIfApiKeyBelongsToAnOwnerUsingAuthenticateCredential() + { var apiKey = Guid.NewGuid(); + var owner = new Credential() { User = new User { Key = 1 } }; + var package = new Package + { + PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner.User } } + }; + var controller = new TestableApiController(); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); + controller.MockUserService + .Setup(x => x.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + apiKey.ToString().ToLowerInvariant())) + .Returns(owner); - controller.DeletePackage(apiKey.ToString(), "theId", "1.0.42"); + ResultAssert.IsEmpty(controller.DeletePackage(apiKey.ToString(), "theId", "1.0.42")); controller.MockPackageService.Verify(x => x.MarkPackageUnlisted(package, true)); controller.MockIndexingService.Verify(i => i.UpdatePackage(package)); } + + [Fact] + public void WillUseUserFromAuthenticateCredentialOverFindByApiKey() + { + var apiKey = Guid.NewGuid(); + var owner = new Credential() { User = new User { Key = 1 } }; + var nonOwner = new User() { ApiKey = apiKey }; + var package = new Package + { + PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner.User } } + }; + var controller = new TestableApiController(); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(nonOwner); + controller.MockUserService + .Setup(x => x.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + apiKey.ToString().ToLowerInvariant())) + .Returns(owner); + + ResultAssert.IsEmpty(controller.DeletePackage(apiKey.ToString(), "theId", "1.0.42")); + + controller.MockPackageService.Verify(x => x.MarkPackageUnlisted(package, true)); + controller.MockIndexingService.Verify(i => i.UpdatePackage(package)); + } + + [Fact] + public void WillFailIfUserFromAuthenticateCredentialIsNotOwner() + { + var apiKey = Guid.NewGuid(); + var nonOwner = new Credential() { User = new User { Key = 1 } }; + var owner = new User() { ApiKey = apiKey }; + var package = new Package + { + PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner } } + }; + var controller = new TestableApiController(); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(owner); + controller.MockUserService + .Setup(x => x.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + apiKey.ToString().ToLowerInvariant())) + .Returns(nonOwner); + + ResultAssert.IsStatusCode( + controller.DeletePackage(apiKey.ToString(), "theId", "1.0.42"), + HttpStatusCode.Forbidden, + String.Format(Strings.ApiKeyNotAuthorized, "delete")); + + controller.MockPackageService.Verify(x => x.MarkPackageUnlisted(package, true), Times.Never()); + } } public class TheGetPackageAction @@ -531,102 +649,137 @@ public void WillThrowIfTheApiKeyIsAnInvalidGuid(string guidValue) var controller = new TestableApiController(); controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns((User)null); - var result = controller.PublishPackage(guidValue, "theId", "1.0.42"); - - Assert.IsType(result); - AssertStatusCodeResult(result, 400, String.Format("The API key '{0}' is invalid.", guidValue)); + ResultAssert.IsStatusCode( + controller.PublishPackage(guidValue, "theId", "1.0.42"), + HttpStatusCode.BadRequest, + String.Format("The API key '{0}' is invalid.", guidValue)); + controller.MockPackageService.Verify(x => x.MarkPackageListed(It.IsAny(), It.IsAny()), Times.Never()); } [Fact] public void WillThrowIfTheApiKeyDoesNotExist() { var controller = new TestableApiController(); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns((User)null); - - var result = controller.PublishPackage(Guid.NewGuid().ToString(), "theId", "1.0.42"); + var apiKey = Guid.NewGuid(); + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).ReturnsNull(); - Assert.IsType(result); - var statusCodeResult = (HttpStatusCodeWithBodyResult)result; - Assert.Equal(403, statusCodeResult.StatusCode); - Assert.Equal(String.Format(Strings.ApiKeyNotAuthorized, "publish"), statusCodeResult.StatusDescription); + ResultAssert.IsStatusCode( + controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"), + HttpStatusCode.Forbidden, + String.Format(Strings.ApiKeyNotAuthorized, "publish")); + controller.MockPackageService.Verify(x => x.MarkPackageListed(It.IsAny(), It.IsAny()), Times.Never()); } [Fact] public void WillThrowIfAPackageWithTheIdAndSemanticVersionDoesNotExist() { var controller = new TestableApiController(); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns((Package)null); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns(new User()); + var apiKey = Guid.NewGuid(); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)).Returns((Package)null); + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(new User()); - var result = controller.PublishPackage(Guid.NewGuid().ToString(), "theId", "1.0.42"); - - Assert.IsType(result); - var statusCodeResult = (HttpStatusCodeWithBodyResult)result; - Assert.Equal(404, statusCodeResult.StatusCode); - Assert.Equal(String.Format(Strings.PackageWithIdAndVersionNotFound, "theId", "1.0.42"), statusCodeResult.StatusDescription); + ResultAssert.IsStatusCode( + controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"), + HttpStatusCode.NotFound, + String.Format(Strings.PackageWithIdAndVersionNotFound, "theId", "1.0.42")); + controller.MockPackageService.Verify(x => x.MarkPackageListed(It.IsAny(), It.IsAny()), Times.Never()); } [Fact] - public void WillFindTheUserThatMatchesTheApiKey() + public void WillNotListThePackageIfApiKeyDoesNotBelongToAnOwner() { var owner = new User { Key = 1 }; var package = new Package { - PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner } } + PackageRegistration = new PackageRegistration { Owners = new[] { new User() } } }; var apiKey = Guid.NewGuid(); var controller = new TestableApiController(); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns(owner); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)).Returns(package); + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(owner); - controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"); + ResultAssert.IsStatusCode( + controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"), + HttpStatusCode.Forbidden, + String.Format(Strings.ApiKeyNotAuthorized, "publish")); - controller.MockUserService.Verify(x => x.FindByApiKey(apiKey)); + controller.MockPackageService.Verify(x => x.MarkPackageListed(package, It.IsAny()), Times.Never()); } [Fact] - public void WillNotListThePackageIfApiKeyDoesNotBelongToAnOwner() + public void WillListThePackageIfApiKeyBelongsToAnOwnerUsingFindByApiKey() { - var owner = new User { Key = 1 }; - var package = new Package - { - PackageRegistration = new PackageRegistration { Owners = new[] { new User() } } - }; var apiKey = Guid.NewGuid(); + var owner = new User { Key = 1, ApiKey = apiKey }; + var package = new Package + { + PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner } } + }; var controller = new TestableApiController(); controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); - controller.MockPackageService.Setup(svc => svc.MarkPackageListed(It.IsAny(), It.IsAny())) - .Throws(new InvalidOperationException("Should not have listed the package!")); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns(owner); + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(owner); - var result = controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"); + ResultAssert.IsEmpty( + controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42")); - Assert.IsType(result); - var statusCodeResult = (HttpStatusCodeWithBodyResult)result; - Assert.Equal(String.Format(Strings.ApiKeyNotAuthorized, "publish"), statusCodeResult.StatusDescription); + controller.MockPackageService.Verify(x => x.MarkPackageListed(package, It.IsAny())); + controller.MockIndexingService.Verify(i => i.UpdatePackage(package)); } [Fact] - public void WillListThePackageIfApiKeyBelongsToAnOwner() + public void WillListThePackageIfApiKeyBelongsToAnOwnerUsingAuthorizeCredential() { - var owner = new User { Key = 1 }; - var package = new Package - { - PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner } } - }; var apiKey = Guid.NewGuid(); + var owner = new Credential { User = new User { Key = 1 } }; + var package = new Package + { + PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner.User } } + }; var controller = new TestableApiController(); controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); - controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns(owner); + controller.MockUserService + .Setup(x => x.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + apiKey.ToString().ToLowerInvariant())) + .Returns(owner); - controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"); + ResultAssert.IsEmpty( + controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42")); controller.MockPackageService.Verify(x => x.MarkPackageListed(package, It.IsAny())); controller.MockIndexingService.Verify(i => i.UpdatePackage(package)); } + + [Fact] + public void WillFailIfUserFromAuthenticateCredentialIsNotOwner() + { + var apiKey = Guid.NewGuid(); + var nonOwner = new Credential { User = new User { Key = 1 } }; + var owner = new User(); + var package = new Package + { + PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner } } + }; + + var controller = new TestableApiController(); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); + controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(owner); + controller.MockUserService + .Setup(x => x.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + apiKey.ToString().ToLowerInvariant())) + .Returns(nonOwner); + + ResultAssert.IsStatusCode( + controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"), + HttpStatusCode.Forbidden, + String.Format(Strings.ApiKeyNotAuthorized, "publish")); + + controller.MockPackageService.Verify(x => x.MarkPackageListed(package, It.IsAny()), Times.Never()); + } } public class TheVerifyPackageKeyAction diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 6337ae8c87..714fdf25d2 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -614,7 +614,75 @@ public void ReturnsCredentialIfOneExistsWithSpecifiedTypeAndValue() public class TheReplaceCredentialMethod { + [Fact] + public void ThrowsExceptionIfNoUserWithProvidedUserName() + { + // Arrange + var users = new List() { + new User("foo", "baz") + }; + var service = new TestableUserService(); + service.MockUserRepository.HasData(users); + + // Act + var ex = Assert.Throws(() => + service.ReplaceCredential("biz", new Credential())); + + // Assert + Assert.Equal(Strings.UserNotFound, ex.Message); + } + + [Fact] + public void AddsNewCredentialIfNoneWithSameTypeForUser() + { + // Arrange + var existingCred = new Credential("foo", "bar"); + var newCred = new Credential("baz", "boz"); + var users = new List() { + new User("foo", "baz") { + Credentials = new List() { + existingCred + } + } + }; + var service = new TestableUserService(); + service.MockUserRepository.HasData(users); + // Act + service.ReplaceCredential("foo", newCred); + + // Assert + Assert.Equal(2, users[0].Credentials.Count); + Assert.Equal(new[] { existingCred, newCred }, users[0].Credentials.ToArray()); + service.MockUserRepository.VerifyCommitted(); + } + + [Fact] + public void ReplacesExistingCredentialIfOneWithSameTypeExistsForUser() + { + // Arrange + var frozenCred = new Credential("foo", "bar"); + var existingCred = new Credential("baz", "bar"); + var newCred = new Credential("baz", "boz"); + var users = new List() { + new User("foo", "baz") { + Credentials = new List() { + existingCred, + frozenCred + } + } + }; + var service = new TestableUserService(); + service.MockUserRepository.HasData(users); + + // Act + service.ReplaceCredential("foo", newCred); + + // Assert + Assert.Equal(2, users[0].Credentials.Count); + Assert.Equal(new[] { frozenCred, newCred }, users[0].Credentials.ToArray()); + service.MockUserRepository.VerifyCommitted(); + } } public class TheGenerateApiKeyMethod diff --git a/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs b/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs index 8783679152..c1a85805c0 100644 --- a/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs +++ b/tests/NuGetGallery.Facts/TestUtils/MockExtensions.cs @@ -13,16 +13,27 @@ public static IReturnsResult ReturnsNull(this ISetup> HasData(this Mock> self, params TType[] fakeData) - where TType : class, IEntity, new() + public static IReturnsResult> HasData(this Mock> self, params T[] fakeData) + where T : class, IEntity, new() { - return HasData(self, (IEnumerable)fakeData); + return HasData(self, (IEnumerable)fakeData); } - public static IReturnsResult> HasData(this Mock> self, IEnumerable fakeData) - where TType : class, IEntity, new() + public static IReturnsResult> HasData(this Mock> self, IEnumerable fakeData) + where T : class, IEntity, new() { return self.Setup(e => e.GetAll()).Returns(fakeData.AsQueryable()); } + + public static void VerifyCommitted(this Mock> self) + where T : class, IEntity, new() + { + self.Verify(e => e.CommitChanges()); + } + + public static void VerifyCommitted(this Mock self) + { + self.Verify(e => e.SaveChanges()); + } } } diff --git a/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs b/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs index a2ca791a71..afd272f4b8 100644 --- a/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs +++ b/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs @@ -62,13 +62,29 @@ private static void DictionariesMatch(IDictionary expected, IDiction public static void IsStatusCode(ActionResult result, HttpStatusCode code) { - IsStatusCode(result, (int)code); + IsStatusCode(result, (int)code, description: null); } public static void IsStatusCode(ActionResult result, int code) { - var statusCodeResult = Assert.IsType(result); + IsStatusCode(result, code, description: null); + } + + public static void IsStatusCode(ActionResult result, HttpStatusCode code, string description) + { + IsStatusCode(result, (int)code, description); + } + + public static void IsStatusCode(ActionResult result, int code, string description) + { + var statusCodeResult = Assert.IsAssignableFrom(result); Assert.Equal(code, statusCodeResult.StatusCode); + Assert.Equal(description, statusCodeResult.StatusDescription); + } + + public static EmptyResult IsEmpty(ActionResult result) + { + return Assert.IsType(result); } } } From cfe5e8e4c6b0db2552b3b5819065568086a59d55 Mon Sep 17 00:00:00 2001 From: anurse Date: Wed, 18 Sep 2013 15:48:20 -0700 Subject: [PATCH 08/13] Finished adding Api tests for new creds table --- .../Controllers/ApiControllerFacts.cs | 190 ++++++++++++++---- 1 file changed, 152 insertions(+), 38 deletions(-) diff --git a/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs index db7ad6bd7c..3c90c41fe1 100644 --- a/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs @@ -62,14 +62,6 @@ public class ApiControllerFacts private static readonly Uri HttpRequestUrl = new Uri("http://nuget.org/api/v2/something"); private static readonly Uri HttpsRequestUrl = new Uri("https://nuget.org/api/v2/something"); - private static void AssertStatusCodeResult(ActionResult result, int statusCode, string statusDesc) - { - Assert.IsType(result); - var httpStatus = (HttpStatusCodeWithBodyResult)result; - Assert.Equal(statusCode, httpStatus.StatusCode); - Assert.Equal(statusDesc, httpStatus.StatusDescription); - } - public class TheCreatePackageAction { [Fact] @@ -328,10 +320,14 @@ public void WillThrowIfTheApiKeyIsAnInvalidGuid(string guidValue) { var controller = new TestableApiController(); + // Act var result = controller.DeletePackage(guidValue, "theId", "1.0.42"); - - Assert.IsType(result); - AssertStatusCodeResult(result, 400, String.Format("The API key '{0}' is invalid.", guidValue)); + + // Assert + ResultAssert.IsStatusCode( + result, + HttpStatusCode.BadRequest, + String.Format("The API key '{0}' is invalid.", guidValue)); } [Fact] @@ -457,6 +453,7 @@ public void WillUseUserFromAuthenticateCredentialOverFindByApiKey() [Fact] public void WillFailIfUserFromAuthenticateCredentialIsNotOwner() { + // Arrange var apiKey = Guid.NewGuid(); var nonOwner = new Credential() { User = new User { Key = 1 } }; var owner = new User() { ApiKey = apiKey }; @@ -473,8 +470,12 @@ public void WillFailIfUserFromAuthenticateCredentialIsNotOwner() apiKey.ToString().ToLowerInvariant())) .Returns(nonOwner); + // Act + var result = controller.DeletePackage(apiKey.ToString(), "theId", "1.0.42"); + + // Assert ResultAssert.IsStatusCode( - controller.DeletePackage(apiKey.ToString(), "theId", "1.0.42"), + result, HttpStatusCode.Forbidden, String.Format(Strings.ApiKeyNotAuthorized, "delete")); @@ -646,11 +647,16 @@ public class ThePublishPackageAction [InlineData("this-is-bad-guid")] public void WillThrowIfTheApiKeyIsAnInvalidGuid(string guidValue) { + // Arrange var controller = new TestableApiController(); controller.MockUserService.Setup(x => x.FindByApiKey(It.IsAny())).Returns((User)null); + // Act + var result = controller.PublishPackage(guidValue, "theId", "1.0.42"); + + // Assert ResultAssert.IsStatusCode( - controller.PublishPackage(guidValue, "theId", "1.0.42"), + result, HttpStatusCode.BadRequest, String.Format("The API key '{0}' is invalid.", guidValue)); controller.MockPackageService.Verify(x => x.MarkPackageListed(It.IsAny(), It.IsAny()), Times.Never()); @@ -659,12 +665,17 @@ public void WillThrowIfTheApiKeyIsAnInvalidGuid(string guidValue) [Fact] public void WillThrowIfTheApiKeyDoesNotExist() { + // Arrange var controller = new TestableApiController(); var apiKey = Guid.NewGuid(); controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).ReturnsNull(); + // Act + var result = controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"); + + // Assert ResultAssert.IsStatusCode( - controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"), + result, HttpStatusCode.Forbidden, String.Format(Strings.ApiKeyNotAuthorized, "publish")); controller.MockPackageService.Verify(x => x.MarkPackageListed(It.IsAny(), It.IsAny()), Times.Never()); @@ -673,13 +684,18 @@ public void WillThrowIfTheApiKeyDoesNotExist() [Fact] public void WillThrowIfAPackageWithTheIdAndSemanticVersionDoesNotExist() { + // Arrange var controller = new TestableApiController(); var apiKey = Guid.NewGuid(); controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)).Returns((Package)null); controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(new User()); + // Act + var result = controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"); + + // Assert ResultAssert.IsStatusCode( - controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"), + result, HttpStatusCode.NotFound, String.Format(Strings.PackageWithIdAndVersionNotFound, "theId", "1.0.42")); controller.MockPackageService.Verify(x => x.MarkPackageListed(It.IsAny(), It.IsAny()), Times.Never()); @@ -688,6 +704,7 @@ public void WillThrowIfAPackageWithTheIdAndSemanticVersionDoesNotExist() [Fact] public void WillNotListThePackageIfApiKeyDoesNotBelongToAnOwner() { + // Arrange var owner = new User { Key = 1 }; var package = new Package { @@ -699,8 +716,12 @@ public void WillNotListThePackageIfApiKeyDoesNotBelongToAnOwner() controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)).Returns(package); controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(owner); + // Act + var result = controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"); + + // Assert ResultAssert.IsStatusCode( - controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"), + result, HttpStatusCode.Forbidden, String.Format(Strings.ApiKeyNotAuthorized, "publish")); @@ -710,6 +731,7 @@ public void WillNotListThePackageIfApiKeyDoesNotBelongToAnOwner() [Fact] public void WillListThePackageIfApiKeyBelongsToAnOwnerUsingFindByApiKey() { + // Arrange var apiKey = Guid.NewGuid(); var owner = new User { Key = 1, ApiKey = apiKey }; var package = new Package @@ -721,9 +743,11 @@ public void WillListThePackageIfApiKeyBelongsToAnOwnerUsingFindByApiKey() controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); controller.MockUserService.Setup(x => x.FindByApiKey(apiKey)).Returns(owner); - ResultAssert.IsEmpty( - controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42")); + // Act + var result = controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"); + // Assert + ResultAssert.IsEmpty(result); controller.MockPackageService.Verify(x => x.MarkPackageListed(package, It.IsAny())); controller.MockIndexingService.Verify(i => i.UpdatePackage(package)); } @@ -731,6 +755,8 @@ public void WillListThePackageIfApiKeyBelongsToAnOwnerUsingFindByApiKey() [Fact] public void WillListThePackageIfApiKeyBelongsToAnOwnerUsingAuthorizeCredential() { + // Arrange + var apiKey = Guid.NewGuid(); var owner = new Credential { User = new User { Key = 1 } }; var package = new Package @@ -746,9 +772,11 @@ public void WillListThePackageIfApiKeyBelongsToAnOwnerUsingAuthorizeCredential() apiKey.ToString().ToLowerInvariant())) .Returns(owner); - ResultAssert.IsEmpty( - controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42")); + // Act + var result = controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"); + // Assert + ResultAssert.IsEmpty(result); controller.MockPackageService.Verify(x => x.MarkPackageListed(package, It.IsAny())); controller.MockIndexingService.Verify(i => i.UpdatePackage(package)); } @@ -756,6 +784,7 @@ public void WillListThePackageIfApiKeyBelongsToAnOwnerUsingAuthorizeCredential() [Fact] public void WillFailIfUserFromAuthenticateCredentialIsNotOwner() { + // Arrange var apiKey = Guid.NewGuid(); var nonOwner = new Credential { User = new User { Key = 1 } }; var owner = new User(); @@ -773,8 +802,12 @@ public void WillFailIfUserFromAuthenticateCredentialIsNotOwner() apiKey.ToString().ToLowerInvariant())) .Returns(nonOwner); + // Act + var result = controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"); + + // Assert ResultAssert.IsStatusCode( - controller.PublishPackage(apiKey.ToString(), "theId", "1.0.42"), + result, HttpStatusCode.Forbidden, String.Format(Strings.ApiKeyNotAuthorized, "publish")); @@ -792,39 +825,74 @@ public void VerifyPackageKeyReturns403IfApiKeyIsInvalidGuid() // Act var result = controller.VerifyPackageKey("bad-guid", "foo", "1.0.0"); - + // Assert - AssertStatusCodeResult(result, 400, "The API key 'bad-guid' is invalid."); + ResultAssert.IsStatusCode( + result, + HttpStatusCode.BadRequest, + "The API key 'bad-guid' is invalid."); } [Fact] - public void VerifyPackageKeyReturns403IfUserDoesNotExist() + public void VerifyPackageKeyReturns403IfUserDoesNotExistByFindByApiKeyOrAuthorizeCredential() { // Arrange var guid = Guid.NewGuid(); var controller = new TestableApiController(); controller.MockUserService.Setup(s => s.FindByApiKey(guid)).Returns(null); + controller.MockUserService + .Setup(s => s.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + guid.ToString().ToLowerInvariant())) + .ReturnsNull(); // Act var result = controller.VerifyPackageKey(guid.ToString(), "foo", "1.0.0"); // Assert - AssertStatusCodeResult(result, 403, "The specified API key does not provide the authority to push packages."); + ResultAssert.IsStatusCode( + result, + HttpStatusCode.Forbidden, + "The specified API key does not provide the authority to push packages."); } [Fact] - public void VerifyPackageKeyReturnsEmptyResultIfApiKeyExistsAndIdAndVersionAreEmpty() + public void VerifyPackageKeyReturnsEmptyResultIfApiKeyExistsInUserRecordAndIdAndVersionAreEmpty() { // Arrange var guid = Guid.NewGuid(); var controller = new TestableApiController(); controller.MockUserService.Setup(s => s.FindByApiKey(guid)).Returns(new User()); + controller.MockUserService + .Setup(s => s.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + guid.ToString().ToLowerInvariant())) + .ReturnsNull(); // Act var result = controller.VerifyPackageKey(guid.ToString(), null, null); // Assert - Assert.IsType(result); + ResultAssert.IsEmpty(result); + } + + [Fact] + public void VerifyPackageKeyReturnsEmptyResultIfApiKeyExistsInCredentialsAndIdAndVersionAreEmpty() + { + // Arrange + var guid = Guid.NewGuid(); + var controller = new TestableApiController(); + controller.MockUserService + .Setup(s => s.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + guid.ToString().ToLowerInvariant())) + .Returns(new Credential() { User = new User() }); + + // Act + var result = controller.VerifyPackageKey(guid.ToString(), null, null); + + // Assert + ResultAssert.IsEmpty(result); } [Fact] @@ -838,18 +906,28 @@ public void VerifyPackageKeyReturns404IfPackageDoesNotExist() // Act var result = controller.VerifyPackageKey(guid.ToString(), "foo", "1.0.0"); - + // Assert - AssertStatusCodeResult(result, 404, "A package with id 'foo' and version '1.0.0' does not exist."); + ResultAssert.IsStatusCode( + result, + HttpStatusCode.NotFound, + "A package with id 'foo' and version '1.0.0' does not exist."); } [Fact] - public void VerifyPackageKeyReturns403IfUserIsNotAnOwner() + public void VerifyPackageKeyReturns403IfUserInCredentialsTableIsNotAnOwner() { // Arrange var guid = Guid.NewGuid(); var controller = new TestableApiController(); - controller.MockUserService.Setup(s => s.FindByApiKey(guid)).Returns(new User()); + var owner = new User(); + var nonOwner = new User(); + controller.MockUserService.Setup(s => s.FindByApiKey(guid)).Returns(owner); + controller.MockUserService + .Setup(s => s.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + guid.ToString().ToLowerInvariant())) + .Returns(new Credential() { User = nonOwner }); controller.MockPackageService.Setup(s => s.FindPackageByIdAndVersion("foo", "1.0.0", true)).Returns( new Package { PackageRegistration = new PackageRegistration() }); @@ -857,11 +935,14 @@ public void VerifyPackageKeyReturns403IfUserIsNotAnOwner() var result = controller.VerifyPackageKey(guid.ToString(), "foo", "1.0.0"); // Assert - AssertStatusCodeResult(result, 403, "The specified API key does not provide the authority to push packages."); + ResultAssert.IsStatusCode( + result, + HttpStatusCode.Forbidden, + "The specified API key does not provide the authority to push packages."); } [Fact] - public void VerifyPackageKeyReturns200IfUserIsAnOwner() + public void VerifyPackageKeyReturns200IfUserHasNoCredentialRecordButIsAnOwner() { // Arrange var guid = Guid.NewGuid(); @@ -869,17 +950,50 @@ public void VerifyPackageKeyReturns200IfUserIsAnOwner() var package = new Package { PackageRegistration = new PackageRegistration() }; package.PackageRegistration.Owners.Add(user); var controller = new TestableApiController(); + controller.MockUserService + .Setup(s => s.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + guid.ToString().ToLowerInvariant())) + .ReturnsNull(); controller.MockUserService.Setup(s => s.FindByApiKey(guid)).Returns(user); controller.MockPackageService.Setup(s => s.FindPackageByIdAndVersion("foo", "1.0.0", true)).Returns(package); // Act var result = controller.VerifyPackageKey(guid.ToString(), "foo", "1.0.0"); - Assert.IsType(result); + // Assert + ResultAssert.IsEmpty(result); } [Fact] - public async void VerifyRecentPopularityStatsDownloads() + public void VerifyPackageKeyReturns200IfUserHasCredentialRecordAndIsAnOwner() + { + // Arrange + var guid = Guid.NewGuid(); + var user = new User(); + var package = new Package { PackageRegistration = new PackageRegistration() }; + package.PackageRegistration.Owners.Add(user); + var controller = new TestableApiController(); + controller.MockUserService + .Setup(s => s.AuthenticateCredential( + Constants.CredentialTypes.ApiKeyV1, + guid.ToString().ToLowerInvariant())) + .Returns(new Credential() { User = user }); + controller.MockUserService.Setup(s => s.FindByApiKey(guid)).ReturnsNull(); + controller.MockPackageService.Setup(s => s.FindPackageByIdAndVersion("foo", "1.0.0", true)).Returns(package); + + // Act + var result = controller.VerifyPackageKey(guid.ToString(), "foo", "1.0.0"); + + // Assert + ResultAssert.IsEmpty(result); + } + } + + public class TheGetStatsDownloadsAction + { + [Fact] + public async Task VerifyRecentPopularityStatsDownloads() { JArray report = new JArray { @@ -926,7 +1040,7 @@ public async void VerifyRecentPopularityStatsDownloads() var fakeReportService = new Mock(); fakeReportService.Setup(x => x.Load("RecentPopularityDetail.json")).Returns(Task.FromResult(new StatisticsReport(fakePackageVersionReport, DateTime.UtcNow))); - + var controller = new TestableApiController { StatisticsService = new JsonStatisticsService(fakeReportService.Object), @@ -946,7 +1060,7 @@ public async void VerifyRecentPopularityStatsDownloads() } [Fact] - public async void VerifyStatsDownloadsReturnsNotFoundWhenStatsNotAvailable() + public async Task VerifyStatsDownloadsReturnsNotFoundWhenStatsNotAvailable() { var controller = new TestableApiController(); controller.MockStatisticsService.Setup(x => x.LoadDownloadPackageVersions()).Returns(Task.FromResult(StatisticsReportResult.Failed)); @@ -961,7 +1075,7 @@ public async void VerifyStatsDownloadsReturnsNotFoundWhenStatsNotAvailable() } [Fact] - public async void VerifyRecentPopularityStatsDownloadsCount() + public async Task VerifyRecentPopularityStatsDownloadsCount() { JArray report = new JArray { From 67060ab2e9cd2086711f34b34e7741d7f0b79123 Mon Sep 17 00:00:00 2001 From: anurse Date: Wed, 18 Sep 2013 16:36:48 -0700 Subject: [PATCH 09/13] Updated GenerateApiKey action to use new table (and to clear out old and busted API key) --- src/NuGetGallery.Core/Entities/User.cs | 2 +- .../Controllers/UsersController.cs | 14 +++++++-- .../201309172217450_CredentialsTable.cs | 6 +++- .../201309172217450_CredentialsTable.resx | 2 +- .../Controllers/UsersControllerFacts.cs | 31 ++++++++++++++++--- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/NuGetGallery.Core/Entities/User.cs b/src/NuGetGallery.Core/Entities/User.cs index 72ad48fa2a..d599e39f97 100644 --- a/src/NuGetGallery.Core/Entities/User.cs +++ b/src/NuGetGallery.Core/Entities/User.cs @@ -21,7 +21,7 @@ public User( Username = username; } - public Guid ApiKey { get; set; } + public Guid? ApiKey { get; set; } [StringLength(256)] public string EmailAddress { get; set; } diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index 31f9a11ca7..ada8ca8a99 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -193,9 +193,17 @@ public virtual ActionResult Packages() [HttpPost] public virtual ActionResult GenerateApiKey() { - UserService.ReplaceCredential( - User.Identity.Name, - CredentialBuilder.CreateV1ApiKey()); + // Get the user + var user = UserService.FindByUsername(User.Identity.Name); + + // Clear the existing API Key, if there is one + if (user.ApiKey != null) + { + user.ApiKey = null; + } + + // Add/Replace the API Key credential, and save to the database + UserService.ReplaceCredential(user, CredentialBuilder.CreateV1ApiKey()); return RedirectToAction(MVC.Users.Account()); } diff --git a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs index f260c098d4..2b7fee4da2 100644 --- a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs +++ b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs @@ -20,19 +20,23 @@ public override void Up() .PrimaryKey(t => t.Key) .ForeignKey("dbo.Users", t => t.UserKey, cascadeDelete: true) .Index(t => t.UserKey); - + CreateIndex( "dbo.Credentials", new[] { "Type", "Value" }, unique: true, name: "IX_Credentials_Type_Value"); + + AlterColumn("dbo.Users", "ApiKey", c => c.Guid()); } public override void Down() { DropIndex("dbo.Credentials", new[] { "UserKey" }); DropIndex("dbo.Credentials", "IX_Credentials_Type_Value"); + DropForeignKey("dbo.Credentials", "UserKey", "dbo.Users"); + AlterColumn("dbo.Users", "ApiKey", c => c.Guid(nullable: false)); DropTable("dbo.Credentials"); } } diff --git a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx index dabe134f2a..47cd308ef3 100644 --- a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx +++ b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx @@ -118,6 +118,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 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 \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs index 9e3d9fc96b..31b7524628 100644 --- a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs @@ -393,23 +393,44 @@ public void RedirectsToAccountPage() var result = controller.GenerateApiKey() as RedirectToRouteResult; - Assert.NotNull(result); - Assert.Equal("Account", result.RouteValues["action"]); - Assert.Equal("Users", result.RouteValues["controller"]); + ResultAssert.IsRedirectToRoute(result, new { action = "Account", controller = "Users" }); } [Fact] - public void GeneratesAnApiKey() + public void ClearsOldApiKeyField() { var controller = new TestableUsersController(); + var user = new User() { ApiKey = Guid.NewGuid() }; controller.MockCurrentIdentity .Setup(i => i.Name) .Returns("the-username"); + controller.MockUserService + .Setup(u => u.FindByUsername("the-username")) + .Returns(user); controller.GenerateApiKey(); + Assert.Null(user.ApiKey); + } + + [Fact] + public void ReplacesTheApiKeyCredential() + { + var controller = new TestableUsersController(); + var user = new User(); + controller.MockCurrentIdentity + .Setup(i => i.Name) + .Returns("the-username"); + controller.MockUserService + .Setup(u => u.FindByUsername("the-username")) + .Returns(user); + + controller.GenerateApiKey(); + controller.MockUserService - .Verify(s => s.GenerateApiKey("the-username")); + .Verify(u => u.ReplaceCredential( + user, + It.Is(c => c.Type == Constants.CredentialTypes.ApiKeyV1))); } } From c6eb576a4adfb9cfdece938aec65ab88bdecf82d Mon Sep 17 00:00:00 2001 From: anurse Date: Thu, 19 Sep 2013 11:12:11 -0700 Subject: [PATCH 10/13] Changed GenerateApiKey action to update old col --- src/NuGetGallery.Core/Entities/User.cs | 2 +- .../Controllers/UsersController.cs | 14 +++--- .../Infrastructure/CredentialBuilder.cs | 7 ++- src/NuGetGallery/Services/UserService.cs | 7 ++- .../Controllers/UsersControllerFacts.cs | 34 ++++++++----- .../Services/UserServiceFacts.cs | 48 +++++++++++++++---- .../TestUtils/ResultAssert.cs | 6 ++- 7 files changed, 83 insertions(+), 35 deletions(-) diff --git a/src/NuGetGallery.Core/Entities/User.cs b/src/NuGetGallery.Core/Entities/User.cs index d599e39f97..72ad48fa2a 100644 --- a/src/NuGetGallery.Core/Entities/User.cs +++ b/src/NuGetGallery.Core/Entities/User.cs @@ -21,7 +21,7 @@ public User( Username = username; } - public Guid? ApiKey { get; set; } + public Guid ApiKey { get; set; } [StringLength(256)] public string EmailAddress { get; set; } diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index ada8ca8a99..24701170be 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -194,16 +194,16 @@ public virtual ActionResult Packages() public virtual ActionResult GenerateApiKey() { // Get the user - var user = UserService.FindByUsername(User.Identity.Name); + var user = UserService.FindByUsername(CurrentUser.Identity.Name); - // Clear the existing API Key, if there is one - if (user.ApiKey != null) - { - user.ApiKey = null; - } + // Generate an API Key + var apiKey = Guid.NewGuid(); + + // Set the existing API Key field + user.ApiKey = apiKey; // Add/Replace the API Key credential, and save to the database - UserService.ReplaceCredential(user, CredentialBuilder.CreateV1ApiKey()); + UserService.ReplaceCredential(user, CredentialBuilder.CreateV1ApiKey(apiKey)); return RedirectToAction(MVC.Users.Account()); } diff --git a/src/NuGetGallery/Infrastructure/CredentialBuilder.cs b/src/NuGetGallery/Infrastructure/CredentialBuilder.cs index 94eb6afd7a..db673fac80 100644 --- a/src/NuGetGallery/Infrastructure/CredentialBuilder.cs +++ b/src/NuGetGallery/Infrastructure/CredentialBuilder.cs @@ -12,7 +12,12 @@ public static class CredentialBuilder { public static Credential CreateV1ApiKey() { - var value = Guid.NewGuid() + return CreateV1ApiKey(Guid.NewGuid()); + } + + public static Credential CreateV1ApiKey(Guid apiKey) + { + var value = apiKey .ToString() .ToLowerInvariant(); return new Credential(Constants.CredentialTypes.ApiKeyV1, value); diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index 791a3d37a8..5036fd910f 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -48,11 +48,12 @@ public virtual User Create( var hashedPassword = Crypto.GenerateSaltedHash(password, Constants.PBKDF2HashAlgorithmId); + var apiKey = Guid.NewGuid(); var newUser = new User( username, hashedPassword) { - ApiKey = Guid.NewGuid(), + ApiKey = apiKey, EmailAllowed = true, UnconfirmedEmailAddress = emailAddress, EmailConfirmationToken = Crypto.GenerateToken(), @@ -60,6 +61,10 @@ public virtual User Create( CreatedUtc = DateTime.UtcNow }; + // Add a credential for the password and the API Key + newUser.Credentials.Add(CredentialBuilder.CreateV1ApiKey(apiKey)); + newUser.Credentials.Add(new Credential(Constants.CredentialTypes.PasswordPbkdf2, newUser.HashedPassword)); + if (!Config.ConfirmEmailAddresses) { newUser.ConfirmEmailAddress(); diff --git a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs index 31b7524628..3c4641c1de 100644 --- a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs @@ -386,31 +386,39 @@ public class TheGenerateApiKeyMethod [Fact] public void RedirectsToAccountPage() { + var user = new User(); var controller = new TestableUsersController(); controller.MockCurrentIdentity - .Setup(i => i.Name) - .Returns("the-username"); + .Setup(i => i.Name) + .Returns("the-username"); + controller.MockUserService + .Setup(u => u.FindByUsername("the-username")) + .Returns(user); - var result = controller.GenerateApiKey() as RedirectToRouteResult; + var result = controller.GenerateApiKey(); ResultAssert.IsRedirectToRoute(result, new { action = "Account", controller = "Users" }); } [Fact] - public void ClearsOldApiKeyField() + public void PutsNewCredentialInOldField() { var controller = new TestableUsersController(); var user = new User() { ApiKey = Guid.NewGuid() }; controller.MockCurrentIdentity - .Setup(i => i.Name) - .Returns("the-username"); + .Setup(i => i.Name) + .Returns("the-username"); controller.MockUserService - .Setup(u => u.FindByUsername("the-username")) - .Returns(user); + .Setup(u => u.FindByUsername("the-username")) + .Returns(user); + Credential created = null; + controller.MockUserService + .Setup(u => u.ReplaceCredential(user, It.IsAny())) + .Callback((_, c) => created = c); controller.GenerateApiKey(); - Assert.Null(user.ApiKey); + Assert.Equal(created.Value, user.ApiKey.ToString().ToLowerInvariant()); } [Fact] @@ -419,11 +427,11 @@ public void ReplacesTheApiKeyCredential() var controller = new TestableUsersController(); var user = new User(); controller.MockCurrentIdentity - .Setup(i => i.Name) - .Returns("the-username"); + .Setup(i => i.Name) + .Returns("the-username"); controller.MockUserService - .Setup(u => u.FindByUsername("the-username")) - .Returns(user); + .Setup(u => u.FindByUsername("the-username")) + .Returns(user); controller.GenerateApiKey(); diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 714fdf25d2..88478b5143 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -34,17 +34,17 @@ public static User CreateAUser( }; } - public static bool VerifyPasswordHash(User user, string password) + public static bool VerifyPasswordHash(string hash, string algorithm, string password) { bool canAuthenticate = CryptographyService.ValidateSaltedHash( - user.HashedPassword, + hash, password, - user.PasswordHashAlgorithm); + algorithm); bool sanity = CryptographyService.ValidateSaltedHash( - user.HashedPassword, + hash, "not_the_password", - user.PasswordHashAlgorithm); + algorithm); return canAuthenticate && !sanity; } @@ -160,7 +160,7 @@ public void UpdatesTheHashedPassword() .Setup(r => r.GetAll()).Returns(new[] { user }.AsQueryable()); var changed = service.ChangePassword("user", "oldpwd", "newpwd"); - Assert.True(VerifyPasswordHash(user, "newpwd")); + Assert.True(VerifyPasswordHash(user.HashedPassword, user.PasswordHashAlgorithm, "newpwd")); } [Fact] @@ -178,7 +178,7 @@ public void MigratesPasswordIfHashAlgorithmIsNotPBKDF2() var changed = service.ChangePassword("user", "oldpwd", "newpwd"); Assert.True(changed); - Assert.True(VerifyPasswordHash(user, "newpwd")); + Assert.True(VerifyPasswordHash(user.HashedPassword, user.PasswordHashAlgorithm, "newpwd")); Assert.Equal("PBKDF2", user.PasswordHashAlgorithm); } } @@ -299,7 +299,7 @@ public void WillHashThePassword() "theEmailAddress"); Assert.Equal("PBKDF2", user.PasswordHashAlgorithm); - Assert.True(VerifyPasswordHash(user, "thePassword")); + Assert.True(VerifyPasswordHash(user.HashedPassword, user.PasswordHashAlgorithm, "thePassword")); } [Fact] @@ -323,6 +323,28 @@ public void WillSaveTheNewUser() .Verify(x => x.CommitChanges()); } + [Fact] + public void WillSaveThePasswordInTheCredentialsTable() + { + var userService = new TestableUserService(); + + var user = userService.Create( + "theUsername", + "thePassword", + "theEmailAddress"); + + Assert.NotNull(user); + var passwordCred = user.Credentials.FirstOrDefault(c => c.Type == Constants.CredentialTypes.PasswordPbkdf2); + Assert.NotNull(passwordCred); + Assert.Equal(Constants.CredentialTypes.PasswordPbkdf2, passwordCred.Type); + Assert.True(VerifyPasswordHash(passwordCred.Value, Constants.PBKDF2HashAlgorithmId, "thePassword")); + + userService.MockUserRepository + .Verify(x => x.InsertOnCommit(user)); + userService.MockUserRepository + .Verify(x => x.CommitChanges()); + } + [Fact] public void WillSaveTheNewUserAsConfirmedWhenConfigured() { @@ -357,7 +379,13 @@ public void SetsAnApiKey() "thePassword", "theEmailAddress"); + userService.MockUserRepository + .Verify(x => x.InsertOnCommit(user)); Assert.NotEqual(Guid.Empty, user.ApiKey); + + var apiKeyCred = user.Credentials.FirstOrDefault(c => c.Type == Constants.CredentialTypes.ApiKeyV1); + Assert.NotNull(apiKeyCred); + Assert.Equal(user.ApiKey.ToString().ToLowerInvariant(), apiKeyCred.Value); } [Fact] @@ -852,7 +880,7 @@ public void ResetsPasswordAndPasswordTokenAndPasswordTokenDate() bool result = userService.ResetPasswordWithToken("user", "some-token", "new-password"); Assert.True(result); - Assert.True(VerifyPasswordHash(user, "new-password")); + Assert.True(VerifyPasswordHash(user.HashedPassword, user.PasswordHashAlgorithm, "new-password")); Assert.Null(user.PasswordResetToken); Assert.Null(user.PasswordResetTokenExpirationDate); userService.MockUserRepository.Verify(u => u.CommitChanges()); @@ -879,7 +907,7 @@ public void ResetsPasswordMigratesPasswordHash() Assert.True(result); Assert.Equal("PBKDF2", user.PasswordHashAlgorithm); - Assert.True(VerifyPasswordHash(user, "new-password")); + Assert.True(VerifyPasswordHash(user.HashedPassword, user.PasswordHashAlgorithm, "new-password")); Assert.Null(user.PasswordResetToken); Assert.Null(user.PasswordResetTokenExpirationDate); userService.MockUserRepository diff --git a/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs b/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs index afd272f4b8..b2169980c2 100644 --- a/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs +++ b/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs @@ -45,9 +45,11 @@ public static ViewResult IsView(ActionResult result, string viewName = "", strin return view; } - private static void DictionariesMatch(IDictionary expected, IDictionary actual) + private static void DictionariesMatch(IDictionary expected, IDictionary actual) { - var expectedKeys = expected.Keys.Cast().ToList(); + var expectedKeys = new HashSet( + expected.Keys, + StringComparer.OrdinalIgnoreCase); foreach (var key in actual.Keys) { From d8ea0c7911b009ca60f39596413411be27aae2c9 Mon Sep 17 00:00:00 2001 From: anurse Date: Thu, 19 Sep 2013 12:16:24 -0700 Subject: [PATCH 11/13] Updated Password-management service methods --- .../Infrastructure/CredentialBuilder.cs | 7 ++ src/NuGetGallery/Services/UserService.cs | 30 ++++--- .../Controllers/UsersControllerFacts.cs | 81 +++++++++++++++++++ .../Services/UserServiceFacts.cs | 58 ++++++++++++- .../TestUtils/ResultAssert.cs | 9 ++- 5 files changed, 168 insertions(+), 17 deletions(-) diff --git a/src/NuGetGallery/Infrastructure/CredentialBuilder.cs b/src/NuGetGallery/Infrastructure/CredentialBuilder.cs index db673fac80..d2f735ae95 100644 --- a/src/NuGetGallery/Infrastructure/CredentialBuilder.cs +++ b/src/NuGetGallery/Infrastructure/CredentialBuilder.cs @@ -22,5 +22,12 @@ public static Credential CreateV1ApiKey(Guid apiKey) .ToLowerInvariant(); return new Credential(Constants.CredentialTypes.ApiKeyV1, value); } + + public static Credential CreatePbkdf2Password(string plaintextPassword) + { + return new Credential( + Constants.CredentialTypes.PasswordPbkdf2, + CryptographyService.GenerateSaltedHash(plaintextPassword, Constants.PBKDF2HashAlgorithmId)); + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index 5036fd910f..7716c534cc 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -286,16 +286,7 @@ public void ReplaceCredential(string userName, Credential credential) public void ReplaceCredential(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); - } - - user.Credentials.Add(credential); + ReplaceCredentialInternal(user, credential); UserRepository.CommitChanges(); } @@ -334,9 +325,24 @@ private static User AuthenticatePassword(string password, User user) private static void ChangePasswordInternal(User user, string newPassword) { - var hashedPassword = Crypto.GenerateSaltedHash(newPassword, Constants.PBKDF2HashAlgorithmId); + var cred = CredentialBuilder.CreatePbkdf2Password(newPassword); user.PasswordHashAlgorithm = Constants.PBKDF2HashAlgorithmId; - user.HashedPassword = hashedPassword; + user.HashedPassword = cred.Value; + ReplaceCredentialInternal(user, cred); + } + + private static 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); + } + + user.Credentials.Add(credential); } } } diff --git a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs index 3c4641c1de..9e21aac077 100644 --- a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs @@ -538,6 +538,87 @@ public void WillSendNewUserEmailIfConfirmationRequired() } } + public class TheChangePasswordMethod + { + [Fact] + public void ReturnsViewIfModelStateInvalid() + { + // Arrange + var controller = new TestableUsersController(); + controller.ModelState.AddModelError("test", "test"); + var inputModel = new PasswordChangeViewModel(); + + // Act + var result = controller.ChangePassword(inputModel); + + // Assert + var outputModel = ResultAssert.IsView(result); + Assert.Same(inputModel, outputModel); + } + + [Fact] + public void AddsModelErrorIfUserServiceFails() + { + // Arrange + var controller = new TestableUsersController(); + controller.MockCurrentIdentity + .Setup(i => i.Name) + .Returns("user"); + controller.MockUserService + .Setup(u => u.ChangePassword("user", "old", "new")) + .Returns(false); + var inputModel = new PasswordChangeViewModel() + { + OldPassword = "old", + NewPassword = "new", + ConfirmPassword = "new" + }; + + // Act + var result = controller.ChangePassword(inputModel); + + // Assert + var outputModel = ResultAssert.IsView(result); + Assert.Same(inputModel, outputModel); + + var errorMessages = controller + .ModelState["OldPassword"] + .Errors + .Select(e => e.ErrorMessage) + .ToArray(); + Assert.Equal(errorMessages, new[] { Strings.CurrentPasswordIncorrect }); + } + + [Fact] + public void RedirectsToPasswordChangedIfUserServiceSucceeds() + { + // Arrange + var controller = new TestableUsersController(); + controller.MockCurrentIdentity + .Setup(i => i.Name) + .Returns("user"); + controller.MockUserService + .Setup(u => u.ChangePassword("user", "old", "new")) + .Returns(true); + var inputModel = new PasswordChangeViewModel() + { + OldPassword = "old", + NewPassword = "new", + ConfirmPassword = "new" + }; + + // Act + var result = controller.ChangePassword(inputModel); + + // Assert + ResultAssert.IsRedirectToRoute(result, new + { + controller = "Users", + action = "PasswordChanged" + }); + } + } + public class TheResetPasswordMethod { [Fact] diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 88478b5143..14eac4aa05 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -161,6 +161,29 @@ public void UpdatesTheHashedPassword() var changed = service.ChangePassword("user", "oldpwd", "newpwd"); Assert.True(VerifyPasswordHash(user.HashedPassword, user.PasswordHashAlgorithm, "newpwd")); + service.MockUserRepository.VerifyCommitted(); + } + + [Fact] + public void UpdatesThePasswordCredential() + { + var hash = CryptographyService.GenerateSaltedHash("oldpwd", "PBKDF2"); + var user = new User { + Username = "user", + Credentials = new List() + { + new Credential(Constants.CredentialTypes.PasswordPbkdf2, hash) + } + }; + var service = new TestableUserService(); + service.MockUserRepository + .Setup(r => r.GetAll()).Returns(new[] { user }.AsQueryable()); + + var changed = service.ChangePassword("user", "oldpwd", "newpwd"); + var cred = user.Credentials.Single(); + Assert.Equal(Constants.CredentialTypes.PasswordPbkdf2, cred.Type); + Assert.True(VerifyPasswordHash(cred.Value, Constants.PBKDF2HashAlgorithmId, "newpwd")); + service.MockUserRepository.VerifyCommitted(); } [Fact] @@ -180,6 +203,7 @@ public void MigratesPasswordIfHashAlgorithmIsNotPBKDF2() Assert.True(changed); Assert.True(VerifyPasswordHash(user.HashedPassword, user.PasswordHashAlgorithm, "newpwd")); Assert.Equal("PBKDF2", user.PasswordHashAlgorithm); + service.MockUserRepository.VerifyCommitted(); } } @@ -883,7 +907,36 @@ public void ResetsPasswordAndPasswordTokenAndPasswordTokenDate() Assert.True(VerifyPasswordHash(user.HashedPassword, user.PasswordHashAlgorithm, "new-password")); Assert.Null(user.PasswordResetToken); Assert.Null(user.PasswordResetTokenExpirationDate); - userService.MockUserRepository.Verify(u => u.CommitChanges()); + userService.MockUserRepository.VerifyCommitted(); + } + + [Fact] + public void ResetsPasswordCredential() + { + var oldCred = CredentialBuilder.CreatePbkdf2Password("thePassword"); + var user = new User + { + Username = "user", + EmailAddress = "confirmed@example.com", + PasswordResetToken = "some-token", + PasswordResetTokenExpirationDate = DateTime.UtcNow.AddDays(1), + HashedPassword = oldCred.Value, + PasswordHashAlgorithm = Constants.PBKDF2HashAlgorithmId, + Credentials = new List() { oldCred } + }; + + var userService = new TestableUserService(); + userService.MockUserRepository + .Setup(r => r.GetAll()) + .Returns(new[] { user }.AsQueryable()); + + bool result = userService.ResetPasswordWithToken("user", "some-token", "new-password"); + + Assert.True(result); + var newCred = user.Credentials.Single(); + Assert.Equal(Constants.CredentialTypes.PasswordPbkdf2, newCred.Type); + Assert.True(VerifyPasswordHash(newCred.Value, Constants.PBKDF2HashAlgorithmId, "new-password")); + userService.MockUserRepository.VerifyCommitted(); } [Fact] @@ -910,8 +963,7 @@ public void ResetsPasswordMigratesPasswordHash() Assert.True(VerifyPasswordHash(user.HashedPassword, user.PasswordHashAlgorithm, "new-password")); Assert.Null(user.PasswordResetToken); Assert.Null(user.PasswordResetTokenExpirationDate); - userService.MockUserRepository - .Verify(u => u.CommitChanges()); + userService.MockUserRepository.VerifyCommitted(); } } diff --git a/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs b/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs index b2169980c2..c227dc1faf 100644 --- a/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs +++ b/tests/NuGetGallery.Facts/TestUtils/ResultAssert.cs @@ -26,13 +26,12 @@ public static RedirectToRouteResult IsRedirectToRoute(ActionResult result, objec return redirect; } - public static ViewResult IsView(ActionResult result, string viewName = "", string masterName = "", object model = null, object viewData = null) + public static ViewResult IsView(ActionResult result, string viewName = "", string masterName = "", object viewData = null) { var view = Assert.IsType(result); Assert.Equal(viewName, view.ViewName); Assert.Equal(masterName, view.MasterName); - Assert.Equal(model, view.ViewData.Model); if (viewData != null) { @@ -45,6 +44,12 @@ public static ViewResult IsView(ActionResult result, string viewName = "", strin return view; } + public static TModel IsView(ActionResult result, string viewName = "", string masterName = "", object viewData = null) + { + var model = Assert.IsType(IsView(result, viewName, masterName, viewData).Model); + return model; + } + private static void DictionariesMatch(IDictionary expected, IDictionary actual) { var expectedKeys = new HashSet( From 264e173e1c866804e1deb180de92dab1ae9e9070 Mon Sep 17 00:00:00 2001 From: anurse Date: Thu, 19 Sep 2013 12:19:32 -0700 Subject: [PATCH 12/13] Rescaffolded CredentialsTable migration --- .../Migrations/201309172217450_CredentialsTable.cs | 14 +++++--------- .../201309172217450_CredentialsTable.resx | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs index 2b7fee4da2..bdbb8973b5 100644 --- a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs +++ b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.cs @@ -20,23 +20,19 @@ public override void Up() .PrimaryKey(t => t.Key) .ForeignKey("dbo.Users", t => t.UserKey, cascadeDelete: true) .Index(t => t.UserKey); - - CreateIndex( - "dbo.Credentials", - new[] { "Type", "Value" }, - unique: true, - name: "IX_Credentials_Type_Value"); - AlterColumn("dbo.Users", "ApiKey", c => c.Guid()); + CreateIndex( + "dbo.Credentials", + new[] { "Type", "Value" }, + unique: true, + name: "IX_Credentials_Type_Value"); } public override void Down() { DropIndex("dbo.Credentials", new[] { "UserKey" }); DropIndex("dbo.Credentials", "IX_Credentials_Type_Value"); - DropForeignKey("dbo.Credentials", "UserKey", "dbo.Users"); - AlterColumn("dbo.Users", "ApiKey", c => c.Guid(nullable: false)); DropTable("dbo.Credentials"); } } diff --git a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx index 47cd308ef3..dabe134f2a 100644 --- a/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx +++ b/src/NuGetGallery/Migrations/201309172217450_CredentialsTable.resx @@ -118,6 +118,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 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 \ No newline at end of file From d56eceedddc0856b195fc2c88e011649296910a8 Mon Sep 17 00:00:00 2001 From: anurse Date: Thu, 19 Sep 2013 16:04:14 -0700 Subject: [PATCH 13/13] Fixed Account action to display API Key from cred --- .../App_Start/ContainerBindings.cs | 4 + .../Controllers/UsersController.cs | 7 +- src/NuGetGallery/Services/UserService.cs | 11 ++- .../Controllers/UsersControllerFacts.cs | 90 ++++++++++++++----- .../Services/UserServiceFacts.cs | 2 + 5 files changed, 86 insertions(+), 28 deletions(-) diff --git a/src/NuGetGallery/App_Start/ContainerBindings.cs b/src/NuGetGallery/App_Start/ContainerBindings.cs index 360524c6d5..bddb1947ed 100644 --- a/src/NuGetGallery/App_Start/ContainerBindings.cs +++ b/src/NuGetGallery/App_Start/ContainerBindings.cs @@ -89,6 +89,10 @@ public override void Load() .To>() .InRequestScope(); + Bind>() + .To>() + .InRequestScope(); + Bind() .To() .InRequestScope(); diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index 24701170be..13dd82cded 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -39,10 +39,15 @@ public virtual ActionResult Account() { var user = UserService.FindByUsername(CurrentUser.Identity.Name); var curatedFeeds = CuratedFeedService.GetFeedsForManager(user.Key); + var apiCredential = user + .Credentials + .FirstOrDefault(c => c.Type == Constants.CredentialTypes.ApiKeyV1); return View( new AccountViewModel { - ApiKey = user.ApiKey.ToString(), + ApiKey = apiCredential == null ? + user.ApiKey.ToString() : + apiCredential.Value, CuratedFeeds = curatedFeeds.Select(cf => cf.Name) }); } diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index 7716c534cc..018907a500 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -125,15 +125,13 @@ public virtual User FindByUsername(string username) { return UserRepository.GetAll() .Include(u => u.Roles) + .Include(u => u.Credentials) .SingleOrDefault(u => u.Username == username); } public virtual User FindByUsernameAndPassword(string username, string password) { - var user = UserRepository.GetAll() - .Include(u => u.Roles) - .Include(u => u.Credentials) - .SingleOrDefault(u => u.Username == username); + var user = FindByUsername(username); return AuthenticatePassword(password, user); } @@ -323,7 +321,7 @@ private static User AuthenticatePassword(string password, User user) return valid ? user : null; } - private static void ChangePasswordInternal(User user, string newPassword) + private void ChangePasswordInternal(User user, string newPassword) { var cred = CredentialBuilder.CreatePbkdf2Password(newPassword); user.PasswordHashAlgorithm = Constants.PBKDF2HashAlgorithmId; @@ -331,7 +329,7 @@ private static void ChangePasswordInternal(User user, string newPassword) ReplaceCredentialInternal(user, cred); } - private static void ReplaceCredentialInternal(User user, Credential credential) + private void ReplaceCredentialInternal(User user, Credential credential) { // Find the credentials we're replacing, if any var creds = user.Credentials @@ -340,6 +338,7 @@ private static void ReplaceCredentialInternal(User user, Credential credential) foreach (var cred in creds) { user.Credentials.Remove(cred); + CredentialRepository.DeleteOnCommit(cred); } user.Credentials.Add(credential); diff --git a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs index 9e21aac077..e56698f102 100644 --- a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net.Mail; using System.Security.Principal; @@ -24,7 +25,7 @@ public void WillGetTheCurrentUserUsingTheRequestIdentityName() controller.MockUserService .Setup(s => s.FindByUsername(It.IsAny())) .Returns(new User { Key = 42 }); - + //act controller.Account(); @@ -40,7 +41,7 @@ public void WillGetCuratedFeedsManagedByTheCurrentUser() controller.MockUserService .Setup(s => s.FindByUsername(It.IsAny())) .Returns(new User { Key = 42 }); - + // act controller.Account(); @@ -57,7 +58,7 @@ public void WillReturnTheAccountViewModelWithTheUserApiKey() controller.MockUserService .Setup(s => s.FindByUsername(It.IsAny())) .Returns(new User { Key = 42, ApiKey = stubApiKey }); - + // act var model = ((ViewResult)controller.Account()).Model as AccountViewModel; @@ -75,13 +76,60 @@ public void WillReturnTheAccountViewModelWithTheCuratedFeeds() controller.MockCuratedFeedService .Setup(stub => stub.GetFeedsForManager(It.IsAny())) .Returns(new[] { new CuratedFeed { Name = "theCuratedFeed" } }); - + // act var model = ((ViewResult)controller.Account()).Model as AccountViewModel; // verify Assert.Equal("theCuratedFeed", model.CuratedFeeds.First()); } + + [Fact] + public void WillUseApiKeyInColumnIfNoCredentialPresent() + { + var apiKey = Guid.NewGuid(); + var controller = new TestableUsersController(); + controller.MockUserService + .Setup(s => s.FindByUsername(It.IsAny())) + .Returns(new User { Key = 42, ApiKey = apiKey }); + controller.MockCuratedFeedService + .Setup(stub => stub.GetFeedsForManager(It.IsAny())) + .Returns(new[] { new CuratedFeed { Name = "theCuratedFeed" } }); + + // act + var result = controller.Account(); + + // verify + var model = ResultAssert.IsView(result); + Assert.Equal(apiKey.ToString().ToLowerInvariant(), model.ApiKey); + } + + [Fact] + public void WillUseApiKeyInCredentialIfPresent() + { + var apiKey = Guid.NewGuid(); + var controller = new TestableUsersController(); + controller.MockUserService + .Setup(s => s.FindByUsername(It.IsAny())) + .Returns(new User + { + Key = 42, + ApiKey = Guid.NewGuid(), + Credentials = new List() { + CredentialBuilder.CreateV1ApiKey(apiKey) + } + }); + controller.MockCuratedFeedService + .Setup(stub => stub.GetFeedsForManager(It.IsAny())) + .Returns(new[] { new CuratedFeed { Name = "theCuratedFeed" } }); + + // act + var result = controller.Account(); + + // verify + var model = ResultAssert.IsView(result); + Assert.Equal(apiKey.ToString().ToLowerInvariant(), model.ApiKey); + } } public class TheConfirmMethod @@ -111,7 +159,7 @@ public void ReturnsConfirmedWhenTokenMatchesUser() controller.MockUserService .Setup(u => u.ConfirmEmailAddress(user, "the-token")) .Returns(true); - + var model = (controller.Confirm("username", "the-token") as ViewResult).Model as EmailConfirmationModel; Assert.True(model.SuccessfulConfirmation); @@ -133,7 +181,7 @@ public void SendsAccountChangedNoticeWhenConfirmingChangedEmail() controller.MockUserService .Setup(u => u.ConfirmEmailAddress(user, "the-token")) .Returns(true); - + var model = (controller.Confirm("username", "the-token") as ViewResult).Model as EmailConfirmationModel; Assert.True(model.SuccessfulConfirmation); @@ -158,7 +206,7 @@ public void DoesntSendAccountChangedEmailsWhenNoOldConfirmedAddress() controller.MockUserService .Setup(u => u.ConfirmEmailAddress(user, "the-token")) .Returns(true); - + // act: var model = (controller.Confirm("username", "the-token") as ViewResult).Model as EmailConfirmationModel; @@ -187,7 +235,7 @@ public void DoesntSendAccountChangedEmailsIfConfirmationTokenDoesntMatch() controller.MockUserService .Setup(u => u.ConfirmEmailAddress(user, "faketoken")) .Returns(false); - + // act: var model = (controller.Confirm("username", "faketoken") as ViewResult).Model as EmailConfirmationModel; @@ -216,7 +264,7 @@ public void ReturnsFalseWhenTokenDoesNotMatchUser() controller.MockUserService .Setup(u => u.ConfirmEmailAddress(user, "not-the-token")) .Returns(false); - + var model = (controller.Confirm("username", "not-the-token") as ViewResult).Model as EmailConfirmationModel; Assert.False(model.SuccessfulConfirmation); @@ -311,7 +359,7 @@ public void WithInvalidUsernameReturnsFileNotFound() controller.MockUserService .Setup(u => u.FindByUsername(It.IsAny())) .ReturnsNull(); - + var result = controller.Edit(new EditProfileViewModel()) as HttpNotFoundResult; Assert.NotNull(result); @@ -371,9 +419,9 @@ public void ReturnsSameViewIfTokenGenerationFails() controller.MockUserService .Setup(s => s.GeneratePasswordResetToken("user", 1440)) .Returns((User)null); - + var model = new ForgotPasswordViewModel { Email = "user" }; - + var result = controller.ForgotPassword(model) as ViewResult; Assert.NotNull(result); @@ -394,7 +442,7 @@ public void RedirectsToAccountPage() controller.MockUserService .Setup(u => u.FindByUsername("the-username")) .Returns(user); - + var result = controller.GenerateApiKey(); ResultAssert.IsRedirectToRoute(result, new { action = "Account", controller = "Users" }); @@ -415,7 +463,7 @@ public void PutsNewCredentialInOldField() controller.MockUserService .Setup(u => u.ReplaceCredential(user, It.IsAny())) .Callback((_, c) => created = c); - + controller.GenerateApiKey(); Assert.Equal(created.Value, user.ApiKey.ToString().ToLowerInvariant()); @@ -437,7 +485,7 @@ public void ReplacesTheApiKeyCredential() controller.MockUserService .Verify(u => u.ReplaceCredential( - user, + user, It.Is(c => c.Type == Constants.CredentialTypes.ApiKeyV1))); } } @@ -463,7 +511,7 @@ public void WillCreateTheUser() controller.MockUserService .Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new User { Username = "theUsername", EmailAddress = "to@example.com" }); - + controller.Register( new RegisterRequest { @@ -483,7 +531,7 @@ public void WillInvalidateModelStateAndShowTheViewWhenAnEntityExceptionIsThrow() controller.MockUserService .Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new EntityException("aMessage")); - + var result = controller.Register( new RegisterRequest { @@ -522,7 +570,7 @@ public void WillSendNewUserEmailIfConfirmationRequired() controller.MockConfig .Setup(x => x.ConfirmEmailAddresses) .Returns(true); - + controller.Register( new RegisterRequest { @@ -671,7 +719,7 @@ public void ShowsDefaultThanksViewWhenConfirmingEmailAddressIsRequired() controller.MockConfig .Setup(x => x.ConfirmEmailAddresses) .Returns(true); - + var result = controller.Thanks() as ViewResult; Assert.Empty(result.ViewName); @@ -685,7 +733,7 @@ public void ShowsConfirmViewWithModelWhenConfirmingEmailAddressIsNotRequired() controller.MockConfig .Setup(x => x.ConfirmEmailAddresses) .Returns(false); - + var result = controller.Thanks() as ViewResult; Assert.Equal("Confirm", result.ViewName); @@ -704,7 +752,7 @@ public class TestableUsersController : UsersController public Mock MockPackageService { get; protected set; } public Mock MockConfig { get; protected set; } public Mock MockUserService { get; protected set; } - + public TestableUsersController() { CuratedFeedService = (MockCuratedFeedService = new Mock()).Object; diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 14eac4aa05..62e639841c 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -733,6 +733,8 @@ public void ReplacesExistingCredentialIfOneWithSameTypeExistsForUser() // Assert Assert.Equal(2, users[0].Credentials.Count); Assert.Equal(new[] { frozenCred, newCred }, users[0].Credentials.ToArray()); + service.MockCredentialRepository + .Verify(x => x.DeleteOnCommit(existingCred)); service.MockUserRepository.VerifyCommitted(); } }