From d7f711bca1104650ea4d5a75fcf17569eef6a54e Mon Sep 17 00:00:00 2001 From: tugberkugurlu Date: Thu, 23 Oct 2014 15:02:24 +0300 Subject: [PATCH] added replace claim functionality. fixes #232 - added ReplaceClaimAsync method to IUserClaimStore interface - implemented ReplaceClaimAsync method from IUserClaimStore inside the Microsoft.AspNet.Identity.EntityFramework.UserStore - added ReplaceClaimAsync method to UserManager - added UserManager tests for ReplaceClaimAsync method. - added some UserStore tests for ReplaceClaimAsync implementation of IUserClaimStore --- .../UserStore.cs | 28 +++++++++++++- .../IUserClaimStore.cs | 14 ++++++- src/Microsoft.AspNet.Identity/UserManager.cs | 37 ++++++++++++++++-- .../InMemoryUserStore.cs | 35 ++++++++++++++++- .../UserStoreTest.cs | 2 + .../InMemoryUserStore.cs | 11 ++++++ .../UserManagerTest.cs | 38 ++++++++++++++++++- test/Shared/UserManagerTestBase.cs | 19 ++++++++++ 8 files changed, 175 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs b/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs index 75f97bb4b6..f224eb7df8 100644 --- a/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs +++ b/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs @@ -441,6 +441,32 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(0); } + public Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException("user"); + } + if (claim == null) + { + throw new ArgumentNullException("claim"); + } + if (newClaim == null) + { + throw new ArgumentNullException("newClaim"); + } + + var matchedClaims = UserClaims.Where(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToList(); + foreach(var matchedClaim in matchedClaims) + { + matchedClaim.ClaimValue = newClaim.Value; + matchedClaim.ClaimType = newClaim.Type; + } + + return Task.FromResult(0); + } + public Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -530,7 +556,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); // todo: ensure logins loaded - var userLogin = + var userLogin = UserLogins.FirstOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); if (userLogin != null) { diff --git a/src/Microsoft.AspNet.Identity/IUserClaimStore.cs b/src/Microsoft.AspNet.Identity/IUserClaimStore.cs index ae483223fc..eb7d0d559f 100644 --- a/src/Microsoft.AspNet.Identity/IUserClaimStore.cs +++ b/src/Microsoft.AspNet.Identity/IUserClaimStore.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - Task> GetClaimsAsync(TUser user, + Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); /// @@ -32,6 +32,16 @@ namespace Microsoft.AspNet.Identity /// Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)); + /// + /// Updates the give claim information with the given new claim information + /// + /// + /// + /// + /// + /// + Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)); + /// /// Remove a user claim /// @@ -39,7 +49,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - Task RemoveClaimsAsync(TUser user, IEnumerable claims, + Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UserManager.cs b/src/Microsoft.AspNet.Identity/UserManager.cs index 5a95a98c85..5287ed215d 100644 --- a/src/Microsoft.AspNet.Identity/UserManager.cs +++ b/src/Microsoft.AspNet.Identity/UserManager.cs @@ -708,7 +708,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task UpdateSecurityStampAsync(TUser user, + public virtual async Task UpdateSecurityStampAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -727,7 +727,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task GeneratePasswordResetTokenAsync(TUser user, + public virtual async Task GeneratePasswordResetTokenAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -946,6 +946,35 @@ namespace Microsoft.AspNet.Identity return await UpdateAsync(user, cancellationToken); } + /// + /// Updates the give claim information with the given new claim information + /// + /// + /// + /// + /// + /// + public virtual async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, + CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + var claimStore = GetClaimStore(); + if (claim == null) + { + throw new ArgumentNullException("claim"); + } + if (newClaim == null) + { + throw new ArgumentNullException("newClaim"); + } + if (user == null) + { + throw new ArgumentNullException("user"); + } + await claimStore.ReplaceClaimAsync(user, claim, newClaim, cancellationToken); + return await UpdateAsync(user, cancellationToken); + } + /// /// Remove a user claim /// @@ -1598,7 +1627,7 @@ namespace Microsoft.AspNet.Identity } if (!_tokenProviders.ContainsKey(tokenProvider)) { - throw new NotSupportedException(String.Format(CultureInfo.CurrentCulture, + throw new NotSupportedException(String.Format(CultureInfo.CurrentCulture, Resources.NoTokenProvider, tokenProvider)); } await _tokenProviders[tokenProvider].NotifyAsync(token, this, user, cancellationToken); @@ -1827,7 +1856,7 @@ namespace Microsoft.AspNet.Identity /// /// Increments the access failed count for the user and if the failed access account is greater than or equal - /// to the MaxFailedAccessAttempsBeforeLockout, the user will be locked out for the next + /// to the MaxFailedAccessAttempsBeforeLockout, the user will be locked out for the next /// DefaultAccountLockoutTimeSpan and the AccessFailedCount will be reset to 0. /// /// diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryUserStore.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryUserStore.cs index c66a2eb85b..95fec53a62 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryUserStore.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryUserStore.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test public InMemoryUserStore(InMemoryContext context) : base(context) { } } - public class InMemoryUserStore : InMemoryUserStore + public class InMemoryUserStore : InMemoryUserStore where TUser:IdentityUser where TContext : DbContext { @@ -370,6 +370,39 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test return Task.FromResult(0); } + /// + /// Updates the give claim with the new one. + /// + /// + /// + /// + /// + /// + public Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException("user"); + } + if (claim == null) + { + throw new ArgumentNullException("claim"); + } + if (newClaim == null) + { + throw new ArgumentNullException("newClaim"); + } + + var matchedClaim = user.Claims.FirstOrDefault(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type); + if(matchedClaim != null) + { + matchedClaim.ClaimValue = newClaim.Value; + matchedClaim.ClaimType = newClaim.Type; + } + return Task.FromResult(0); + } + /// /// Remove claims from a user /// diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.Test/UserStoreTest.cs b/test/Microsoft.AspNet.Identity.EntityFramework.Test/UserStoreTest.cs index 04a5e07c4e..c52e9eccf7 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.Test/UserStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.Test/UserStoreTest.cs @@ -172,6 +172,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework.Test await Assert.ThrowsAsync( async () => await store.RemoveFromRoleAsync(null, null)); await Assert.ThrowsAsync(async () => await store.RemoveClaimsAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.ReplaceClaimAsync(null, null, null)); await Assert.ThrowsAsync(async () => await store.FindByLoginAsync(null, null)); await Assert.ThrowsAsync(async () => await store.FindByIdAsync(null)); await Assert.ThrowsAsync(async () => await store.FindByNameAsync(null)); @@ -199,6 +200,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework.Test await Assert.ThrowsAsync("user", async () => await store.UpdateAsync(null)); await Assert.ThrowsAsync("user", async () => await store.DeleteAsync(null)); await Assert.ThrowsAsync("user", async () => await store.AddClaimsAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.ReplaceClaimAsync(null, null, null)); await Assert.ThrowsAsync("user", async () => await store.RemoveClaimsAsync(null, null)); await Assert.ThrowsAsync("user", async () => await store.GetClaimsAsync(null)); await Assert.ThrowsAsync("user", async () => await store.GetLoginsAsync(null)); diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs index 2d62ea0053..53ff6c8bd7 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs @@ -47,6 +47,17 @@ namespace Microsoft.AspNet.Identity.InMemory return Task.FromResult(0); } + public Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + var matchedClaims = user.Claims.Where(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToList(); + foreach(var matchedClaim in matchedClaims) + { + matchedClaim.ClaimValue = newClaim.Value; + matchedClaim.ClaimType = newClaim.Type; + } + return Task.FromResult(0); + } + public Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) { foreach (var claim in claims) diff --git a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs index 35ae3c1cac..35eb06c3a5 100644 --- a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs @@ -315,6 +315,27 @@ namespace Microsoft.AspNet.Identity.Test store.VerifyAll(); } + [Fact] + public async Task UpdateClaimCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var claim = new Claim("1", "1"); + var newClaim = new Claim("1", "2"); + store.Setup(s => s.ReplaceClaimAsync(user, It.IsAny(), It.IsAny(), CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.ReplaceClaimAsync(user, claim, newClaim); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + [Fact] public async Task RemoveClaimsCallsStore() { @@ -455,6 +476,7 @@ namespace Microsoft.AspNet.Identity.Test var manager = MockHelpers.TestUserManager(new NoopUserStore()); Assert.False(manager.SupportsUserClaim); await Assert.ThrowsAsync(async () => await manager.AddClaimAsync(null, null)); + await Assert.ThrowsAsync(async () => await manager.ReplaceClaimAsync(null, null, null)); await Assert.ThrowsAsync(async () => await manager.RemoveClaimAsync(null, null)); await Assert.ThrowsAsync(async () => await manager.GetClaimsAsync(null)); } @@ -541,11 +563,12 @@ namespace Microsoft.AspNet.Identity.Test await Assert.ThrowsAsync("user", async () => await manager.UpdateAsync(null)); await Assert.ThrowsAsync("user", async () => await manager.DeleteAsync(null)); await Assert.ThrowsAsync("claim", async () => await manager.AddClaimAsync(null, null)); + await Assert.ThrowsAsync("claim", async () => await manager.ReplaceClaimAsync(null, null, null)); await Assert.ThrowsAsync("claims", async () => await manager.AddClaimsAsync(null, null)); await Assert.ThrowsAsync("userName", async () => await manager.FindByNameAsync(null)); await Assert.ThrowsAsync("userName", async () => await manager.FindByUserNamePasswordAsync(null, null)); await Assert.ThrowsAsync("login", async () => await manager.AddLoginAsync(null, null)); - await Assert.ThrowsAsync("loginProvider", + await Assert.ThrowsAsync("loginProvider", async () => await manager.RemoveLoginAsync(null, null, null)); await Assert.ThrowsAsync("providerKey", async () => await manager.RemoveLoginAsync(null, "", null)); @@ -594,6 +617,8 @@ namespace Microsoft.AspNet.Identity.Test async () => await manager.RemoveFromRoleAsync(null, null)); await Assert.ThrowsAsync("user", async () => await manager.RemoveFromRolesAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.ReplaceClaimAsync(null, new Claim("a", "b"), new Claim("a", "c"))); await Assert.ThrowsAsync("user", async () => await manager.UpdateSecurityStampAsync(null)); await Assert.ThrowsAsync("user", @@ -690,6 +715,7 @@ namespace Microsoft.AspNet.Identity.Test await Assert.ThrowsAsync(() => manager.CreateAsync(null, null)); await Assert.ThrowsAsync(() => manager.UpdateAsync(null)); await Assert.ThrowsAsync(() => manager.DeleteAsync(null)); + await Assert.ThrowsAsync(() => manager.ReplaceClaimAsync(null, null, null)); await Assert.ThrowsAsync(() => manager.UpdateSecurityStampAsync(null)); await Assert.ThrowsAsync(() => manager.GetSecurityStampAsync(null)); await Assert.ThrowsAsync(() => manager.GeneratePasswordResetTokenAsync(null)); @@ -730,6 +756,11 @@ namespace Microsoft.AspNet.Identity.Test return Task.FromResult(0); } + public Task ReplaceClaimAsync(TestUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + public Task RemoveClaimsAsync(TestUser user, IEnumerable claim, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult(0); @@ -991,6 +1022,11 @@ namespace Microsoft.AspNet.Identity.Test throw new NotImplementedException(); } + public Task ReplaceClaimAsync(TestUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + public Task RemoveClaimsAsync(TestUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); diff --git a/test/Shared/UserManagerTestBase.cs b/test/Shared/UserManagerTestBase.cs index a9bf752c00..2754f1eabc 100644 --- a/test/Shared/UserManagerTestBase.cs +++ b/test/Shared/UserManagerTestBase.cs @@ -353,6 +353,25 @@ namespace Microsoft.AspNet.Identity.Test Assert.Equal(0, userClaims.Count); } + [Fact] + public async Task CanReplaceUserClaim() + { + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, new Claim("c", "a"))); + var userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims.Count); + Claim claim = new Claim("c", "b"); + Claim oldClaim = userClaims.FirstOrDefault(); + IdentityResultAssert.IsSuccess(await manager.ReplaceClaimAsync(user, oldClaim, claim)); + var newUserClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, newUserClaims.Count); + Claim newClaim = newUserClaims.FirstOrDefault(); + Assert.Equal(claim.Type, newClaim.Type); + Assert.Equal(claim.Value, newClaim.Value); + } + [Fact] public async Task ChangePasswordFallsIfPasswordWrong() {