diff --git a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/IdentityDbContext.cs b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/IdentityDbContext.cs index 2fafc38694..0655306595 100644 --- a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/IdentityDbContext.cs +++ b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/IdentityDbContext.cs @@ -2,7 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore { @@ -131,13 +135,35 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore /// public DbSet RoleClaims { get; set; } + private string _version; /// - /// Configures the schema needed for the identity framework. + /// Gets or sets the version to use during . + /// If not set, defaults to the value in the + /// + public string Version { + get => _version ?? + this.GetService() + .Extensions.OfType() + .FirstOrDefault()?.ApplicationServiceProvider + ?.GetService>() + ?.Value?.Version; + set => _version = value; + } + + /// + /// Creates the latest identity model. + /// + /// + protected void OnModelCreatingLatest(ModelBuilder builder) + => OnModelCreatingV2(builder); + + /// + /// Configures the schema needed for the identity framework version . /// /// /// The builder being used to construct the model for this context. /// - protected override void OnModelCreating(ModelBuilder builder) + protected void OnModelCreatingV2(ModelBuilder builder) { builder.Entity(b => { @@ -152,7 +178,6 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore b.Property(u => u.Email).HasMaxLength(256); b.Property(u => u.NormalizedEmail).HasMaxLength(256); - // Replace with b.HasMany(). b.HasMany().WithOne().HasForeignKey(uc => uc.UserId).IsRequired(); b.HasMany().WithOne().HasForeignKey(ul => ul.UserId).IsRequired(); b.HasMany().WithOne().HasForeignKey(ur => ur.UserId).IsRequired(); @@ -173,19 +198,19 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore b.HasMany().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired(); }); - builder.Entity(b => + builder.Entity(b => { b.HasKey(uc => uc.Id); b.ToTable("AspNetUserClaims"); }); - builder.Entity(b => + builder.Entity(b => { b.HasKey(rc => rc.Id); b.ToTable("AspNetRoleClaims"); }); - builder.Entity(b => + builder.Entity(b => { b.HasKey(r => new { r.UserId, r.RoleId }); b.ToTable("AspNetUserRoles"); @@ -197,11 +222,53 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore b.ToTable("AspNetUserLogins"); }); - builder.Entity(b => + builder.Entity(b => { b.HasKey(l => new { l.UserId, l.LoginProvider, l.Name }); b.ToTable("AspNetUserTokens"); }); } + + /// + /// Configures the schema needed for the identity framework version . + /// + /// + /// The builder being used to construct the model for this context. + /// + protected void OnModelCreatingV1(ModelBuilder builder) + { + + OnModelCreatingV2(builder); + + // Ignore the additional 2.0 properties that were added + builder.Entity(b => + { + b.Ignore(u => u.LastPasswordChangeDate); + b.Ignore(u => u.LastSignInDate); + b.Ignore(u => u.CreateDate); + }); + } + + /// + /// Configures the schema needed for the identity framework. + /// + /// + /// The builder being used to construct the model for this context. + /// + protected override void OnModelCreating(ModelBuilder builder) + { + if (Version == IdentityStoreOptions.Version_Latest) + { + OnModelCreatingLatest(builder); + } + else if (Version == IdentityStoreOptions.Version2_0) + { + OnModelCreatingV2(builder); + } + else // Default is always the v1 schema + { + OnModelCreatingV1(builder); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/IdentityEntityFrameworkBuilderExtensions.cs b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/IdentityEntityFrameworkBuilderExtensions.cs index 6a25296f53..c88e77fc5a 100644 --- a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/IdentityEntityFrameworkBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/IdentityEntityFrameworkBuilderExtensions.cs @@ -15,6 +15,52 @@ namespace Microsoft.Extensions.DependencyInjection /// public static class IdentityEntityFrameworkBuilderExtensions { + /// + /// Adds an Entity Framework implementation of identity information stores with the schema/features in + /// . + /// + /// The Entity Framework database context to use. + /// The instance this method extends. + /// The instance this method extends. + public static IdentityBuilder AddEntityFrameworkStoresV1(this IdentityBuilder builder) + where TContext : DbContext + { + builder.Services.Configure(o => o.Version = IdentityStoreOptions.Version1_0); + AddStores(builder.Services, typeof(UserStoreV1<,,,,,,,,>), typeof(RoleStoreV1<,,,,>), builder.UserType, builder.RoleType, typeof(TContext)); + return builder; + } + + /// + /// Adds an Entity Framework implementation of identity information stores with the schema/features in + /// . + /// + /// The Entity Framework database context to use. + /// The instance this method extends. + /// The instance this method extends. + public static IdentityBuilder AddEntityFrameworkStoresV2(this IdentityBuilder builder) + where TContext : DbContext + { + builder.Services.Configure(o => o.Version = IdentityStoreOptions.Version2_0); + // Note: RoleStore was not changed for V2. + AddStores(builder.Services, typeof(UserStoreV2<,,,,,,,,>), typeof(RoleStoreV1<,,,,>), builder.UserType, builder.RoleType, typeof(TContext)); + return builder; + } + + /// + /// Adds an Entity Framework implementation of identity information stores with the latest schema/features in + /// . + /// + /// The Entity Framework database context to use. + /// The instance this method extends. + /// The instance this method extends. + public static IdentityBuilder AddEntityFrameworkStoresLatest(this IdentityBuilder builder) + where TContext : DbContext + { + builder.Services.Configure(o => o.Version = IdentityStoreOptions.Version_Latest); + AddStores(builder.Services, typeof(UserStore<,,,,,,,,>), typeof(RoleStore<,,,,>), builder.UserType, builder.RoleType, typeof(TContext)); + return builder; + } + /// /// Adds an Entity Framework implementation of identity information stores. /// @@ -23,12 +69,9 @@ namespace Microsoft.Extensions.DependencyInjection /// The instance this method extends. public static IdentityBuilder AddEntityFrameworkStores(this IdentityBuilder builder) where TContext : DbContext - { - AddStores(builder.Services, builder.UserType, builder.RoleType, typeof(TContext)); - return builder; - } + => builder.AddEntityFrameworkStoresV1(); - private static void AddStores(IServiceCollection services, Type userType, Type roleType, Type contextType) + private static void AddStores(IServiceCollection services, Type userStore, Type roleStore, Type userType, Type roleType, Type contextType) { var identityUserType = FindGenericBaseType(userType, typeof(IdentityUser<,,,,>)); if (identityUserType == null) @@ -43,7 +86,7 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddScoped( typeof(IUserStore<>).MakeGenericType(userType), - typeof(UserStore<,,,,,,,,>).MakeGenericType(userType, roleType, contextType, + userStore.MakeGenericType(userType, roleType, contextType, identityUserType.GenericTypeArguments[0], identityUserType.GenericTypeArguments[1], identityUserType.GenericTypeArguments[2], @@ -52,7 +95,7 @@ namespace Microsoft.Extensions.DependencyInjection identityRoleType.GenericTypeArguments[2])); services.TryAddScoped( typeof(IRoleStore<>).MakeGenericType(roleType), - typeof(RoleStore<,,,,>).MakeGenericType(roleType, contextType, + roleStore.MakeGenericType(roleType, contextType, identityRoleType.GenericTypeArguments[0], identityRoleType.GenericTypeArguments[1], identityRoleType.GenericTypeArguments[2])); diff --git a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/RoleStore.cs b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/RoleStore.cs index 2baedd84c8..f05e9827dd 100644 --- a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/RoleStore.cs +++ b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/RoleStore.cs @@ -2,70 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore { - /// - /// Creates a new instance of a persistence store for roles. - /// - /// The type of the class representing a role - public class RoleStore : RoleStore - where TRole : IdentityRole - { - /// - /// Constructs a new instance of . - /// - /// The . - /// The . - public RoleStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } - } - - /// - /// Creates a new instance of a persistence store for roles. - /// - /// The type of the class representing a role. - /// The type of the data context class used to access the store. - public class RoleStore : RoleStore - where TRole : IdentityRole - where TContext : DbContext - { - /// - /// Constructs a new instance of . - /// - /// The . - /// The . - public RoleStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } - } - - /// - /// Creates a new instance of a persistence store for roles. - /// - /// The type of the class representing a role. - /// The type of the data context class used to access the store. - /// The type of the primary key for a role. - public class RoleStore : RoleStore, IdentityRoleClaim>, - IQueryableRoleStore, - IRoleClaimStore - where TRole : IdentityRole - where TKey : IEquatable - where TContext : DbContext - { - /// - /// Constructs a new instance of . - /// - /// The . - /// The . - public RoleStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } - } - /// /// Creates a new instance of a persistence store for roles. /// @@ -75,8 +15,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore /// The type of the class representing a user role. /// The type of the class representing a role claim. public class RoleStore : - IQueryableRoleStore, - IRoleClaimStore + RoleStoreV1 where TRole : IdentityRole where TKey : IEquatable where TContext : DbContext @@ -88,368 +27,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore /// /// The . /// The . - public RoleStore(TContext context, IdentityErrorDescriber describer = null) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - Context = context; - ErrorDescriber = describer ?? new IdentityErrorDescriber(); - } - - private bool _disposed; - - - /// - /// Gets the database context for this store. - /// - public TContext Context { get; private set; } - - /// - /// Gets or sets the for any error that occurred with the current operation. - /// - public IdentityErrorDescriber ErrorDescriber { get; set; } - - /// - /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. - /// - /// - /// True if changes should be automatically persisted, otherwise false. - /// - public bool AutoSaveChanges { get; set; } = true; - - /// Saves the current store. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - private async Task SaveChanges(CancellationToken cancellationToken) - { - if (AutoSaveChanges) - { - await Context.SaveChangesAsync(cancellationToken); - } - } - - /// - /// Creates a new role in a store as an asynchronous operation. - /// - /// The role to create in the store. - /// The used to propagate notifications that the operation should be canceled. - /// A that represents the of the asynchronous query. - public async virtual Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - Context.Add(role); - await SaveChanges(cancellationToken); - return IdentityResult.Success; - } - - /// - /// Updates a role in a store as an asynchronous operation. - /// - /// The role to update in the store. - /// The used to propagate notifications that the operation should be canceled. - /// A that represents the of the asynchronous query. - public async virtual Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - Context.Attach(role); - role.ConcurrencyStamp = Guid.NewGuid().ToString(); - Context.Update(role); - try - { - await SaveChanges(cancellationToken); - } - catch (DbUpdateConcurrencyException) - { - return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); - } - return IdentityResult.Success; - } - - /// - /// Deletes a role from the store as an asynchronous operation. - /// - /// The role to delete from the store. - /// The used to propagate notifications that the operation should be canceled. - /// A that represents the of the asynchronous query. - public async virtual Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - Context.Remove(role); - try - { - await SaveChanges(cancellationToken); - } - catch (DbUpdateConcurrencyException) - { - return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); - } - return IdentityResult.Success; - } - - /// - /// Gets the ID for a role from the store as an asynchronous operation. - /// - /// The role whose ID should be returned. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the ID of the role. - public virtual Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - return Task.FromResult(ConvertIdToString(role.Id)); - } - - /// - /// Gets the name of a role from the store as an asynchronous operation. - /// - /// The role whose name should be returned. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the name of the role. - public virtual Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - return Task.FromResult(role.Name); - } - - /// - /// Sets the name of a role in the store as an asynchronous operation. - /// - /// The role whose name should be set. - /// The name of the role. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - role.Name = roleName; - return TaskCache.CompletedTask; - } - - /// - /// Converts the provided to a strongly typed key object. - /// - /// The id to convert. - /// An instance of representing the provided . - public virtual TKey ConvertIdFromString(string id) - { - if (id == null) - { - return default(TKey); - } - return (TKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id); - } - - /// - /// Converts the provided to its string representation. - /// - /// The id to convert. - /// An representation of the provided . - public virtual string ConvertIdToString(TKey id) - { - if (id.Equals(default(TKey))) - { - return null; - } - return id.ToString(); - } - - /// - /// Finds the role who has the specified ID as an asynchronous operation. - /// - /// The role ID to look for. - /// The used to propagate notifications that the operation should be canceled. - /// A that result of the look up. - public virtual Task FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - var roleId = ConvertIdFromString(id); - return Roles.FirstOrDefaultAsync(u => u.Id.Equals(roleId), cancellationToken); - } - - /// - /// Finds the role who has the specified normalized name as an asynchronous operation. - /// - /// The normalized role name to look for. - /// The used to propagate notifications that the operation should be canceled. - /// A that result of the look up. - public virtual Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - return Roles.FirstOrDefaultAsync(r => r.NormalizedName == normalizedName, cancellationToken); - } - - /// - /// Get a role's normalized name as an asynchronous operation. - /// - /// The role whose normalized name should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the name of the role. - public virtual Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - return Task.FromResult(role.NormalizedName); - } - - /// - /// Set a role's normalized name as an asynchronous operation. - /// - /// The role whose normalized name should be set. - /// The normalized name to set - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - role.NormalizedName = normalizedName; - return TaskCache.CompletedTask; - } - - /// - /// Throws if this class has been disposed. - /// - protected void ThrowIfDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - /// - /// Dispose the stores - /// - public void Dispose() - { - _disposed = true; - } - - /// - /// Get the claims associated with the specified as an asynchronous operation. - /// - /// The role whose claims should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the claims granted to a role. - public async virtual Task> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - - return await RoleClaims.Where(rc => rc.RoleId.Equals(role.Id)).Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToListAsync(cancellationToken); - } - - /// - /// Adds the given to the specified . - /// - /// The role to add the claim to. - /// The claim to add to the role. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - if (claim == null) - { - throw new ArgumentNullException(nameof(claim)); - } - - RoleClaims.Add(CreateRoleClaim(role, claim)); - return Task.FromResult(false); - } - - /// - /// Removes the given from the specified . - /// - /// The role to remove the claim from. - /// The claim to remove from the role. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public async virtual Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - if (claim == null) - { - throw new ArgumentNullException(nameof(claim)); - } - var claims = await RoleClaims.Where(rc => rc.RoleId.Equals(role.Id) && rc.ClaimValue == claim.Value && rc.ClaimType == claim.Type).ToListAsync(cancellationToken); - foreach (var c in claims) - { - RoleClaims.Remove(c); - } - } - - /// - /// A navigation property for the roles the store contains. - /// - public virtual IQueryable Roles - { - get { return Context.Set(); } - } - - private DbSet RoleClaims { get { return Context.Set(); } } - - /// - /// Creates a entity representing a role claim. - /// - /// The associated role. - /// The associated claim. - /// The role claim entity. - protected virtual TRoleClaim CreateRoleClaim(TRole role, Claim claim) - { - return new TRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value }; - } + public RoleStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) + { } } } diff --git a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/RoleStoreV1.cs b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/RoleStoreV1.cs new file mode 100644 index 0000000000..d3823cc8ea --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/RoleStoreV1.cs @@ -0,0 +1,254 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore +{ + /// + /// Creates a new instance of a persistence store for roles. + /// + /// The type of the class representing a role. + /// The type of the data context class used to access the store. + /// The type of the primary key for a role. + /// The type of the class representing a user role. + /// The type of the class representing a role claim. + public class RoleStoreV1 : + RoleStoreBaseV1 + where TRole : IdentityRole + where TKey : IEquatable + where TContext : DbContext + where TUserRole : IdentityUserRole, new() + where TRoleClaim : IdentityRoleClaim, new() + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public RoleStoreV1(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber()) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + Context = context; + } + + /// + /// Gets the database context for this store. + /// + public TContext Context { get; private set; } + + /// + /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + /// + /// + /// True if changes should be automatically persisted, otherwise false. + /// + public bool AutoSaveChanges { get; set; } = true; + + /// Saves the current store. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + private async Task SaveChanges(CancellationToken cancellationToken) + { + if (AutoSaveChanges) + { + await Context.SaveChangesAsync(cancellationToken); + } + } + + /// + /// Creates a new role in a store as an asynchronous operation. + /// + /// The role to create in the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public override async Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + Context.Add(role); + await SaveChanges(cancellationToken); + return IdentityResult.Success; + } + + /// + /// Updates a role in a store as an asynchronous operation. + /// + /// The role to update in the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public async override Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + Context.Attach(role); + role.ConcurrencyStamp = Guid.NewGuid().ToString(); + Context.Update(role); + try + { + await SaveChanges(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; + } + + /// + /// Deletes a role from the store as an asynchronous operation. + /// + /// The role to delete from the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public async override Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + Context.Remove(role); + try + { + await SaveChanges(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; + } + + /// + /// Finds the role who has the specified ID as an asynchronous operation. + /// + /// The role ID to look for. + /// The used to propagate notifications that the operation should be canceled. + /// A that result of the look up. + public override Task FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var roleId = ConvertIdFromString(id); + return Roles.FirstOrDefaultAsync(u => u.Id.Equals(roleId), cancellationToken); + } + + /// + /// Finds the role who has the specified normalized name as an asynchronous operation. + /// + /// The normalized role name to look for. + /// The used to propagate notifications that the operation should be canceled. + /// A that result of the look up. + public override Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return Roles.FirstOrDefaultAsync(r => r.NormalizedName == normalizedName, cancellationToken); + } + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The role whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a role. + public async override Task> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + return await RoleClaims.Where(rc => rc.RoleId.Equals(role.Id)).Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToListAsync(cancellationToken); + } + + /// + /// Adds the given to the specified . + /// + /// The role to add the claim to. + /// The claim to add to the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + + RoleClaims.Add(CreateRoleClaim(role, claim)); + return Task.FromResult(false); + } + + /// + /// Removes the given from the specified . + /// + /// The role to remove the claim from. + /// The claim to remove from the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override async Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + var claims = await RoleClaims.Where(rc => rc.RoleId.Equals(role.Id) && rc.ClaimValue == claim.Value && rc.ClaimType == claim.Type).ToListAsync(cancellationToken); + foreach (var c in claims) + { + RoleClaims.Remove(c); + } + } + + /// + /// A navigation property for the roles the store contains. + /// + public override IQueryable Roles + { + get { return Context.Set(); } + } + + private DbSet RoleClaims { get { return Context.Set(); } } + + /// + /// Creates a entity representing a role claim. + /// + /// The associated role. + /// The associated claim. + /// The role claim entity. + public override TRoleClaim CreateRoleClaim(TRole role, Claim claim) + { + return new TRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value }; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStore.cs b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStore.cs index 35135ec080..a0c982bee8 100644 --- a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStore.cs +++ b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStore.cs @@ -2,86 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore { - /// - /// Represents a new instance of a persistence store for users, using the default implementation - /// of with a string as a primary key. - /// - public class UserStore : UserStore> - { - /// - /// Constructs a new instance of . - /// - /// The . - /// The . - public UserStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } - } - - /// - /// Creates a new instance of a persistence store for the specified user type. - /// - /// The type representing a user. - public class UserStore : UserStore - where TUser : IdentityUser, new() - { - /// - /// Constructs a new instance of . - /// - /// The . - /// The . - public UserStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } - } - - /// - /// Represents a new instance of a persistence store for the specified user and role types. - /// - /// The type representing a user. - /// The type representing a role. - /// The type of the data context class used to access the store. - public class UserStore : UserStore - where TUser : IdentityUser - where TRole : IdentityRole - where TContext : DbContext - { - /// - /// Constructs a new instance of . - /// - /// The . - /// The . - public UserStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } - } - - /// - /// Represents a new instance of a persistence store for the specified user and role types. - /// - /// The type representing a user. - /// The type representing a role. - /// The type of the data context class used to access the store. - /// The type of the primary key for a role. - public class UserStore : UserStore, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> - where TUser : IdentityUser - where TRole : IdentityRole - where TContext : DbContext - where TKey : IEquatable - { - /// - /// Constructs a new instance of . - /// - /// The . - /// The . - public UserStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } - } - /// /// Represents a new instance of a persistence store for the specified user and role types. /// @@ -95,20 +19,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore /// The type representing a user token. /// The type representing a role claim. public class UserStore : - UserStoreBase, - IUserLoginStore, - IUserRoleStore, - IUserClaimStore, - IUserPasswordStore, - IUserSecurityStampStore, - IUserEmailStore, - IUserLockoutStore, - IUserPhoneNumberStore, - IQueryableUserStore, - IUserTwoFactorStore, - IUserAuthenticationTokenStore, - IUserAuthenticatorKeyStore, - IUserTwoFactorRecoveryCodeStore + UserStoreV2 where TUser : IdentityUser where TRole : IdentityRole where TContext : DbContext @@ -124,622 +35,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore /// /// The context used to access the store. /// The used to describe store errors. - public UserStore(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber()) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - Context = context; - } - - /// - /// Gets the database context for this store. - /// - public TContext Context { get; private set; } - - private DbSet UsersSet { get { return Context.Set(); } } - private DbSet Roles { get { return Context.Set(); } } - private DbSet UserClaims { get { return Context.Set(); } } - private DbSet UserRoles { get { return Context.Set(); } } - private DbSet UserLogins { get { return Context.Set(); } } - private DbSet UserTokens { get { return Context.Set(); } } - - /// - /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. - /// - /// - /// True if changes should be automatically persisted, otherwise false. - /// - public bool AutoSaveChanges { get; set; } = true; - - /// Saves the current store. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - protected Task SaveChanges(CancellationToken cancellationToken) - { - return AutoSaveChanges ? Context.SaveChangesAsync(cancellationToken) : TaskCache.CompletedTask; - } - - /// - /// Creates the specified in the user store. - /// - /// The user to create. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the of the creation operation. - public async override Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - Context.Add(user); - await SaveChanges(cancellationToken); - return IdentityResult.Success; - } - - /// - /// Updates the specified in the user store. - /// - /// The user to update. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the of the update operation. - public async override Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - Context.Attach(user); - user.ConcurrencyStamp = Guid.NewGuid().ToString(); - Context.Update(user); - try - { - await SaveChanges(cancellationToken); - } - catch (DbUpdateConcurrencyException) - { - return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); - } - return IdentityResult.Success; - } - - /// - /// Deletes the specified from the user store. - /// - /// The user to delete. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the of the update operation. - public async override Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - Context.Remove(user); - try - { - await SaveChanges(cancellationToken); - } - catch (DbUpdateConcurrencyException) - { - return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); - } - return IdentityResult.Success; - } - - /// - /// Finds and returns a user, if any, who has the specified . - /// - /// The user ID to search for. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The that represents the asynchronous operation, containing the user matching the specified if it exists. - /// - public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - var id = ConvertIdFromString(userId); - return UsersSet.FindAsync(new object[] { id }, cancellationToken); - } - - /// - /// Finds and returns a user, if any, who has the specified normalized user name. - /// - /// The normalized user name to search for. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The that represents the asynchronous operation, containing the user matching the specified if it exists. - /// - public override Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - return Users.FirstOrDefaultAsync(u => u.NormalizedUserName == normalizedUserName, cancellationToken); - } - - /// - /// A navigation property for the users the store contains. - /// - public override IQueryable Users - { - get { return UsersSet; } - } - - /// - /// Return a role with the normalized name if it exists. - /// - /// The normalized role name. - /// The used to propagate notifications that the operation should be canceled. - /// The role if it exists. - protected override Task FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) - { - return Roles.SingleOrDefaultAsync(r => r.NormalizedName == normalizedRoleName, cancellationToken); - } - - /// - /// Return a user role for the userId and roleId if it exists. - /// - /// The user's id. - /// The role's id. - /// The used to propagate notifications that the operation should be canceled. - /// The user role if it exists. - protected override Task FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken) - { - return UserRoles.FindAsync(new object[] { userId, roleId }, cancellationToken); - } - - /// - /// Return a user with the matching userId if it exists. - /// - /// The user's id. - /// The used to propagate notifications that the operation should be canceled. - /// The user if it exists. - protected override Task FindUserAsync(TKey userId, CancellationToken cancellationToken) - { - return Users.SingleOrDefaultAsync(u => u.Id.Equals(userId), cancellationToken); - } - - /// - /// Return a user login with the matching userId, provider, providerKey if it exists. - /// - /// The user's id. - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - protected override Task FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken) - { - return UserLogins.SingleOrDefaultAsync(userLogin => userLogin.UserId.Equals(userId) && userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey, cancellationToken); - } - - /// - /// Return a user login with provider, providerKey if it exists. - /// - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - protected override Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) - { - return UserLogins.SingleOrDefaultAsync(userLogin => userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey, cancellationToken); - } - - - /// - /// Adds the given to the specified . - /// - /// The user to add the role to. - /// The role to add. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public async override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - if (string.IsNullOrWhiteSpace(normalizedRoleName)) - { - throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName)); - } - var roleEntity = await FindRoleAsync(normalizedRoleName, cancellationToken); - if (roleEntity == null) - { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.RoleNotFound, normalizedRoleName)); - } - UserRoles.Add(CreateUserRole(user, roleEntity)); - } - - /// - /// Removes the given from the specified . - /// - /// The user to remove the role from. - /// The role to remove. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public async override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - if (string.IsNullOrWhiteSpace(normalizedRoleName)) - { - throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName)); - } - var roleEntity = await FindRoleAsync(normalizedRoleName, cancellationToken); - if (roleEntity != null) - { - var userRole = await FindUserRoleAsync(user.Id, roleEntity.Id, cancellationToken); - if (userRole != null) - { - UserRoles.Remove(userRole); - } - } - } - - /// - /// Retrieves the roles the specified is a member of. - /// - /// The user whose roles should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the roles the user is a member of. - public override async Task> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - var userId = user.Id; - var query = from userRole in UserRoles - join role in Roles on userRole.RoleId equals role.Id - where userRole.UserId.Equals(userId) - select role.Name; - return await query.ToListAsync(); - } - - /// - /// Returns a flag indicating if the specified user is a member of the give . - /// - /// The user whose role membership should be checked. - /// The role to check membership of - /// The used to propagate notifications that the operation should be canceled. - /// A containing a flag indicating if the specified user is a member of the given group. If the - /// user is a member of the group the returned value with be true, otherwise it will be false. - public override async Task IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - if (string.IsNullOrWhiteSpace(normalizedRoleName)) - { - throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName)); - } - var role = await FindRoleAsync(normalizedRoleName, cancellationToken); - if (role != null) - { - var userRole = await FindUserRoleAsync(user.Id, role.Id, cancellationToken); - return userRole != null; - } - return false; - } - - /// - /// Get the claims associated with the specified as an asynchronous operation. - /// - /// The user whose claims should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the claims granted to a user. - public async override Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return await UserClaims.Where(uc => uc.UserId.Equals(user.Id)).Select(c => c.ToClaim()).ToListAsync(cancellationToken); - } - - /// - /// Adds the given to the specified . - /// - /// The user to add the claim to. - /// The claim to add to the user. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public override Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - if (claims == null) - { - throw new ArgumentNullException(nameof(claims)); - } - foreach (var claim in claims) - { - UserClaims.Add(CreateUserClaim(user, claim)); - } - return Task.FromResult(false); - } - - /// - /// Replaces the on the specified , with the . - /// - /// The user to replace the claim on. - /// The claim replace. - /// The new claim replacing the . - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public async override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - if (claim == null) - { - throw new ArgumentNullException(nameof(claim)); - } - if (newClaim == null) - { - throw new ArgumentNullException(nameof(newClaim)); - } - - var matchedClaims = await UserClaims.Where(uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken); - foreach (var matchedClaim in matchedClaims) - { - matchedClaim.ClaimValue = newClaim.Value; - matchedClaim.ClaimType = newClaim.Type; - } - } - - /// - /// Removes the given from the specified . - /// - /// The user to remove the claims from. - /// The claim to remove. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public async override Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - if (claims == null) - { - throw new ArgumentNullException(nameof(claims)); - } - foreach (var claim in claims) - { - var matchedClaims = await UserClaims.Where(uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken); - foreach (var c in matchedClaims) - { - UserClaims.Remove(c); - } - } - } - - /// - /// Adds the given to the specified . - /// - /// The user to add the login to. - /// The login to add to the user. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public override Task AddLoginAsync(TUser user, UserLoginInfo login, - CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - if (login == null) - { - throw new ArgumentNullException(nameof(login)); - } - UserLogins.Add(CreateUserLogin(user, login)); - return Task.FromResult(false); - } - - /// - /// Removes the given from the specified . - /// - /// The user to remove the login from. - /// The login to remove from the user. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public override async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, - CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - var entry = await FindUserLoginAsync(user.Id, loginProvider, providerKey, cancellationToken); - if (entry != null) - { - UserLogins.Remove(entry); - } - } - - /// - /// Retrieves the associated logins for the specified . - /// - /// The user whose associated logins to retrieve. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The for the asynchronous operation, containing a list of for the specified , if any. - /// - public async override Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - var userId = user.Id; - return await UserLogins.Where(l => l.UserId.Equals(userId)) - .Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)).ToListAsync(cancellationToken); - } - - /// - /// Retrieves the user associated with the specified login provider and login provider key. - /// - /// The login provider who provided the . - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. - /// - public async override Task FindByLoginAsync(string loginProvider, string providerKey, - CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - var userLogin = await FindUserLoginAsync(loginProvider, providerKey, cancellationToken); - if (userLogin != null) - { - return await FindUserAsync(userLogin.UserId, cancellationToken); - } - return null; - } - - /// - /// Gets the user, if any, associated with the specified, normalized email address. - /// - /// The normalized email address to return the user for. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. - /// - public override Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - return Users.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, cancellationToken); - } - - /// - /// Retrieves all users with the specified claim. - /// - /// The claim whose users should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The contains a list of users, if any, that contain the specified claim. - /// - public async override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (claim == null) - { - throw new ArgumentNullException(nameof(claim)); - } - - var query = from userclaims in UserClaims - join user in Users on userclaims.UserId equals user.Id - where userclaims.ClaimValue == claim.Value - && userclaims.ClaimType == claim.Type - select user; - - return await query.ToListAsync(cancellationToken); - } - - /// - /// Retrieves all users in the specified role. - /// - /// The role whose users should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The contains a list of users, if any, that are in the specified role. - /// - public async override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (string.IsNullOrEmpty(normalizedRoleName)) - { - throw new ArgumentNullException(nameof(normalizedRoleName)); - } - - var role = await FindRoleAsync(normalizedRoleName, cancellationToken); - - if (role != null) - { - var query = from userrole in UserRoles - join user in Users on userrole.UserId equals user.Id - where userrole.RoleId.Equals(role.Id) - select user; - - return await query.ToListAsync(cancellationToken); - } - return new List(); - } - - /// - /// Find a user token if it exists. - /// - /// The token owner. - /// The login provider for the token. - /// The name of the token. - /// The used to propagate notifications that the operation should be canceled. - /// The user token if it exists. - protected override Task FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) - => UserTokens.FindAsync(new object[] { user.Id, loginProvider, name }, cancellationToken); - - /// - /// Add a new user token. - /// - /// The token to be added. - /// - protected override Task AddUserTokenAsync(TUserToken token) - { - UserTokens.Add(token); - return TaskCache.CompletedTask; - } - - - /// - /// Remove a new user token. - /// - /// The token to be removed. - /// - protected override Task RemoveUserTokenAsync(TUserToken token) - { - UserTokens.Remove(token); - return TaskCache.CompletedTask; - } + public UserStore(TContext context, IdentityErrorDescriber describer) : base(context, describer) + { } } } diff --git a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStoreCore.cs b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStoreCore.cs new file mode 100644 index 0000000000..b732c72445 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStoreCore.cs @@ -0,0 +1,650 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore +{ + internal class UserStoreCore : + UserStoreBase + where TUser : IdentityUser + where TRole : IdentityRole + where TContext : DbContext + where TKey : IEquatable + where TUserClaim : IdentityUserClaim, new() + where TUserRole : IdentityUserRole, new() + where TUserLogin : IdentityUserLogin, new() + where TUserToken : IdentityUserToken, new() + where TRoleClaim : IdentityRoleClaim, new() + { + /// + /// Creates a new instance of the store. + /// + /// The context used to access the store. + /// The used to describe store errors. + public UserStoreCore(TContext context, IdentityErrorDescriber describer) : base(describer) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + Context = context; + } + + /// + /// Gets the database context for this store. + /// + public TContext Context { get; private set; } + + public DbSet UsersSet { get { return Context.Set(); } } + public DbSet Roles { get { return Context.Set(); } } + public DbSet UserClaims { get { return Context.Set(); } } + public DbSet UserRoles { get { return Context.Set(); } } + public DbSet UserLogins { get { return Context.Set(); } } + public DbSet UserTokens { get { return Context.Set(); } } + + /// + /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + /// + /// + /// True if changes should be automatically persisted, otherwise false. + /// + public bool AutoSaveChanges { get; set; } = true; + + /// Saves the current store. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public Task SaveChanges(CancellationToken cancellationToken) + { + return AutoSaveChanges ? Context.SaveChangesAsync(cancellationToken) : TaskCache.CompletedTask; + } + + /// + /// Creates the specified in the user store. + /// + /// The user to create. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the creation operation. + public async override Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + Context.Add(user); + await SaveChanges(cancellationToken); + return IdentityResult.Success; + } + + /// + /// Updates the specified in the user store. + /// + /// The user to update. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public async override Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + Context.Attach(user); + user.ConcurrencyStamp = Guid.NewGuid().ToString(); + Context.Update(user); + try + { + await SaveChanges(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; + } + + /// + /// Deletes the specified from the user store. + /// + /// The user to delete. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public async override Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + Context.Remove(user); + try + { + await SaveChanges(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; + } + + /// + /// Finds and returns a user, if any, who has the specified . + /// + /// The user ID to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var id = ConvertIdFromString(userId); + return UsersSet.FindAsync(new object[] { id }, cancellationToken); + } + + /// + /// Finds and returns a user, if any, who has the specified normalized user name. + /// + /// The normalized user name to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public override Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return Users.FirstOrDefaultAsync(u => u.NormalizedUserName == normalizedUserName, cancellationToken); + } + + /// + /// A navigation property for the users the store contains. + /// + public override IQueryable Users + { + get { return UsersSet; } + } + + /// + /// Return a role with the normalized name if it exists. + /// + /// The normalized role name. + /// The used to propagate notifications that the operation should be canceled. + /// The role if it exists. + public override Task FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) + { + return Roles.SingleOrDefaultAsync(r => r.NormalizedName == normalizedRoleName, cancellationToken); + } + + /// + /// Return a user role for the userId and roleId if it exists. + /// + /// The user's id. + /// The role's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user role if it exists. + public override Task FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken) + { + return UserRoles.FindAsync(new object[] { userId, roleId }, cancellationToken); + } + + /// + /// Return a user with the matching userId if it exists. + /// + /// The user's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user if it exists. + public override Task FindUserAsync(TKey userId, CancellationToken cancellationToken) + { + return Users.SingleOrDefaultAsync(u => u.Id.Equals(userId), cancellationToken); + } + + /// + /// Return a user login with the matching userId, provider, providerKey if it exists. + /// + /// The user's id. + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + public override Task FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken) + { + return UserLogins.SingleOrDefaultAsync(userLogin => userLogin.UserId.Equals(userId) && userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey, cancellationToken); + } + + /// + /// Return a user login with provider, providerKey if it exists. + /// + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + public override Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + { + return UserLogins.SingleOrDefaultAsync(userLogin => userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey, cancellationToken); + } + + + /// + /// Adds the given to the specified . + /// + /// The user to add the role to. + /// The role to add. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (string.IsNullOrWhiteSpace(normalizedRoleName)) + { + throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName)); + } + var roleEntity = await FindRoleAsync(normalizedRoleName, cancellationToken); + if (roleEntity == null) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.RoleNotFound, normalizedRoleName)); + } + UserRoles.Add(CreateUserRole(user, roleEntity)); + } + + /// + /// Removes the given from the specified . + /// + /// The user to remove the role from. + /// The role to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (string.IsNullOrWhiteSpace(normalizedRoleName)) + { + throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName)); + } + var roleEntity = await FindRoleAsync(normalizedRoleName, cancellationToken); + if (roleEntity != null) + { + var userRole = await FindUserRoleAsync(user.Id, roleEntity.Id, cancellationToken); + if (userRole != null) + { + UserRoles.Remove(userRole); + } + } + } + + /// + /// Retrieves the roles the specified is a member of. + /// + /// The user whose roles should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the roles the user is a member of. + public override async Task> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var userId = user.Id; + var query = from userRole in UserRoles + join role in Roles on userRole.RoleId equals role.Id + where userRole.UserId.Equals(userId) + select role.Name; + return await query.ToListAsync(); + } + + /// + /// Returns a flag indicating if the specified user is a member of the give . + /// + /// The user whose role membership should be checked. + /// The role to check membership of + /// The used to propagate notifications that the operation should be canceled. + /// A containing a flag indicating if the specified user is a member of the given group. If the + /// user is a member of the group the returned value with be true, otherwise it will be false. + public override async Task IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (string.IsNullOrWhiteSpace(normalizedRoleName)) + { + throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName)); + } + var role = await FindRoleAsync(normalizedRoleName, cancellationToken); + if (role != null) + { + var userRole = await FindUserRoleAsync(user.Id, role.Id, cancellationToken); + return userRole != null; + } + return false; + } + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The user whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a user. + public async override Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + return await UserClaims.Where(uc => uc.UserId.Equals(user.Id)).Select(c => c.ToClaim()).ToListAsync(cancellationToken); + } + + /// + /// Adds the given to the specified . + /// + /// The user to add the claim to. + /// The claim to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (claims == null) + { + throw new ArgumentNullException(nameof(claims)); + } + foreach (var claim in claims) + { + UserClaims.Add(CreateUserClaim(user, claim)); + } + return Task.FromResult(false); + } + + /// + /// Replaces the on the specified , with the . + /// + /// The user to replace the claim on. + /// The claim replace. + /// The new claim replacing the . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + if (newClaim == null) + { + throw new ArgumentNullException(nameof(newClaim)); + } + + var matchedClaims = await UserClaims.Where(uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken); + foreach (var matchedClaim in matchedClaims) + { + matchedClaim.ClaimValue = newClaim.Value; + matchedClaim.ClaimType = newClaim.Type; + } + } + + /// + /// Removes the given from the specified . + /// + /// The user to remove the claims from. + /// The claim to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async override Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (claims == null) + { + throw new ArgumentNullException(nameof(claims)); + } + foreach (var claim in claims) + { + var matchedClaims = await UserClaims.Where(uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken); + foreach (var c in matchedClaims) + { + UserClaims.Remove(c); + } + } + } + + /// + /// Adds the given to the specified . + /// + /// The user to add the login to. + /// The login to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddLoginAsync(TUser user, UserLoginInfo login, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (login == null) + { + throw new ArgumentNullException(nameof(login)); + } + UserLogins.Add(CreateUserLogin(user, login)); + return Task.FromResult(false); + } + + /// + /// Removes the given from the specified . + /// + /// The user to remove the login from. + /// The login to remove from the user. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var entry = await FindUserLoginAsync(user.Id, loginProvider, providerKey, cancellationToken); + if (entry != null) + { + UserLogins.Remove(entry); + } + } + + /// + /// Retrieves the associated logins for the specified . + /// + /// The user whose associated logins to retrieve. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing a list of for the specified , if any. + /// + public async override Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var userId = user.Id; + return await UserLogins.Where(l => l.UserId.Equals(userId)) + .Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)).ToListAsync(cancellationToken); + } + + /// + /// Retrieves the user associated with the specified login provider and login provider key. + /// + /// The login provider who provided the . + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. + /// + public async override Task FindByLoginAsync(string loginProvider, string providerKey, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var userLogin = await FindUserLoginAsync(loginProvider, providerKey, cancellationToken); + if (userLogin != null) + { + return await FindUserAsync(userLogin.UserId, cancellationToken); + } + return null; + } + + /// + /// Gets the user, if any, associated with the specified, normalized email address. + /// + /// The normalized email address to return the user for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. + /// + public override Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return Users.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, cancellationToken); + } + + /// + /// Retrieves all users with the specified claim. + /// + /// The claim whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that contain the specified claim. + /// + public async override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + + var query = from userclaims in UserClaims + join user in Users on userclaims.UserId equals user.Id + where userclaims.ClaimValue == claim.Value + && userclaims.ClaimType == claim.Type + select user; + + return await query.ToListAsync(cancellationToken); + } + + /// + /// Retrieves all users in the specified role. + /// + /// The role whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that are in the specified role. + /// + public async override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (string.IsNullOrEmpty(normalizedRoleName)) + { + throw new ArgumentNullException(nameof(normalizedRoleName)); + } + + var role = await FindRoleAsync(normalizedRoleName, cancellationToken); + + if (role != null) + { + var query = from userrole in UserRoles + join user in Users on userrole.UserId equals user.Id + where userrole.RoleId.Equals(role.Id) + select user; + + return await query.ToListAsync(cancellationToken); + } + return new List(); + } + + /// + /// Find a user token if it exists. + /// + /// The token owner. + /// The login provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The user token if it exists. + public override Task FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + => UserTokens.FindAsync(new object[] { user.Id, loginProvider, name }, cancellationToken); + + /// + /// Add a new user token. + /// + /// The token to be added. + /// + public override Task AddUserTokenAsync(TUserToken token) + { + UserTokens.Add(token); + return TaskCache.CompletedTask; + } + + /// + /// Remove a new user token. + /// + /// The token to be removed. + /// + public override Task RemoveUserTokenAsync(TUserToken token) + { + UserTokens.Remove(token); + return TaskCache.CompletedTask; + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStoreV1.cs b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStoreV1.cs new file mode 100644 index 0000000000..c8a0871aca --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStoreV1.cs @@ -0,0 +1,373 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore +{ + /// + /// Represents a new instance of a persistence store for the specified user and role types that supports + /// . + /// + /// The type representing a user. + /// The type representing a role. + /// The type of the data context class used to access the store. + /// The type of the primary key for a role. + /// The type representing a claim. + /// The type representing a user role. + /// The type representing a user external login. + /// The type representing a user token. + /// The type representing a role claim. + public class UserStoreV1 : + UserStoreBaseV1 + where TUser : IdentityUser + where TRole : IdentityRole + where TContext : DbContext + where TKey : IEquatable + where TUserClaim : IdentityUserClaim, new() + where TUserRole : IdentityUserRole, new() + where TUserLogin : IdentityUserLogin, new() + where TUserToken : IdentityUserToken, new() + where TRoleClaim : IdentityRoleClaim, new() + { + /// + /// Creates a new instance of the store. + /// + /// The context used to access the store. + /// The used to describe store errors. + public UserStoreV1(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber()) + { + _core = new UserStoreCore(context, describer); + } + + private readonly UserStoreCore _core; + + /// + /// Gets the database context for this store. + /// + public TContext Context => _core.Context; + + /// + /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + /// + /// + /// True if changes should be automatically persisted, otherwise false. + /// + public bool AutoSaveChanges + { + get => _core.AutoSaveChanges; + set => _core.AutoSaveChanges = value; + } + + /// Saves the current store. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + protected Task SaveChanges(CancellationToken cancellationToken) => _core.SaveChanges(cancellationToken); + + /// + /// Creates the specified in the user store. + /// + /// The user to create. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the creation operation. + public override Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.CreateAsync(user, cancellationToken); + + /// + /// Updates the specified in the user store. + /// + /// The user to update. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public override Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.UpdateAsync(user, cancellationToken); + + /// + /// Deletes the specified from the user store. + /// + /// The user to delete. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public override Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.DeleteAsync(user, cancellationToken); + + /// + /// Finds and returns a user, if any, who has the specified . + /// + /// The user ID to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + => _core.FindByIdAsync(userId, cancellationToken); + + /// + /// Finds and returns a user, if any, who has the specified normalized user name. + /// + /// The normalized user name to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public override Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) + => _core.FindByNameAsync(normalizedUserName, cancellationToken); + + /// + /// A navigation property for the users the store contains. + /// + public override IQueryable Users => _core.UsersSet; + + /// + /// Return a role with the normalized name if it exists. + /// + /// The normalized role name. + /// The used to propagate notifications that the operation should be canceled. + /// The role if it exists. + public override Task FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) + => _core.FindRoleAsync(normalizedRoleName, cancellationToken); + + /// + /// Return a user role for the userId and roleId if it exists. + /// + /// The user's id. + /// The role's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user role if it exists. + public override Task FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken) + => _core.FindUserRoleAsync(userId, roleId, cancellationToken); + + /// + /// Return a user with the matching userId if it exists. + /// + /// The user's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user if it exists. + public override Task FindUserAsync(TKey userId, CancellationToken cancellationToken) + => _core.FindUserAsync(userId, cancellationToken); + + /// + /// Return a user login with the matching userId, provider, providerKey if it exists. + /// + /// The user's id. + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + public override Task FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken) + => _core.FindUserLoginAsync(userId, loginProvider, providerKey, cancellationToken); + + /// + /// Return a user login with provider, providerKey if it exists. + /// + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + public override Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + => _core.FindUserLoginAsync(loginProvider, providerKey, cancellationToken); + + /// + /// Adds the given to the specified . + /// + /// The user to add the role to. + /// The role to add. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + => _core.AddToRoleAsync(user, normalizedRoleName, cancellationToken); + + /// + /// Removes the given from the specified . + /// + /// The user to remove the role from. + /// The role to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + => _core.RemoveFromRoleAsync(user, normalizedRoleName, cancellationToken); + + /// + /// Retrieves the roles the specified is a member of. + /// + /// The user whose roles should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the roles the user is a member of. + public override Task> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.GetRolesAsync(user, cancellationToken); + + /// + /// Returns a flag indicating if the specified user is a member of the give . + /// + /// The user whose role membership should be checked. + /// The role to check membership of + /// The used to propagate notifications that the operation should be canceled. + /// A containing a flag indicating if the specified user is a member of the given group. If the + /// user is a member of the group the returned value with be true, otherwise it will be false. + public override Task IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + => _core.IsInRoleAsync(user, normalizedRoleName, cancellationToken); + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The user whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a user. + public override Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.GetClaimsAsync(user, cancellationToken); + + /// + /// Adds the given to the specified . + /// + /// The user to add the claim to. + /// The claim to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + => _core.AddClaimsAsync(user, claims, cancellationToken); + + /// + /// Replaces the on the specified , with the . + /// + /// The user to replace the claim on. + /// The claim replace. + /// The new claim replacing the . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + => _core.ReplaceClaimAsync(user, claim, newClaim, cancellationToken); + + /// + /// Removes the given from the specified . + /// + /// The user to remove the claims from. + /// The claim to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + => _core.RemoveClaimsAsync(user, claims, cancellationToken); + + /// + /// Adds the given to the specified . + /// + /// The user to add the login to. + /// The login to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)) + => _core.AddLoginAsync(user, login, cancellationToken); + + /// + /// Removes the given from the specified . + /// + /// The user to remove the login from. + /// The login to remove from the user. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + => _core.RemoveLoginAsync(user, loginProvider, providerKey, cancellationToken); + + /// + /// Retrieves the associated logins for the specified . + /// + /// The user whose associated logins to retrieve. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing a list of for the specified , if any. + /// + public override Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.GetLoginsAsync(user, cancellationToken); + + /// + /// Retrieves the user associated with the specified login provider and login provider key. + /// + /// The login provider who provided the . + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. + /// + public override Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + => _core.FindByLoginAsync(loginProvider, providerKey, cancellationToken); + + /// + /// Gets the user, if any, associated with the specified, normalized email address. + /// + /// The normalized email address to return the user for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. + /// + public override Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + => _core.FindByEmailAsync(normalizedEmail, cancellationToken); + + /// + /// Retrieves all users with the specified claim. + /// + /// The claim whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that contain the specified claim. + /// + public override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + => _core.GetUsersForClaimAsync(claim, cancellationToken); + + /// + /// Retrieves all users in the specified role. + /// + /// The role whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that are in the specified role. + /// + public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + => _core.GetUsersInRoleAsync(normalizedRoleName, cancellationToken); + + /// + /// Find a user token if it exists. + /// + /// The token owner. + /// The login provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The user token if it exists. + public override Task FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + => _core.FindTokenAsync(user, loginProvider, name, cancellationToken); + + /// + /// Add a new user token. + /// + /// The token to be added. + /// + public override Task AddUserTokenAsync(TUserToken token) + => _core.AddUserTokenAsync(token); + + /// + /// Remove a new user token. + /// + /// The token to be removed. + /// + public override Task RemoveUserTokenAsync(TUserToken token) + => _core.RemoveUserTokenAsync(token); + + /// + /// Dispose the store + /// + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) + { + _core.Dispose(); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStoreV2.cs b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStoreV2.cs new file mode 100644 index 0000000000..416e74a909 --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.EntityFrameworkCore/UserStoreV2.cs @@ -0,0 +1,373 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore +{ + /// + /// Represents a new instance of a persistence store for the specified user and role types that supports + /// . + /// + /// The type representing a user. + /// The type representing a role. + /// The type of the data context class used to access the store. + /// The type of the primary key for a role. + /// The type representing a claim. + /// The type representing a user role. + /// The type representing a user external login. + /// The type representing a user token. + /// The type representing a role claim. + public class UserStoreV2 : + UserStoreBaseV2 + where TUser : IdentityUser + where TRole : IdentityRole + where TContext : DbContext + where TKey : IEquatable + where TUserClaim : IdentityUserClaim, new() + where TUserRole : IdentityUserRole, new() + where TUserLogin : IdentityUserLogin, new() + where TUserToken : IdentityUserToken, new() + where TRoleClaim : IdentityRoleClaim, new() + { + /// + /// Creates a new instance of the store. + /// + /// The context used to access the store. + /// The used to describe store errors. + public UserStoreV2(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber()) + { + _core = new UserStoreCore(context, describer); + } + + private readonly UserStoreCore _core; + + /// + /// Gets the database context for this store. + /// + public TContext Context => _core.Context; + + /// + /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + /// + /// + /// True if changes should be automatically persisted, otherwise false. + /// + public bool AutoSaveChanges + { + get => _core.AutoSaveChanges; + set => _core.AutoSaveChanges = value; + } + + /// Saves the current store. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + protected Task SaveChanges(CancellationToken cancellationToken) => _core.SaveChanges(cancellationToken); + + /// + /// Creates the specified in the user store. + /// + /// The user to create. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the creation operation. + public override Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.CreateAsync(user, cancellationToken); + + /// + /// Updates the specified in the user store. + /// + /// The user to update. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public override Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.UpdateAsync(user, cancellationToken); + + /// + /// Deletes the specified from the user store. + /// + /// The user to delete. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public override Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.DeleteAsync(user, cancellationToken); + + /// + /// Finds and returns a user, if any, who has the specified . + /// + /// The user ID to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + => _core.FindByIdAsync(userId, cancellationToken); + + /// + /// Finds and returns a user, if any, who has the specified normalized user name. + /// + /// The normalized user name to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public override Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) + => _core.FindByNameAsync(normalizedUserName, cancellationToken); + + /// + /// A navigation property for the users the store contains. + /// + public override IQueryable Users => _core.UsersSet; + + /// + /// Return a role with the normalized name if it exists. + /// + /// The normalized role name. + /// The used to propagate notifications that the operation should be canceled. + /// The role if it exists. + public override Task FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) + => _core.FindRoleAsync(normalizedRoleName, cancellationToken); + + /// + /// Return a user role for the userId and roleId if it exists. + /// + /// The user's id. + /// The role's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user role if it exists. + public override Task FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken) + => _core.FindUserRoleAsync(userId, roleId, cancellationToken); + + /// + /// Return a user with the matching userId if it exists. + /// + /// The user's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user if it exists. + public override Task FindUserAsync(TKey userId, CancellationToken cancellationToken) + => _core.FindUserAsync(userId, cancellationToken); + + /// + /// Return a user login with the matching userId, provider, providerKey if it exists. + /// + /// The user's id. + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + public override Task FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken) + => _core.FindUserLoginAsync(userId, loginProvider, providerKey, cancellationToken); + + /// + /// Return a user login with provider, providerKey if it exists. + /// + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + public override Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + => _core.FindUserLoginAsync(loginProvider, providerKey, cancellationToken); + + /// + /// Adds the given to the specified . + /// + /// The user to add the role to. + /// The role to add. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + => _core.AddToRoleAsync(user, normalizedRoleName, cancellationToken); + + /// + /// Removes the given from the specified . + /// + /// The user to remove the role from. + /// The role to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + => _core.RemoveFromRoleAsync(user, normalizedRoleName, cancellationToken); + + /// + /// Retrieves the roles the specified is a member of. + /// + /// The user whose roles should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the roles the user is a member of. + public override Task> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.GetRolesAsync(user, cancellationToken); + + /// + /// Returns a flag indicating if the specified user is a member of the give . + /// + /// The user whose role membership should be checked. + /// The role to check membership of + /// The used to propagate notifications that the operation should be canceled. + /// A containing a flag indicating if the specified user is a member of the given group. If the + /// user is a member of the group the returned value with be true, otherwise it will be false. + public override Task IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + => _core.IsInRoleAsync(user, normalizedRoleName, cancellationToken); + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The user whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a user. + public override Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.GetClaimsAsync(user, cancellationToken); + + /// + /// Adds the given to the specified . + /// + /// The user to add the claim to. + /// The claim to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + => _core.AddClaimsAsync(user, claims, cancellationToken); + + /// + /// Replaces the on the specified , with the . + /// + /// The user to replace the claim on. + /// The claim replace. + /// The new claim replacing the . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + => _core.ReplaceClaimAsync(user, claim, newClaim, cancellationToken); + + /// + /// Removes the given from the specified . + /// + /// The user to remove the claims from. + /// The claim to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + => _core.RemoveClaimsAsync(user, claims, cancellationToken); + + /// + /// Adds the given to the specified . + /// + /// The user to add the login to. + /// The login to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)) + => _core.AddLoginAsync(user, login, cancellationToken); + + /// + /// Removes the given from the specified . + /// + /// The user to remove the login from. + /// The login to remove from the user. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + => _core.RemoveLoginAsync(user, loginProvider, providerKey, cancellationToken); + + /// + /// Retrieves the associated logins for the specified . + /// + /// The user whose associated logins to retrieve. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing a list of for the specified , if any. + /// + public override Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + => _core.GetLoginsAsync(user, cancellationToken); + + /// + /// Retrieves the user associated with the specified login provider and login provider key. + /// + /// The login provider who provided the . + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. + /// + public override Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + => _core.FindByLoginAsync(loginProvider, providerKey, cancellationToken); + + /// + /// Gets the user, if any, associated with the specified, normalized email address. + /// + /// The normalized email address to return the user for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. + /// + public override Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + => _core.FindByEmailAsync(normalizedEmail, cancellationToken); + + /// + /// Retrieves all users with the specified claim. + /// + /// The claim whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that contain the specified claim. + /// + public override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + => _core.GetUsersForClaimAsync(claim, cancellationToken); + + /// + /// Retrieves all users in the specified role. + /// + /// The role whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that are in the specified role. + /// + public override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + => _core.GetUsersInRoleAsync(normalizedRoleName, cancellationToken); + + /// + /// Find a user token if it exists. + /// + /// The token owner. + /// The login provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The user token if it exists. + public override Task FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + => _core.FindTokenAsync(user, loginProvider, name, cancellationToken); + + /// + /// Add a new user token. + /// + /// The token to be added. + /// + public override Task AddUserTokenAsync(TUserToken token) + => _core.AddUserTokenAsync(token); + + /// + /// Remove a new user token. + /// + /// The token to be removed. + /// + public override Task RemoveUserTokenAsync(TUserToken token) + => _core.RemoveUserTokenAsync(token); + + /// + /// Dispose the store + /// + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) + { + _core.Dispose(); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Specification.Tests/IdentitySpecificationTestBase.cs b/src/Microsoft.AspNetCore.Identity.Specification.Tests/IdentitySpecificationTestBase.cs index 220dd11818..8e8785766d 100644 --- a/src/Microsoft.AspNetCore.Identity.Specification.Tests/IdentitySpecificationTestBase.cs +++ b/src/Microsoft.AspNetCore.Identity.Specification.Tests/IdentitySpecificationTestBase.cs @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Identity.Test /// /// /// - protected virtual void SetupIdentityServices(IServiceCollection services, object context = null) + protected virtual void SetupIdentityServices(IServiceCollection services, object context) { services.AddSingleton(new ConfigurationBuilder().Build()); services.AddSingleton(); @@ -192,6 +192,31 @@ namespace Microsoft.AspNetCore.Identity.Test /// The query to use. protected abstract Expression> RoleNameStartsWithPredicate(string roleName); + /// + /// Test. + /// + /// Task + [Fact] + public async Task CreateUserWillSetCreateDateOnlyIfSupported() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var userId = await manager.GetUserIdAsync(user); + if (manager.SupportsUserActivity) + { + Assert.NotNull(await manager.GetCreateDateAsync(user)); + } + else + { + await Assert.ThrowsAsync(() => manager.GetCreateDateAsync(user)); + } + } + /// /// Test. /// @@ -490,9 +515,17 @@ namespace Microsoft.AspNetCore.Identity.Test IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); manager.PasswordValidators.Clear(); manager.PasswordValidators.Add(new AlwaysBadValidator()); + if (manager.SupportsUserActivity) + { + Assert.Null(await manager.GetLastPasswordChangeDateAsync(user)); + } IdentityResultAssert.IsFailure(await manager.AddPasswordAsync(user, "password"), AlwaysBadValidator.ErrorMessage); IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(user)} password validation failed: {AlwaysBadValidator.ErrorMessage.Code}."); + if (manager.SupportsUserActivity) + { + Assert.Null(await manager.GetLastPasswordChangeDateAsync(user)); + } } /// @@ -578,6 +611,12 @@ namespace Microsoft.AspNetCore.Identity.Test var logins = await manager.GetLoginsAsync(user); Assert.NotNull(logins); Assert.Equal(0, logins.Count()); + if (manager.SupportsUserActivity) + { + Assert.NotNull(await manager.GetCreateDateAsync(user)); + Assert.Null(await manager.GetLastPasswordChangeDateAsync(user)); + Assert.Null(await manager.GetLastSignInDateAsync(user)); + } } /// @@ -604,6 +643,12 @@ namespace Microsoft.AspNetCore.Identity.Test Assert.Equal(provider, logins.First().LoginProvider); Assert.Equal(providerKey, logins.First().ProviderKey); Assert.Equal(display, logins.First().ProviderDisplayName); + if (manager.SupportsUserActivity) + { + Assert.NotNull(await manager.GetCreateDateAsync(user)); + Assert.Null(await manager.GetLastPasswordChangeDateAsync(user)); + Assert.Null(await manager.GetLastSignInDateAsync(user)); + } } /// @@ -624,13 +669,27 @@ namespace Microsoft.AspNetCore.Identity.Test var login = new UserLoginInfo("Provider", userId, "display"); IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, login)); Assert.False(await manager.HasPasswordAsync(user)); + if (manager.SupportsUserActivity) + { + Assert.Null(await manager.GetLastPasswordChangeDateAsync(user)); + } IdentityResultAssert.IsSuccess(await manager.AddPasswordAsync(user, "password")); + if (manager.SupportsUserActivity) + { + Assert.NotNull(await manager.GetLastPasswordChangeDateAsync(user)); + } Assert.True(await manager.HasPasswordAsync(user)); var logins = await manager.GetLoginsAsync(user); Assert.NotNull(logins); Assert.Equal(1, logins.Count()); Assert.Equal(user, await manager.FindByLoginAsync(login.LoginProvider, login.ProviderKey)); Assert.True(await manager.CheckPasswordAsync(user, "password")); + + if (manager.SupportsUserActivity) + { + Assert.NotNull(await manager.GetCreateDateAsync(user)); + Assert.Null(await manager.GetLastSignInDateAsync(user)); + } } /// @@ -728,12 +787,57 @@ namespace Microsoft.AspNetCore.Identity.Test const string password = "password"; const string newPassword = "newpassword"; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, password)); + DateTimeOffset? createChange = null; + if (manager.SupportsUserActivity) + { + createChange = await manager.GetLastPasswordChangeDateAsync(user); + Assert.NotNull(createChange); + Assert.Null(await manager.GetLastSignInDateAsync(user)); + } + else + { + await Assert.ThrowsAsync(() => manager.GetLastPasswordChangeDateAsync(user)); + await Assert.ThrowsAsync(() => manager.GetLastSignInDateAsync(user)); + } var stamp = await manager.GetSecurityStampAsync(user); Assert.NotNull(stamp); IdentityResultAssert.IsSuccess(await manager.ChangePasswordAsync(user, password, newPassword)); Assert.False(await manager.CheckPasswordAsync(user, password)); Assert.True(await manager.CheckPasswordAsync(user, newPassword)); Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + + if (manager.SupportsUserActivity) + { + var changeDate = await manager.GetLastPasswordChangeDateAsync(user); + Assert.NotNull(changeDate); + Assert.True(createChange < changeDate); + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanUpdateLastSignInTime() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + if (manager.SupportsUserActivity) + { + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.Null(await manager.GetLastSignInDateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.UpdateLastSignInDateAsync(user)); + Assert.NotNull(await manager.GetLastSignInDateAsync(user)); + } + else + { + await Assert.ThrowsAsync(() => manager.UpdateLastSignInDateAsync(user)); + } } /// @@ -884,9 +988,20 @@ namespace Microsoft.AspNetCore.Identity.Test var manager = CreateManager(); var user = CreateTestUser(); IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "password")); + DateTimeOffset? createChange = null; + if (manager.SupportsUserActivity) + { + createChange = await manager.GetLastPasswordChangeDateAsync(user); + Assert.NotNull(createChange); + } var result = await manager.ChangePasswordAsync(user, "bogus", "newpassword"); IdentityResultAssert.IsFailure(result, "Incorrect password."); IdentityResultAssert.VerifyLogMessage(manager.Logger, $"Change password failed for user {await manager.GetUserIdAsync(user)}."); + + if (manager.SupportsUserActivity) + { + Assert.Equal(createChange, await manager.GetLastPasswordChangeDateAsync(user)); + } } /// diff --git a/src/Microsoft.AspNetCore.Identity/SignInManager.cs b/src/Microsoft.AspNetCore.Identity/SignInManager.cs index 566477cd1b..8f8c9811ec 100644 --- a/src/Microsoft.AspNetCore.Identity/SignInManager.cs +++ b/src/Microsoft.AspNetCore.Identity/SignInManager.cs @@ -163,7 +163,7 @@ namespace Microsoft.AspNetCore.Identity { var auth = await Context.AuthenticateAsync(IdentityConstants.ApplicationScheme); var authenticationMethod = auth?.Principal?.FindFirstValue(ClaimTypes.AuthenticationMethod); - await SignInAsync(user, auth?.Properties, authenticationMethod); + await SignInAsync(user, auth?.Properties, authenticationMethod, updateLastSignIn: false); } /// @@ -172,8 +172,9 @@ namespace Microsoft.AspNetCore.Identity /// The user to sign-in. /// Flag indicating whether the sign-in cookie should persist after the browser is closed. /// Name of the method used to authenticate the user. + /// If true, will update the last sign for the user if supported. /// The task object representing the asynchronous operation. - public virtual Task SignInAsync(TUser user, bool isPersistent, string authenticationMethod = null) + public virtual Task SignInAsync(TUser user, bool isPersistent, string authenticationMethod = null, bool updateLastSignIn = true) { return SignInAsync(user, new AuthenticationProperties { IsPersistent = isPersistent }, authenticationMethod); } @@ -184,8 +185,9 @@ namespace Microsoft.AspNetCore.Identity /// The user to sign-in. /// Properties applied to the login and authentication cookie. /// Name of the method used to authenticate the user. + /// If true, will update the last sign for the user if supported. /// The task object representing the asynchronous operation. - public virtual async Task SignInAsync(TUser user, AuthenticationProperties authenticationProperties, string authenticationMethod = null) + public virtual async Task SignInAsync(TUser user, AuthenticationProperties authenticationProperties, string authenticationMethod = null, bool updateLastSignIn = true) { var userPrincipal = await CreateUserPrincipalAsync(user); // Review: should we guard against CreateUserPrincipal returning null? @@ -196,6 +198,10 @@ namespace Microsoft.AspNetCore.Identity await Context.SignInAsync(IdentityConstants.ApplicationScheme, userPrincipal, authenticationProperties ?? new AuthenticationProperties()); + if (updateLastSignIn && UserManager.SupportsUserActivity) + { + await UserManager.UpdateLastSignInDateAsync(user); + } } /// diff --git a/src/Microsoft.Extensions.Identity.Core/IUserActivityStore.cs b/src/Microsoft.Extensions.Identity.Core/IUserActivityStore.cs new file mode 100644 index 0000000000..c7d924b9ee --- /dev/null +++ b/src/Microsoft.Extensions.Identity.Core/IUserActivityStore.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity +{ + /// + /// Provides an abstraction for a store containing users' password expiration data. + /// + /// The type encapsulating a user. + public interface IUserActivityStore : IUserStore where TUser : class + { + /// + /// Sets the last password change date for the specified . + /// + /// The user. + /// The last password change date. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + Task SetLastPasswordChangeDateAsync(TUser user, DateTimeOffset? changeDate, CancellationToken cancellationToken); + + /// + /// Gets the last password change date for the specified . + /// + /// The user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, returning the password hash for the specified . + Task GetLastPasswordChangeDateAsync(TUser user, CancellationToken cancellationToken); + + /// + /// Sets the creation date for the specified . + /// + /// The user. + /// The date the user was created. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + Task SetCreateDateAsync(TUser user, DateTimeOffset? creationDate, CancellationToken cancellationToken); + + /// + /// Gets the creation date for the specified . + /// + /// The user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, returning the password hash for the specified . + Task GetCreateDateAsync(TUser user, CancellationToken cancellationToken); + + /// + /// Sets the last signin date for the specified . + /// + /// The user. + /// The date the user last signed in. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + Task SetLastSignInDateAsync(TUser user, DateTimeOffset? lastSignIn, CancellationToken cancellationToken); + + /// + /// Gets the last signin date for the specified . + /// + /// The user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, returning the password hash for the specified . + Task GetLastSignInDateAsync(TUser user, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Identity.Core/Properties/Resources.Designer.cs b/src/Microsoft.Extensions.Identity.Core/Properties/Resources.Designer.cs index d39c98abad..e29a08dd06 100644 --- a/src/Microsoft.Extensions.Identity.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.Extensions.Identity.Core/Properties/Resources.Designer.cs @@ -762,6 +762,22 @@ namespace Microsoft.AspNetCore.Identity return string.Format(CultureInfo.CurrentCulture, GetString("PasswordRequiresUniqueChars"), p0); } + /// + /// Store does not implement IUserActivityStore<User>. + /// + internal static string StoreNotIUserActivityStore + { + get { return GetString("StoreNotIUserActivityStore"); } + } + + /// + /// Store does not implement IUserActivityStore<User>. + /// + internal static string FormatStoreNotIUserActivityStore() + { + return GetString("StoreNotIUserActivityStore"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.Extensions.Identity.Core/Resources.resx b/src/Microsoft.Extensions.Identity.Core/Resources.resx index 39a1443a4b..4df00dfadf 100644 --- a/src/Microsoft.Extensions.Identity.Core/Resources.resx +++ b/src/Microsoft.Extensions.Identity.Core/Resources.resx @@ -305,4 +305,8 @@ Passwords must use at least {0} different characters. Error message for passwords that are based on similar characters + + Store does not implement IUserActivityStore<User>. + Error when the store does not implement this interface + \ No newline at end of file diff --git a/src/Microsoft.Extensions.Identity.Core/SignInOptions.cs b/src/Microsoft.Extensions.Identity.Core/SignInOptions.cs index ac8ddc0571..5f4fae7a71 100644 --- a/src/Microsoft.Extensions.Identity.Core/SignInOptions.cs +++ b/src/Microsoft.Extensions.Identity.Core/SignInOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; + namespace Microsoft.AspNetCore.Identity { /// diff --git a/src/Microsoft.Extensions.Identity.Core/SignInResult.cs b/src/Microsoft.Extensions.Identity.Core/SignInResult.cs index faae170c3b..9b2bfcdc64 100644 --- a/src/Microsoft.Extensions.Identity.Core/SignInResult.cs +++ b/src/Microsoft.Extensions.Identity.Core/SignInResult.cs @@ -38,6 +38,11 @@ namespace Microsoft.AspNetCore.Identity /// True if the user attempting to sign-in requires two factor authentication, otherwise false. public bool RequiresTwoFactor { get; protected set; } + /// + /// Returns a flag indication whether the user attempting to sign-in requires a password change. + /// + public bool RequiresPasswordChange { get; protected set; } + /// /// Returns a that represents a successful sign-in. /// diff --git a/src/Microsoft.Extensions.Identity.Core/UserManager.cs b/src/Microsoft.Extensions.Identity.Core/UserManager.cs index 6ab877124b..6f283dab33 100644 --- a/src/Microsoft.Extensions.Identity.Core/UserManager.cs +++ b/src/Microsoft.Extensions.Identity.Core/UserManager.cs @@ -339,6 +339,21 @@ namespace Microsoft.AspNetCore.Identity } } + /// + /// Gets a flag indicating whether the backing user store supports tracking user activity like creation date. + /// + /// + /// true if the backing user store supports user activity tracking, otherwise false. + /// + public virtual bool SupportsUserActivity + { + get + { + ThrowIfDisposed(); + return Store is IUserActivityStore; + } + } + /// /// Gets a flag indicating whether the backing user store supports returning /// collections of information. @@ -466,6 +481,11 @@ namespace Microsoft.AspNetCore.Identity await UpdateNormalizedUserNameAsync(user); await UpdateNormalizedEmailAsync(user); + if (SupportsUserActivity) + { + await GetUserActivityStore().SetCreateDateAsync(user, DateTimeOffset.UtcNow, CancellationToken); + } + return await Store.CreateAsync(user, CancellationToken); } @@ -2225,6 +2245,79 @@ namespace Microsoft.AspNetCore.Identity return IdentityResult.Failed(ErrorDescriber.RecoveryCodeRedemptionFailed()); } + /// + /// Gets a date representing when a user was created. + /// + /// The user. + /// + /// A that represents the lookup. + /// + public virtual Task GetCreateDateAsync(TUser user) + { + ThrowIfDisposed(); + var store = GetUserActivityStore(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return store.GetCreateDateAsync(user, CancellationToken); + } + + /// + /// Gets a date representing when the user last changed their password. + /// + /// The user. + /// + /// A that represents the lookup. + /// + public virtual Task GetLastPasswordChangeDateAsync(TUser user) + { + ThrowIfDisposed(); + var store = GetUserActivityStore(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return store.GetLastPasswordChangeDateAsync(user, CancellationToken); + } + + /// + /// Gets a date representing when the user last signed in. + /// + /// The user. + /// + /// A that represents the lookup. + /// + public virtual Task GetLastSignInDateAsync(TUser user) + { + ThrowIfDisposed(); + var store = GetUserActivityStore(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return store.GetLastSignInDateAsync(user, CancellationToken); + } + + /// + /// Update the last sign in date to DateTimeOffest.UtcNow. + /// + /// The user. + /// + /// A that represents the lookup. + /// + public virtual async Task UpdateLastSignInDateAsync(TUser user) + { + ThrowIfDisposed(); + var store = GetUserActivityStore(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + await store.SetLastSignInDateAsync(user, DateTimeOffset.UtcNow, CancellationToken); + return await UpdateAsync(user); + } + /// /// Releases the unmanaged resources used by the role manager and optionally releases the managed resources. /// @@ -2248,6 +2341,17 @@ namespace Microsoft.AspNetCore.Identity return cast; } + internal IUserActivityStore GetUserActivityStore() + { + var cast = Store as IUserActivityStore; + if (cast == null) + { + throw new NotSupportedException(Resources.StoreNotIUserActivityStore); + } + return cast; + } + + internal IUserLockoutStore GetUserLockoutStore() { var cast = Store as IUserLockoutStore; @@ -2310,6 +2414,10 @@ namespace Microsoft.AspNetCore.Identity } var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null; await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken); + if (SupportsUserActivity) + { + await GetUserActivityStore().SetLastPasswordChangeDateAsync(user, DateTimeOffset.UtcNow, CancellationToken); + } await UpdateSecurityStampInternal(user); return IdentityResult.Success; } diff --git a/src/Microsoft.Extensions.Identity.Stores/IdentityStoreOptions.cs b/src/Microsoft.Extensions.Identity.Stores/IdentityStoreOptions.cs new file mode 100644 index 0000000000..36bb891aef --- /dev/null +++ b/src/Microsoft.Extensions.Identity.Stores/IdentityStoreOptions.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity +{ + /// + /// Used for store schema versioning. + /// + public class IdentityStoreOptions + { + /// + /// Matches version 1.x.x + /// + public const string Version1_0 = "v1.0"; + + /// + /// Matches version 2.0.0 + /// + public const string Version2_0 = "v2.0"; + + /// + /// Used to represent the most current version. + /// + public const string Version_Latest = "latest"; + + /// + /// Used to determine what features/schema are supported in the store. + /// + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Identity.Stores/IdentityUser.cs b/src/Microsoft.Extensions.Identity.Stores/IdentityUser.cs index efc8a9b713..3769ffcb07 100644 --- a/src/Microsoft.Extensions.Identity.Stores/IdentityUser.cs +++ b/src/Microsoft.Extensions.Identity.Stores/IdentityUser.cs @@ -149,6 +149,21 @@ namespace Microsoft.AspNetCore.Identity /// public virtual int AccessFailedCount { get; set; } + /// + /// Gets or sets the date and time, in UTC, when the user was created. + /// + public virtual DateTimeOffset? CreateDate { get; set; } + + /// + /// Gets or sets the date and time, in UTC, when the user last signed in. + /// + public virtual DateTimeOffset? LastSignInDate { get; set; } + + /// + /// Gets or sets the date and time, in UTC, when the user last changed his password. + /// + public virtual DateTimeOffset? LastPasswordChangeDate { get; set; } + /// /// Returns the username for this user. /// diff --git a/src/Microsoft.Extensions.Identity.Stores/RoleStoreBase.cs b/src/Microsoft.Extensions.Identity.Stores/RoleStoreBase.cs index 3e7c5de7b1..2eeba89c56 100644 --- a/src/Microsoft.Extensions.Identity.Stores/RoleStoreBase.cs +++ b/src/Microsoft.Extensions.Identity.Stores/RoleStoreBase.cs @@ -2,26 +2,18 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Identity { /// - /// Creates a new instance of a persistence store for roles. + /// Creates a new instance of a persistence store for roles that always will match the latest /// Creates a new instance of a persistence store for roles that matches . /// /// The type of the class representing a role. /// The type of the primary key for a role. /// The type of the class representing a user role. /// The type of the class representing a role claim. public abstract class RoleStoreBase : - IQueryableRoleStore, - IRoleClaimStore + RoleStoreBaseV1 where TRole : IdentityRole where TKey : IEquatable where TUserRole : IdentityUserRole, new() @@ -31,242 +23,6 @@ namespace Microsoft.AspNetCore.Identity /// Constructs a new instance of . /// /// The . - public RoleStoreBase(IdentityErrorDescriber describer) - { - if (describer == null) - { - throw new ArgumentNullException(nameof(describer)); - } - - ErrorDescriber = describer; - } - - private bool _disposed; - - /// - /// Gets or sets the for any error that occurred with the current operation. - /// - public IdentityErrorDescriber ErrorDescriber { get; set; } - - /// - /// Creates a new role in a store as an asynchronous operation. - /// - /// The role to create in the store. - /// The used to propagate notifications that the operation should be canceled. - /// A that represents the of the asynchronous query. - public abstract Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Updates a role in a store as an asynchronous operation. - /// - /// The role to update in the store. - /// The used to propagate notifications that the operation should be canceled. - /// A that represents the of the asynchronous query. - public abstract Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Deletes a role from the store as an asynchronous operation. - /// - /// The role to delete from the store. - /// The used to propagate notifications that the operation should be canceled. - /// A that represents the of the asynchronous query. - public abstract Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Gets the ID for a role from the store as an asynchronous operation. - /// - /// The role whose ID should be returned. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the ID of the role. - public virtual Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - return Task.FromResult(ConvertIdToString(role.Id)); - } - - /// - /// Gets the name of a role from the store as an asynchronous operation. - /// - /// The role whose name should be returned. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the name of the role. - public virtual Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - return Task.FromResult(role.Name); - } - - /// - /// Sets the name of a role in the store as an asynchronous operation. - /// - /// The role whose name should be set. - /// The name of the role. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - role.Name = roleName; - return TaskCache.CompletedTask; - } - - /// - /// Converts the provided to a strongly typed key object. - /// - /// The id to convert. - /// An instance of representing the provided . - public virtual TKey ConvertIdFromString(string id) - { - if (id == null) - { - return default(TKey); - } - return (TKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id); - } - - /// - /// Converts the provided to its string representation. - /// - /// The id to convert. - /// An representation of the provided . - public virtual string ConvertIdToString(TKey id) - { - if (id.Equals(default(TKey))) - { - return null; - } - return id.ToString(); - } - - /// - /// Finds the role who has the specified ID as an asynchronous operation. - /// - /// The role ID to look for. - /// The used to propagate notifications that the operation should be canceled. - /// A that result of the look up. - public abstract Task FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Finds the role who has the specified normalized name as an asynchronous operation. - /// - /// The normalized role name to look for. - /// The used to propagate notifications that the operation should be canceled. - /// A that result of the look up. - public abstract Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Get a role's normalized name as an asynchronous operation. - /// - /// The role whose normalized name should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the name of the role. - public virtual Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - return Task.FromResult(role.NormalizedName); - } - - /// - /// Set a role's normalized name as an asynchronous operation. - /// - /// The role whose normalized name should be set. - /// The normalized name to set - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - role.NormalizedName = normalizedName; - return TaskCache.CompletedTask; - } - - /// - /// Throws if this class has been disposed. - /// - protected void ThrowIfDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - /// - /// Dispose the stores - /// - public void Dispose() - { - _disposed = true; - } - - /// - /// Get the claims associated with the specified as an asynchronous operation. - /// - /// The role whose claims should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the claims granted to a role. - public abstract Task> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Adds the given to the specified . - /// - /// The role to add the claim to. - /// The claim to add to the role. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public abstract Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Removes the given from the specified . - /// - /// The role to remove the claim from. - /// The claim to remove from the role. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public abstract Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// A navigation property for the roles the store contains. - /// - public abstract IQueryable Roles - { - get; - } - - /// - /// Creates a entity representing a role claim. - /// - /// The associated role. - /// The associated claim. - /// The role claim entity. - protected virtual TRoleClaim CreateRoleClaim(TRole role, Claim claim) - { - return new TRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value }; - } + public RoleStoreBase(IdentityErrorDescriber describer) : base(describer) { } } } diff --git a/src/Microsoft.Extensions.Identity.Stores/RoleStoreBaseV1.cs b/src/Microsoft.Extensions.Identity.Stores/RoleStoreBaseV1.cs new file mode 100644 index 0000000000..e1bc904453 --- /dev/null +++ b/src/Microsoft.Extensions.Identity.Stores/RoleStoreBaseV1.cs @@ -0,0 +1,272 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Identity +{ + /// + /// Creates a new instance of a persistence store for roles that matches . + /// + /// The type of the class representing a role. + /// The type of the primary key for a role. + /// The type of the class representing a user role. + /// The type of the class representing a role claim. + public abstract class RoleStoreBaseV1 : + IQueryableRoleStore, + IRoleClaimStore + where TRole : IdentityRole + where TKey : IEquatable + where TUserRole : IdentityUserRole, new() + where TRoleClaim : IdentityRoleClaim, new() + { + /// + /// Constructs a new instance of . + /// + /// The . + public RoleStoreBaseV1(IdentityErrorDescriber describer) + { + if (describer == null) + { + throw new ArgumentNullException(nameof(describer)); + } + + ErrorDescriber = describer; + } + + private bool _disposed; + + /// + /// Gets or sets the for any error that occurred with the current operation. + /// + public IdentityErrorDescriber ErrorDescriber { get; set; } + + /// + /// Creates a new role in a store as an asynchronous operation. + /// + /// The role to create in the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public abstract Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Updates a role in a store as an asynchronous operation. + /// + /// The role to update in the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public abstract Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Deletes a role from the store as an asynchronous operation. + /// + /// The role to delete from the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public abstract Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Gets the ID for a role from the store as an asynchronous operation. + /// + /// The role whose ID should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the ID of the role. + public virtual Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return Task.FromResult(ConvertIdToString(role.Id)); + } + + /// + /// Gets the name of a role from the store as an asynchronous operation. + /// + /// The role whose name should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the name of the role. + public virtual Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return Task.FromResult(role.Name); + } + + /// + /// Sets the name of a role in the store as an asynchronous operation. + /// + /// The role whose name should be set. + /// The name of the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + role.Name = roleName; + return TaskCache.CompletedTask; + } + + /// + /// Converts the provided to a strongly typed key object. + /// + /// The id to convert. + /// An instance of representing the provided . + public virtual TKey ConvertIdFromString(string id) + { + if (id == null) + { + return default(TKey); + } + return (TKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id); + } + + /// + /// Converts the provided to its string representation. + /// + /// The id to convert. + /// An representation of the provided . + public virtual string ConvertIdToString(TKey id) + { + if (id.Equals(default(TKey))) + { + return null; + } + return id.ToString(); + } + + /// + /// Finds the role who has the specified ID as an asynchronous operation. + /// + /// The role ID to look for. + /// The used to propagate notifications that the operation should be canceled. + /// A that result of the look up. + public abstract Task FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Finds the role who has the specified normalized name as an asynchronous operation. + /// + /// The normalized role name to look for. + /// The used to propagate notifications that the operation should be canceled. + /// A that result of the look up. + public abstract Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Get a role's normalized name as an asynchronous operation. + /// + /// The role whose normalized name should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the name of the role. + public virtual Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return Task.FromResult(role.NormalizedName); + } + + /// + /// Set a role's normalized name as an asynchronous operation. + /// + /// The role whose normalized name should be set. + /// The normalized name to set + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + role.NormalizedName = normalizedName; + return TaskCache.CompletedTask; + } + + /// + /// Throws if this class has been disposed. + /// + protected void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + /// Dispose the stores + /// + public void Dispose() + { + _disposed = true; + } + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The role whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a role. + public abstract Task> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Adds the given to the specified . + /// + /// The role to add the claim to. + /// The claim to add to the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public abstract Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Removes the given from the specified . + /// + /// The role to remove the claim from. + /// The claim to remove from the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public abstract Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// A navigation property for the roles the store contains. + /// + public abstract IQueryable Roles + { + get; + } + + /// + /// Creates a entity representing a role claim. + /// + /// The associated role. + /// The associated claim. + /// The role claim entity. + public virtual TRoleClaim CreateRoleClaim(TRole role, Claim claim) + { + return new TRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value }; + } + } +} diff --git a/src/Microsoft.Extensions.Identity.Stores/UserStoreBase.cs b/src/Microsoft.Extensions.Identity.Stores/UserStoreBase.cs index 80d7ebab9d..4be6c2f05a 100644 --- a/src/Microsoft.Extensions.Identity.Stores/UserStoreBase.cs +++ b/src/Microsoft.Extensions.Identity.Stores/UserStoreBase.cs @@ -2,18 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Identity { /// /// Represents a new instance of a persistence store for the specified user and role types. + /// This type will always support the most current schema. /// /// The type representing a user. /// The type representing a role. @@ -24,19 +18,7 @@ namespace Microsoft.AspNetCore.Identity /// The type representing a user token. /// The type representing a role claim. public abstract class UserStoreBase : - IUserLoginStore, - IUserRoleStore, - IUserClaimStore, - IUserPasswordStore, - IUserSecurityStampStore, - IUserEmailStore, - IUserLockoutStore, - IUserPhoneNumberStore, - IQueryableUserStore, - IUserTwoFactorStore, - IUserAuthenticationTokenStore, - IUserAuthenticatorKeyStore, - IUserTwoFactorRecoveryCodeStore + UserStoreBaseV2 where TUser : IdentityUser where TRole : IdentityRole where TKey : IEquatable @@ -50,1093 +32,7 @@ namespace Microsoft.AspNetCore.Identity /// Creates a new instance. /// /// The used to describe store errors. - public UserStoreBase(IdentityErrorDescriber describer) - { - if (describer == null) - { - throw new ArgumentNullException(nameof(describer)); - } + public UserStoreBase(IdentityErrorDescriber describer) : base(describer) { } - ErrorDescriber = describer; - } - - private bool _disposed; - - /// - /// Gets or sets the for any error that occurred with the current operation. - /// - public IdentityErrorDescriber ErrorDescriber { get; set; } - - /// - /// Called to create a new instance of a . - /// - /// The associated user. - /// The associated role. - /// - protected virtual TUserRole CreateUserRole(TUser user, TRole role) - { - return new TUserRole() - { - UserId = user.Id, - RoleId = role.Id - }; - } - - /// - /// Called to create a new instance of a . - /// - /// The associated user. - /// The associated claim. - /// - protected virtual TUserClaim CreateUserClaim(TUser user, Claim claim) - { - var userClaim = new TUserClaim { UserId = user.Id }; - userClaim.InitializeFromClaim(claim); - return userClaim; - } - - /// - /// Called to create a new instance of a . - /// - /// The associated user. - /// The sasociated login. - /// - protected virtual TUserLogin CreateUserLogin(TUser user, UserLoginInfo login) - { - return new TUserLogin - { - UserId = user.Id, - ProviderKey = login.ProviderKey, - LoginProvider = login.LoginProvider, - ProviderDisplayName = login.ProviderDisplayName - }; - } - - /// - /// Called to create a new instance of a . - /// - /// The associated user. - /// The associated login provider. - /// The name of the user token. - /// The value of the user token. - /// - protected virtual TUserToken CreateUserToken(TUser user, string loginProvider, string name, string value) - { - return new TUserToken - { - UserId = user.Id, - LoginProvider = loginProvider, - Name = name, - Value = value - }; - } - - /// - /// Gets the user identifier for the specified . - /// - /// The user whose identifier should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the identifier for the specified . - public virtual Task GetUserIdAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(ConvertIdToString(user.Id)); - } - - /// - /// Gets the user name for the specified . - /// - /// The user whose name should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the name for the specified . - public virtual Task GetUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.UserName); - } - - /// - /// Sets the given for the specified . - /// - /// The user whose name should be set. - /// The user name to set. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.UserName = userName; - return TaskCache.CompletedTask; - } - - /// - /// Gets the normalized user name for the specified . - /// - /// The user whose normalized name should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the normalized user name for the specified . - public virtual Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.NormalizedUserName); - } - - /// - /// Sets the given normalized name for the specified . - /// - /// The user whose name should be set. - /// The normalized name to set. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.NormalizedUserName = normalizedName; - return TaskCache.CompletedTask; - } - - /// - /// Creates the specified in the user store. - /// - /// The user to create. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the of the creation operation. - public abstract Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Updates the specified in the user store. - /// - /// The user to update. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the of the update operation. - public abstract Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Deletes the specified from the user store. - /// - /// The user to delete. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the of the update operation. - public abstract Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Finds and returns a user, if any, who has the specified . - /// - /// The user ID to search for. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The that represents the asynchronous operation, containing the user matching the specified if it exists. - /// - public abstract Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Converts the provided to a strongly typed key object. - /// - /// The id to convert. - /// An instance of representing the provided . - public virtual TKey ConvertIdFromString(string id) - { - if (id == null) - { - return default(TKey); - } - return (TKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id); - } - - /// - /// Converts the provided to its string representation. - /// - /// The id to convert. - /// An representation of the provided . - public virtual string ConvertIdToString(TKey id) - { - if (object.Equals(id, default(TKey))) - { - return null; - } - return id.ToString(); - } - - /// - /// Finds and returns a user, if any, who has the specified normalized user name. - /// - /// The normalized user name to search for. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The that represents the asynchronous operation, containing the user matching the specified if it exists. - /// - public abstract Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// A navigation property for the users the store contains. - /// - public abstract IQueryable Users - { - get; - } - - /// - /// Sets the password hash for a user. - /// - /// The user to set the password hash for. - /// The password hash to set. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.PasswordHash = passwordHash; - return TaskCache.CompletedTask; - } - - /// - /// Gets the password hash for a user. - /// - /// The user to retrieve the password hash for. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the password hash for the user. - public virtual Task GetPasswordHashAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.PasswordHash); - } - - /// - /// Returns a flag indicating if the specified user has a password. - /// - /// The user to retrieve the password hash for. - /// The used to propagate notifications that the operation should be canceled. - /// A containing a flag indicating if the specified user has a password. If the - /// user has a password the returned value with be true, otherwise it will be false. - public virtual Task HasPasswordAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult(user.PasswordHash != null); - } - - /// - /// Return a role with the normalized name if it exists. - /// - /// The normalized role name. - /// The used to propagate notifications that the operation should be canceled. - /// The role if it exists. - protected abstract Task FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken); - - /// - /// Return a user role for the userId and roleId if it exists. - /// - /// The user's id. - /// The role's id. - /// The used to propagate notifications that the operation should be canceled. - /// The user role if it exists. - protected abstract Task FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken); - - /// - /// Return a user with the matching userId if it exists. - /// - /// The user's id. - /// The used to propagate notifications that the operation should be canceled. - /// The user if it exists. - protected abstract Task FindUserAsync(TKey userId, CancellationToken cancellationToken); - - /// - /// Return a user login with the matching userId, provider, providerKey if it exists. - /// - /// The user's id. - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - protected abstract Task FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken); - - /// - /// Return a user login with provider, providerKey if it exists. - /// - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - protected abstract Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken); - - /// - /// Adds the given to the specified . - /// - /// The user to add the role to. - /// The role to add. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public abstract Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Removes the given from the specified . - /// - /// The user to remove the role from. - /// The role to remove. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public abstract Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Retrieves the roles the specified is a member of. - /// - /// The user whose roles should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the roles the user is a member of. - public abstract Task> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Returns a flag indicating if the specified user is a member of the give . - /// - /// The user whose role membership should be checked. - /// The role to check membership of - /// The used to propagate notifications that the operation should be canceled. - /// A containing a flag indicating if the specified user is a member of the given group. If the - /// user is a member of the group the returned value with be true, otherwise it will be false. - public abstract Task IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Throws if this class has been disposed. - /// - protected void ThrowIfDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - /// - /// Dispose the store - /// - public void Dispose() - { - _disposed = true; - } - - /// - /// Get the claims associated with the specified as an asynchronous operation. - /// - /// The user whose claims should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the claims granted to a user. - public abstract Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Adds the given to the specified . - /// - /// The user to add the claim to. - /// The claim to add to the user. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public abstract Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Replaces the on the specified , with the . - /// - /// The user to replace the claim on. - /// The claim replace. - /// The new claim replacing the . - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public abstract Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Removes the given from the specified . - /// - /// The user to remove the claims from. - /// The claim to remove. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public abstract Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Adds the given to the specified . - /// - /// The user to add the login to. - /// The login to add to the user. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public abstract Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Removes the given from the specified . - /// - /// The user to remove the login from. - /// The login to remove from the user. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public abstract Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Retrieves the associated logins for the specified . - /// - /// The user whose associated logins to retrieve. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The for the asynchronous operation, containing a list of for the specified , if any. - /// - public abstract Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Retrieves the user associated with the specified login provider and login provider key.. - /// - /// The login provider who provided the . - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. - /// - public async virtual Task FindByLoginAsync(string loginProvider, string providerKey, - CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - var userLogin = await FindUserLoginAsync(loginProvider, providerKey, cancellationToken); - if (userLogin != null) - { - return await FindUserAsync(userLogin.UserId, cancellationToken); - } - return null; - } - - /// - /// Gets a flag indicating whether the email address for the specified has been verified, true if the email address is verified otherwise - /// false. - /// - /// The user whose email confirmation status should be returned. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The task object containing the results of the asynchronous operation, a flag indicating whether the email address for the specified - /// has been confirmed or not. - /// - public virtual Task GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.EmailConfirmed); - } - - /// - /// Sets the flag indicating whether the specified 's email address has been confirmed or not. - /// - /// The user whose email confirmation status should be set. - /// A flag indicating if the email address has been confirmed, true if the address is confirmed otherwise false. - /// The used to propagate notifications that the operation should be canceled. - /// The task object representing the asynchronous operation. - public virtual Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.EmailConfirmed = confirmed; - return TaskCache.CompletedTask; - } - - /// - /// Sets the address for a . - /// - /// The user whose email should be set. - /// The email to set. - /// The used to propagate notifications that the operation should be canceled. - /// The task object representing the asynchronous operation. - public virtual Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.Email = email; - return TaskCache.CompletedTask; - } - - /// - /// Gets the email address for the specified . - /// - /// The user whose email should be returned. - /// The used to propagate notifications that the operation should be canceled. - /// The task object containing the results of the asynchronous operation, the email address for the specified . - public virtual Task GetEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.Email); - } - - /// - /// Returns the normalized email for the specified . - /// - /// The user whose email address to retrieve. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The task object containing the results of the asynchronous lookup operation, the normalized email address if any associated with the specified user. - /// - public virtual Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.NormalizedEmail); - } - - /// - /// Sets the normalized email for the specified . - /// - /// The user whose email address to set. - /// The normalized email to set for the specified . - /// The used to propagate notifications that the operation should be canceled. - /// The task object representing the asynchronous operation. - public virtual Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.NormalizedEmail = normalizedEmail; - return TaskCache.CompletedTask; - } - - /// - /// Gets the user, if any, associated with the specified, normalized email address. - /// - /// The normalized email address to return the user for. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. - /// - public abstract Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Gets the last a user's last lockout expired, if any. - /// Any time in the past should be indicates a user is not locked out. - /// - /// The user whose lockout date should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// - /// A that represents the result of the asynchronous query, a containing the last time - /// a user's lockout expired, if any. - /// - public virtual Task GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.LockoutEnd); - } - - /// - /// Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user. - /// - /// The user whose lockout date should be set. - /// The after which the 's lockout should end. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.LockoutEnd = lockoutEnd; - return TaskCache.CompletedTask; - } - - /// - /// Records that a failed access has occurred, incrementing the failed access count. - /// - /// The user whose cancellation count should be incremented. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the incremented failed access count. - public virtual Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.AccessFailedCount++; - return Task.FromResult(user.AccessFailedCount); - } - - /// - /// Resets a user's failed access count. - /// - /// The user whose failed access count should be reset. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - /// This is typically called after the account is successfully accessed. - public virtual Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.AccessFailedCount = 0; - return TaskCache.CompletedTask; - } - - /// - /// Retrieves the current failed access count for the specified .. - /// - /// The user whose failed access count should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the failed access count. - public virtual Task GetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.AccessFailedCount); - } - - /// - /// Retrieves a flag indicating whether user lockout can enabled for the specified user. - /// - /// The user whose ability to be locked out should be returned. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The that represents the asynchronous operation, true if a user can be locked out, otherwise false. - /// - public virtual Task GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.LockoutEnabled); - } - - /// - /// Set the flag indicating if the specified can be locked out.. - /// - /// The user whose ability to be locked out should be set. - /// A flag indicating if lock out can be enabled for the specified . - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.LockoutEnabled = enabled; - return TaskCache.CompletedTask; - } - - /// - /// Sets the telephone number for the specified . - /// - /// The user whose telephone number should be set. - /// The telephone number to set. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.PhoneNumber = phoneNumber; - return TaskCache.CompletedTask; - } - - /// - /// Gets the telephone number, if any, for the specified . - /// - /// The user whose telephone number should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the user's telephone number, if any. - public virtual Task GetPhoneNumberAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.PhoneNumber); - } - - /// - /// Gets a flag indicating whether the specified 's telephone number has been confirmed. - /// - /// The user to return a flag for, indicating whether their telephone number is confirmed. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The that represents the asynchronous operation, returning true if the specified has a confirmed - /// telephone number otherwise false. - /// - public virtual Task GetPhoneNumberConfirmedAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.PhoneNumberConfirmed); - } - - /// - /// Sets a flag indicating if the specified 's phone number has been confirmed.. - /// - /// The user whose telephone number confirmation status should be set. - /// A flag indicating whether the user's telephone number has been confirmed. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.PhoneNumberConfirmed = confirmed; - return TaskCache.CompletedTask; - } - - /// - /// Sets the provided security for the specified . - /// - /// The user whose security stamp should be set. - /// The security stamp to set. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - if (stamp == null) - { - throw new ArgumentNullException(nameof(stamp)); - } - user.SecurityStamp = stamp; - return TaskCache.CompletedTask; - } - - /// - /// Get the security stamp for the specified . - /// - /// The user whose security stamp should be set. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the security stamp for the specified . - public virtual Task GetSecurityStampAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.SecurityStamp); - } - - /// - /// Sets a flag indicating whether the specified has two factor authentication enabled or not, - /// as an asynchronous operation. - /// - /// The user whose two factor authentication enabled status should be set. - /// A flag indicating whether the specified has two factor authentication enabled. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - user.TwoFactorEnabled = enabled; - return TaskCache.CompletedTask; - } - - /// - /// Returns a flag indicating whether the specified has two factor authentication enabled or not, - /// as an asynchronous operation. - /// - /// The user whose two factor authentication enabled status should be set. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The that represents the asynchronous operation, containing a flag indicating whether the specified - /// has two factor authentication enabled or not. - /// - public virtual Task GetTwoFactorEnabledAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return Task.FromResult(user.TwoFactorEnabled); - } - - /// - /// Retrieves all users with the specified claim. - /// - /// The claim whose users should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The contains a list of users, if any, that contain the specified claim. - /// - public abstract Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Retrieves all users in the specified role. - /// - /// The role whose users should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// - /// The contains a list of users, if any, that are in the specified role. - /// - public abstract Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Find a user token if it exists. - /// - /// The token owner. - /// The login provider for the token. - /// The name of the token. - /// The used to propagate notifications that the operation should be canceled. - /// The user token if it exists. - protected abstract Task FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken); - - /// - /// Add a new user token. - /// - /// The token to be added. - /// - protected abstract Task AddUserTokenAsync(TUserToken token); - - /// - /// Remove a new user token. - /// - /// The token to be removed. - /// - protected abstract Task RemoveUserTokenAsync(TUserToken token); - - /// - /// Sets the token value for a particular user. - /// - /// The user. - /// The authentication provider for the token. - /// The name of the token. - /// The value of the token. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual async Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var token = await FindTokenAsync(user, loginProvider, name, cancellationToken); - if (token == null) - { - await AddUserTokenAsync(CreateUserToken(user, loginProvider, name, value)); - } - else - { - token.Value = value; - } - } - - /// - /// Deletes a token for a user. - /// - /// The user. - /// The authentication provider for the token. - /// The name of the token. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual async Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - var entry = await FindTokenAsync(user, loginProvider, name, cancellationToken); - if (entry != null) - { - await RemoveUserTokenAsync(entry); - } - } - - /// - /// Returns the token value. - /// - /// The user. - /// The authentication provider for the token. - /// The name of the token. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual async Task GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - var entry = await FindTokenAsync(user, loginProvider, name, cancellationToken); - return entry?.Value; - } - - private const string InternalLoginProvider = "[AspNetUserStore]"; - private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; - private const string RecoveryCodeTokenName = "RecoveryCodes"; - - /// - /// Sets the authenticator key for the specified . - /// - /// The user whose authenticator key should be set. - /// The authenticator key to set. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken) - { - return SetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, key, cancellationToken); - } - - /// - /// Get the authenticator key for the specified . - /// - /// The user whose security stamp should be set. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation, containing the security stamp for the specified . - public virtual Task GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken) - { - return GetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, cancellationToken); - } - - /// - /// Updates the recovery codes for the user while invalidating any previous recovery codes. - /// - /// The user to store new recovery codes for. - /// The new recovery codes for the user. - /// The used to propagate notifications that the operation should be canceled. - /// The new recovery codes for the user. - public virtual Task ReplaceCodesAsync(TUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) - { - var mergedCodes = string.Join(";", recoveryCodes); - return SetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, mergedCodes, cancellationToken); - } - - /// - /// Returns whether a recovery code is valid for a user. Note: recovery codes are only valid - /// once, and will be invalid after use. - /// - /// The user who owns the recovery code. - /// The recovery code to use. - /// The used to propagate notifications that the operation should be canceled. - /// True if the recovery code was found for the user. - public virtual async Task RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - if (code == null) - { - throw new ArgumentNullException(nameof(code)); - } - - var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? ""; - var splitCodes = mergedCodes.Split(';'); - if (splitCodes.Contains(code)) - { - var updatedCodes = new List(splitCodes.Where(s => s != code)); - await ReplaceCodesAsync(user, updatedCodes, cancellationToken); - return true; - } - return false; - } } } diff --git a/src/Microsoft.Extensions.Identity.Stores/UserStoreBaseV1.cs b/src/Microsoft.Extensions.Identity.Stores/UserStoreBaseV1.cs new file mode 100644 index 0000000000..d88e216029 --- /dev/null +++ b/src/Microsoft.Extensions.Identity.Stores/UserStoreBaseV1.cs @@ -0,0 +1,1157 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Identity +{ + /// + /// Represents a new instance of a persistence store for the specified user and role types. + /// + /// The type representing a user. + /// The type representing a role. + /// The type of the primary key for a role. + /// The type representing a claim. + /// The type representing a user role. + /// The type representing a user external login. + /// The type representing a user token. + /// The type representing a role claim. + public abstract class UserStoreBaseV1 : + IUserLoginStore, + IUserRoleStore, + IUserClaimStore, + IUserPasswordStore, + IUserSecurityStampStore, + IUserEmailStore, + IUserLockoutStore, + IUserPhoneNumberStore, + IQueryableUserStore, + IUserTwoFactorStore, + IUserAuthenticationTokenStore, + IUserAuthenticatorKeyStore, + IUserTwoFactorRecoveryCodeStore + where TUser : IdentityUser + where TRole : IdentityRole + where TKey : IEquatable + where TUserClaim : IdentityUserClaim, new() + where TUserRole : IdentityUserRole, new() + where TUserLogin : IdentityUserLogin, new() + where TUserToken : IdentityUserToken, new() + where TRoleClaim : IdentityRoleClaim, new() + { + /// + /// Creates a new instance. + /// + /// The used to describe store errors. + public UserStoreBaseV1(IdentityErrorDescriber describer) + { + if (describer == null) + { + throw new ArgumentNullException(nameof(describer)); + } + + ErrorDescriber = describer; + } + + private bool _disposed; + + /// + /// Gets or sets the for any error that occurred with the current operation. + /// + public IdentityErrorDescriber ErrorDescriber { get; set; } + + /// + /// Called to create a new instance of a . + /// + /// The associated user. + /// The associated role. + /// + public virtual TUserRole CreateUserRole(TUser user, TRole role) + { + return new TUserRole() + { + UserId = user.Id, + RoleId = role.Id + }; + } + + /// + /// Called to create a new instance of a . + /// + /// The associated user. + /// The associated claim. + /// + public virtual TUserClaim CreateUserClaim(TUser user, Claim claim) + { + var userClaim = new TUserClaim { UserId = user.Id }; + userClaim.InitializeFromClaim(claim); + return userClaim; + } + + /// + /// Called to create a new instance of a . + /// + /// The associated user. + /// The sasociated login. + /// + public virtual TUserLogin CreateUserLogin(TUser user, UserLoginInfo login) + { + return new TUserLogin + { + UserId = user.Id, + ProviderKey = login.ProviderKey, + LoginProvider = login.LoginProvider, + ProviderDisplayName = login.ProviderDisplayName + }; + } + + /// + /// Called to create a new instance of a . + /// + /// The associated user. + /// The associated login provider. + /// The name of the user token. + /// The value of the user token. + /// + public virtual TUserToken CreateUserToken(TUser user, string loginProvider, string name, string value) + { + return new TUserToken + { + UserId = user.Id, + LoginProvider = loginProvider, + Name = name, + Value = value + }; + } + + /// + /// Gets the user identifier for the specified . + /// + /// The user whose identifier should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the identifier for the specified . + public virtual Task GetUserIdAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(ConvertIdToString(user.Id)); + } + + /// + /// Gets the user name for the specified . + /// + /// The user whose name should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the name for the specified . + public virtual Task GetUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.UserName); + } + + /// + /// Sets the given for the specified . + /// + /// The user whose name should be set. + /// The user name to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.UserName = userName; + return TaskCache.CompletedTask; + } + + /// + /// Gets the normalized user name for the specified . + /// + /// The user whose normalized name should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the normalized user name for the specified . + public virtual Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.NormalizedUserName); + } + + /// + /// Sets the given normalized name for the specified . + /// + /// The user whose name should be set. + /// The normalized name to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.NormalizedUserName = normalizedName; + return TaskCache.CompletedTask; + } + + /// + /// Creates the specified in the user store. + /// + /// The user to create. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the creation operation. + public abstract Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Updates the specified in the user store. + /// + /// The user to update. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public abstract Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Deletes the specified from the user store. + /// + /// The user to delete. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public abstract Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Finds and returns a user, if any, who has the specified . + /// + /// The user ID to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public abstract Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Converts the provided to a strongly typed key object. + /// + /// The id to convert. + /// An instance of representing the provided . + public virtual TKey ConvertIdFromString(string id) + { + if (id == null) + { + return default(TKey); + } + return (TKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id); + } + + /// + /// Converts the provided to its string representation. + /// + /// The id to convert. + /// An representation of the provided . + public virtual string ConvertIdToString(TKey id) + { + if (object.Equals(id, default(TKey))) + { + return null; + } + return id.ToString(); + } + + /// + /// Finds and returns a user, if any, who has the specified normalized user name. + /// + /// The normalized user name to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public abstract Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// A navigation property for the users the store contains. + /// + public abstract IQueryable Users + { + get; + } + + /// + /// Sets the password hash for a user. + /// + /// The user to set the password hash for. + /// The password hash to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.PasswordHash = passwordHash; + return TaskCache.CompletedTask; + } + + /// + /// Gets the password hash for a user. + /// + /// The user to retrieve the password hash for. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the password hash for the user. + public virtual Task GetPasswordHashAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.PasswordHash); + } + + /// + /// Returns a flag indicating if the specified user has a password. + /// + /// The user to retrieve the password hash for. + /// The used to propagate notifications that the operation should be canceled. + /// A containing a flag indicating if the specified user has a password. If the + /// user has a password the returned value with be true, otherwise it will be false. + public virtual Task HasPasswordAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(user.PasswordHash != null); + } + + /// + /// Return a role with the normalized name if it exists. + /// + /// The normalized role name. + /// The used to propagate notifications that the operation should be canceled. + /// The role if it exists. + public abstract Task FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken); + + /// + /// Return a user role for the userId and roleId if it exists. + /// + /// The user's id. + /// The role's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user role if it exists. + public abstract Task FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken); + + /// + /// Return a user with the matching userId if it exists. + /// + /// The user's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user if it exists. + public abstract Task FindUserAsync(TKey userId, CancellationToken cancellationToken); + + /// + /// Return a user login with the matching userId, provider, providerKey if it exists. + /// + /// The user's id. + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + public abstract Task FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken); + + /// + /// Return a user login with provider, providerKey if it exists. + /// + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + public abstract Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken); + + /// + /// Adds the given to the specified . + /// + /// The user to add the role to. + /// The role to add. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public abstract Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Removes the given from the specified . + /// + /// The user to remove the role from. + /// The role to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public abstract Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Retrieves the roles the specified is a member of. + /// + /// The user whose roles should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the roles the user is a member of. + public abstract Task> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Returns a flag indicating if the specified user is a member of the give . + /// + /// The user whose role membership should be checked. + /// The role to check membership of + /// The used to propagate notifications that the operation should be canceled. + /// A containing a flag indicating if the specified user is a member of the given group. If the + /// user is a member of the group the returned value with be true, otherwise it will be false. + public abstract Task IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// True if this class has been disposed. + /// + protected bool IsDisposed => _disposed; + + /// + /// Throws if this class has been disposed. + /// + protected void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + /// Dispose the store + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose the store + /// + /// True when called from Dispose + protected virtual void Dispose(bool disposing) + { + _disposed = true; + } + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The user whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a user. + public abstract Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Adds the given to the specified . + /// + /// The user to add the claim to. + /// The claim to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public abstract Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Replaces the on the specified , with the . + /// + /// The user to replace the claim on. + /// The claim replace. + /// The new claim replacing the . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public abstract Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Removes the given from the specified . + /// + /// The user to remove the claims from. + /// The claim to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public abstract Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Adds the given to the specified . + /// + /// The user to add the login to. + /// The login to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public abstract Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Removes the given from the specified . + /// + /// The user to remove the login from. + /// The login to remove from the user. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public abstract Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Retrieves the associated logins for the specified . + /// + /// The user whose associated logins to retrieve. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing a list of for the specified , if any. + /// + public abstract Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Retrieves the user associated with the specified login provider and login provider key.. + /// + /// The login provider who provided the . + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. + /// + public async virtual Task FindByLoginAsync(string loginProvider, string providerKey, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var userLogin = await FindUserLoginAsync(loginProvider, providerKey, cancellationToken); + if (userLogin != null) + { + return await FindUserAsync(userLogin.UserId, cancellationToken); + } + return null; + } + + /// + /// Gets a flag indicating whether the email address for the specified has been verified, true if the email address is verified otherwise + /// false. + /// + /// The user whose email confirmation status should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous operation, a flag indicating whether the email address for the specified + /// has been confirmed or not. + /// + public virtual Task GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.EmailConfirmed); + } + + /// + /// Sets the flag indicating whether the specified 's email address has been confirmed or not. + /// + /// The user whose email confirmation status should be set. + /// A flag indicating if the email address has been confirmed, true if the address is confirmed otherwise false. + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public virtual Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.EmailConfirmed = confirmed; + return TaskCache.CompletedTask; + } + + /// + /// Sets the address for a . + /// + /// The user whose email should be set. + /// The email to set. + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public virtual Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.Email = email; + return TaskCache.CompletedTask; + } + + /// + /// Gets the email address for the specified . + /// + /// The user whose email should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// The task object containing the results of the asynchronous operation, the email address for the specified . + public virtual Task GetEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.Email); + } + + /// + /// Returns the normalized email for the specified . + /// + /// The user whose email address to retrieve. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous lookup operation, the normalized email address if any associated with the specified user. + /// + public virtual Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.NormalizedEmail); + } + + /// + /// Sets the normalized email for the specified . + /// + /// The user whose email address to set. + /// The normalized email to set for the specified . + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public virtual Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.NormalizedEmail = normalizedEmail; + return TaskCache.CompletedTask; + } + + /// + /// Gets the user, if any, associated with the specified, normalized email address. + /// + /// The normalized email address to return the user for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. + /// + public abstract Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Gets the last a user's last lockout expired, if any. + /// Any time in the past should be indicates a user is not locked out. + /// + /// The user whose lockout date should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// A that represents the result of the asynchronous query, a containing the last time + /// a user's lockout expired, if any. + /// + public virtual Task GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.LockoutEnd); + } + + /// + /// Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user. + /// + /// The user whose lockout date should be set. + /// The after which the 's lockout should end. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.LockoutEnd = lockoutEnd; + return TaskCache.CompletedTask; + } + + /// + /// Records that a failed access has occurred, incrementing the failed access count. + /// + /// The user whose cancellation count should be incremented. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the incremented failed access count. + public virtual Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.AccessFailedCount++; + return Task.FromResult(user.AccessFailedCount); + } + + /// + /// Resets a user's failed access count. + /// + /// The user whose failed access count should be reset. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + /// This is typically called after the account is successfully accessed. + public virtual Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.AccessFailedCount = 0; + return TaskCache.CompletedTask; + } + + /// + /// Retrieves the current failed access count for the specified .. + /// + /// The user whose failed access count should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the failed access count. + public virtual Task GetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.AccessFailedCount); + } + + /// + /// Retrieves a flag indicating whether user lockout can enabled for the specified user. + /// + /// The user whose ability to be locked out should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, true if a user can be locked out, otherwise false. + /// + public virtual Task GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.LockoutEnabled); + } + + /// + /// Set the flag indicating if the specified can be locked out.. + /// + /// The user whose ability to be locked out should be set. + /// A flag indicating if lock out can be enabled for the specified . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.LockoutEnabled = enabled; + return TaskCache.CompletedTask; + } + + /// + /// Sets the telephone number for the specified . + /// + /// The user whose telephone number should be set. + /// The telephone number to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.PhoneNumber = phoneNumber; + return TaskCache.CompletedTask; + } + + /// + /// Gets the telephone number, if any, for the specified . + /// + /// The user whose telephone number should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the user's telephone number, if any. + public virtual Task GetPhoneNumberAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.PhoneNumber); + } + + /// + /// Gets a flag indicating whether the specified 's telephone number has been confirmed. + /// + /// The user to return a flag for, indicating whether their telephone number is confirmed. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, returning true if the specified has a confirmed + /// telephone number otherwise false. + /// + public virtual Task GetPhoneNumberConfirmedAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.PhoneNumberConfirmed); + } + + /// + /// Sets a flag indicating if the specified 's phone number has been confirmed.. + /// + /// The user whose telephone number confirmation status should be set. + /// A flag indicating whether the user's telephone number has been confirmed. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.PhoneNumberConfirmed = confirmed; + return TaskCache.CompletedTask; + } + + /// + /// Sets the provided security for the specified . + /// + /// The user whose security stamp should be set. + /// The security stamp to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (stamp == null) + { + throw new ArgumentNullException(nameof(stamp)); + } + user.SecurityStamp = stamp; + return TaskCache.CompletedTask; + } + + /// + /// Get the security stamp for the specified . + /// + /// The user whose security stamp should be set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the security stamp for the specified . + public virtual Task GetSecurityStampAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.SecurityStamp); + } + + /// + /// Sets a flag indicating whether the specified has two factor authentication enabled or not, + /// as an asynchronous operation. + /// + /// The user whose two factor authentication enabled status should be set. + /// A flag indicating whether the specified has two factor authentication enabled. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.TwoFactorEnabled = enabled; + return TaskCache.CompletedTask; + } + + /// + /// Returns a flag indicating whether the specified has two factor authentication enabled or not, + /// as an asynchronous operation. + /// + /// The user whose two factor authentication enabled status should be set. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing a flag indicating whether the specified + /// has two factor authentication enabled or not. + /// + public virtual Task GetTwoFactorEnabledAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.TwoFactorEnabled); + } + + /// + /// Retrieves all users with the specified claim. + /// + /// The claim whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that contain the specified claim. + /// + public abstract Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Retrieves all users in the specified role. + /// + /// The role whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that are in the specified role. + /// + public abstract Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Find a user token if it exists. + /// + /// The token owner. + /// The login provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The user token if it exists. + public abstract Task FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken); + + /// + /// Add a new user token. + /// + /// The token to be added. + /// + public abstract Task AddUserTokenAsync(TUserToken token); + + /// + /// Remove a new user token. + /// + /// The token to be removed. + /// + public abstract Task RemoveUserTokenAsync(TUserToken token); + + /// + /// Sets the token value for a particular user. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The value of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual async Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var token = await FindTokenAsync(user, loginProvider, name, cancellationToken); + if (token == null) + { + await AddUserTokenAsync(CreateUserToken(user, loginProvider, name, value)); + } + else + { + token.Value = value; + } + } + + /// + /// Deletes a token for a user. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual async Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var entry = await FindTokenAsync(user, loginProvider, name, cancellationToken); + if (entry != null) + { + await RemoveUserTokenAsync(entry); + } + } + + /// + /// Returns the token value. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual async Task GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var entry = await FindTokenAsync(user, loginProvider, name, cancellationToken); + return entry?.Value; + } + + private const string InternalLoginProvider = "[AspNetUserStore]"; + private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; + private const string RecoveryCodeTokenName = "RecoveryCodes"; + + /// + /// Sets the authenticator key for the specified . + /// + /// The user whose authenticator key should be set. + /// The authenticator key to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken) + { + return SetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, key, cancellationToken); + } + + /// + /// Get the authenticator key for the specified . + /// + /// The user whose security stamp should be set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the security stamp for the specified . + public virtual Task GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken) + { + return GetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, cancellationToken); + } + + /// + /// Updates the recovery codes for the user while invalidating any previous recovery codes. + /// + /// The user to store new recovery codes for. + /// The new recovery codes for the user. + /// The used to propagate notifications that the operation should be canceled. + /// The new recovery codes for the user. + public virtual Task ReplaceCodesAsync(TUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) + { + var mergedCodes = string.Join(";", recoveryCodes); + return SetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, mergedCodes, cancellationToken); + } + + /// + /// Returns whether a recovery code is valid for a user. Note: recovery codes are only valid + /// once, and will be invalid after use. + /// + /// The user who owns the recovery code. + /// The recovery code to use. + /// The used to propagate notifications that the operation should be canceled. + /// True if the recovery code was found for the user. + public virtual async Task RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (code == null) + { + throw new ArgumentNullException(nameof(code)); + } + + var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? ""; + var splitCodes = mergedCodes.Split(';'); + if (splitCodes.Contains(code)) + { + var updatedCodes = new List(splitCodes.Where(s => s != code)); + await ReplaceCodesAsync(user, updatedCodes, cancellationToken); + return true; + } + return false; + } + } +} diff --git a/src/Microsoft.Extensions.Identity.Stores/UserStoreBaseV2.cs b/src/Microsoft.Extensions.Identity.Stores/UserStoreBaseV2.cs new file mode 100644 index 0000000000..43531b04f3 --- /dev/null +++ b/src/Microsoft.Extensions.Identity.Stores/UserStoreBaseV2.cs @@ -0,0 +1,152 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Identity +{ + /// + /// Represents a new instance of a persistence store for the specified user and role types. + /// + /// The type representing a user. + /// The type representing a role. + /// The type of the primary key for a role. + /// The type representing a claim. + /// The type representing a user role. + /// The type representing a user external login. + /// The type representing a user token. + /// The type representing a role claim. + public abstract class UserStoreBaseV2 : + UserStoreBaseV1, + IUserActivityStore + where TUser : IdentityUser + where TRole : IdentityRole + where TKey : IEquatable + where TUserClaim : IdentityUserClaim, new() + where TUserRole : IdentityUserRole, new() + where TUserLogin : IdentityUserLogin, new() + where TUserToken : IdentityUserToken, new() + where TRoleClaim : IdentityRoleClaim, new() + { + /// + /// Creates a new instance. + /// + /// The used to describe store errors. + public UserStoreBaseV2(IdentityErrorDescriber describer) : base(describer) { } + + /// + /// Sets the last password change date for the specified . + /// + /// The user. + /// The last password change date. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetLastPasswordChangeDateAsync(TUser user, DateTimeOffset? changeDate, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.LastPasswordChangeDate = changeDate; + return TaskCache.CompletedTask; + } + + /// + /// Gets the last password change date for the specified . + /// + /// The user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, returning the password hash for the specified . + public virtual Task GetLastPasswordChangeDateAsync(TUser user, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.LastPasswordChangeDate); + } + + /// + /// Sets the creation date for the specified . + /// + /// The user. + /// The date the user was created. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetCreateDateAsync(TUser user, DateTimeOffset? creationDate, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.CreateDate = creationDate; + return TaskCache.CompletedTask; + } + + /// + /// Gets the creation date for the specified . + /// + /// The user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, returning the password hash for the specified . + public virtual Task GetCreateDateAsync(TUser user, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.CreateDate); + } + + /// + /// Sets the last signin date for the specified . + /// + /// The user. + /// The date the user last signed in. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetLastSignInDateAsync(TUser user, DateTimeOffset? lastSignIn, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.LastSignInDate = lastSignIn; + return TaskCache.CompletedTask; + } + + /// + /// Gets the last signin date for the specified . + /// + /// The user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, returning the password hash for the specified . + public virtual Task GetLastSignInDateAsync(TUser user, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return Task.FromResult(user.LastSignInDate); + } + + } +} diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryContext.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryContext.cs index e1d2e349a0..1261747bb3 100644 --- a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryContext.cs +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryContext.cs @@ -9,16 +9,18 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test public class InMemoryContext : InMemoryContext { - public InMemoryContext(DbContextOptions options) : base(options) - { } + public InMemoryContext(DbContextOptions options) : base(options) { } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseInMemoryDatabase("Scratch"); } public class InMemoryContext : InMemoryContext where TUser : IdentityUser { - public InMemoryContext(DbContextOptions options) : base(options) - { } + public InMemoryContext(DbContextOptions options) : base(options) { } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseInMemoryDatabase("Scratch"); } public class InMemoryContext : IdentityDbContext @@ -26,13 +28,10 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test where TRole : IdentityRole where TKey : IEquatable { - public InMemoryContext(DbContextOptions options) : base(options) - { } + public InMemoryContext(DbContextOptions options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseInMemoryDatabase("Scratch"); - } + => optionsBuilder.UseInMemoryDatabase("Scratch"); } public abstract class InMemoryContext : IdentityDbContext @@ -45,12 +44,57 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test where TRoleClaim : IdentityRoleClaim where TUserToken : IdentityUserToken { - public InMemoryContext(DbContextOptions options) : base(options) - { } + public InMemoryContext(DbContextOptions options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseInMemoryDatabase("Scratch"); - } + => optionsBuilder.UseInMemoryDatabase("Scratch"); + } + + public class InMemoryContextV1 : + InMemoryContextV1 + { + public InMemoryContextV1(DbContextOptions options) : base(options) { } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseInMemoryDatabase("ScratchV1"); + } + + public class InMemoryContextV1 : + InMemoryContext + where TUser : IdentityUser + { + public InMemoryContextV1(DbContextOptions options) : base(options) { } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseInMemoryDatabase("ScratchV1"); + } + + public class InMemoryContextV1 : IdentityDbContext + where TUser : IdentityUser + where TRole : IdentityRole + where TKey : IEquatable + { + public InMemoryContextV1(DbContextOptions options) : base(options) { } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseInMemoryDatabase("ScratchV1"); + } + + public abstract class InMemoryContextV1 : IdentityDbContext + where TUser : IdentityUser + where TRole : IdentityRole + where TKey : IEquatable + where TUserClaim : IdentityUserClaim + where TUserRole : IdentityUserRole + where TUserLogin : IdentityUserLogin + where TRoleClaim : IdentityRoleClaim + where TUserToken : IdentityUserToken + { + public InMemoryContextV1(DbContextOptions options) : base(options) { } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseInMemoryDatabase("ScratchV1"); + + // TODO: remove once we fix the base + protected override void OnModelCreating(ModelBuilder builder) + => base.OnModelCreatingV1(builder); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryEFUserStoreTest.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryEFUserStoreTest.cs index 5a4d0fdc20..f8ddd77528 100644 --- a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryEFUserStoreTest.cs +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryEFUserStoreTest.cs @@ -3,27 +3,25 @@ using System; using System.Linq.Expressions; +using System.Threading.Tasks; using Microsoft.AspNetCore.Identity.Test; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Xunit; namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test { public class InMemoryEFUserStoreTest : IdentitySpecificationTestBase { protected override object CreateTestContext() - { - return new InMemoryContext(new DbContextOptionsBuilder().Options); - } + => new InMemoryContext(new DbContextOptionsBuilder().Options); protected override void AddUserStore(IServiceCollection services, object context = null) - { - services.AddSingleton>(new UserStore((InMemoryContext)context)); - } + => services.AddSingleton>(new UserStore, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim>((InMemoryContext)context, new IdentityErrorDescriber())); protected override void AddRoleStore(IServiceCollection services, object context = null) { - var store = new RoleStore((InMemoryContext)context); + var store = new RoleStore, IdentityRoleClaim>((InMemoryContext)context); services.AddSingleton>(store); } diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryEFUserStoreV1Test.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryEFUserStoreV1Test.cs new file mode 100644 index 0000000000..c1c8f5cf25 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryEFUserStoreV1Test.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Test; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test +{ + public class InMemoryEFUserStoreV1Test : IdentitySpecificationTestBase + { + protected override object CreateTestContext() + => new InMemoryContextV1(new DbContextOptionsBuilder().Options); + + protected override void AddUserStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new UserStoreV1, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim>((InMemoryContextV1)context, new IdentityErrorDescriber())); + } + + protected override void AddRoleStore(IServiceCollection services, object context = null) + { + var store = new RoleStoreV1, IdentityRoleClaim>((InMemoryContextV1)context); + services.AddSingleton>(store); + } + + protected override IdentityUser CreateTestUser(string namePrefix = "", string email = "", string phoneNumber = "", + bool lockoutEnabled = false, DateTimeOffset? lockoutEnd = default(DateTimeOffset?), bool useNamePrefixAsUserName = false) + { + return new IdentityUser + { + UserName = useNamePrefixAsUserName ? namePrefix : string.Format("{0}{1}", namePrefix, Guid.NewGuid()), + Email = email, + PhoneNumber = phoneNumber, + LockoutEnabled = lockoutEnabled, + LockoutEnd = lockoutEnd + }; + } + + protected override IdentityRole CreateTestRole(string roleNamePrefix = "", bool useRoleNamePrefixAsRoleName = false) + { + var roleName = useRoleNamePrefixAsRoleName ? roleNamePrefix : string.Format("{0}{1}", roleNamePrefix, Guid.NewGuid()); + return new IdentityRole(roleName); + } + + protected override void SetUserPasswordHash(IdentityUser user, string hashedPassword) + { + user.PasswordHash = hashedPassword; + } + + protected override Expression> UserNameEqualsPredicate(string userName) => u => u.UserName == userName; + + protected override Expression> RoleNameEqualsPredicate(string roleName) => r => r.Name == roleName; + + protected override Expression> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName); + + protected override Expression> RoleNameStartsWithPredicate(string roleName) => r => r.Name.StartsWith(roleName); + } +} diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryStoreWithGenericsTest.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryStoreWithGenericsTest.cs index 679221e048..a7e4b01afd 100644 --- a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryStoreWithGenericsTest.cs +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/InMemoryStoreWithGenericsTest.cs @@ -200,12 +200,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test { public string LoginContext { get; set; } - public UserStoreWithGenerics(InMemoryContextWithGenerics context, string loginContext) : base(context) + public UserStoreWithGenerics(InMemoryContextWithGenerics context, string loginContext) : base(context, new IdentityErrorDescriber()) { LoginContext = loginContext; } - protected override IdentityUserRoleWithDate CreateUserRole(IdentityUserWithGenerics user, MyIdentityRole role) + public override IdentityUserRoleWithDate CreateUserRole(IdentityUserWithGenerics user, MyIdentityRole role) { return new IdentityUserRoleWithDate() { @@ -215,12 +215,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test }; } - protected override IdentityUserClaimWithIssuer CreateUserClaim(IdentityUserWithGenerics user, Claim claim) + public override IdentityUserClaimWithIssuer CreateUserClaim(IdentityUserWithGenerics user, Claim claim) { return new IdentityUserClaimWithIssuer { UserId = user.Id, ClaimType = claim.Type, ClaimValue = claim.Value, Issuer = claim.Issuer }; } - protected override IdentityUserLoginWithContext CreateUserLogin(IdentityUserWithGenerics user, UserLoginInfo login) + public override IdentityUserLoginWithContext CreateUserLogin(IdentityUserWithGenerics user, UserLoginInfo login) { return new IdentityUserLoginWithContext { @@ -232,7 +232,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test }; } - protected override IdentityUserTokenWithStuff CreateUserToken(IdentityUserWithGenerics user, string loginProvider, string name, string value) + public override IdentityUserTokenWithStuff CreateUserToken(IdentityUserWithGenerics user, string loginProvider, string name, string value) { return new IdentityUserTokenWithStuff { diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/RoleStoreTest.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/RoleStoreTest.cs index a356787781..84d82a14fd 100644 --- a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/RoleStoreTest.cs +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/RoleStoreTest.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test var services = TestIdentityFactory.CreateTestServices(); services.AddEntityFrameworkInMemoryDatabase(); services.AddSingleton(new InMemoryContext(new DbContextOptionsBuilder().Options)); - services.AddTransient, RoleStore>(); + services.AddTransient, RoleStore, IdentityRoleClaim>>(); services.AddSingleton>(); var provider = services.BuildServiceProvider(); var manager = provider.GetRequiredService>(); @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test [Fact] public async Task RoleStoreMethodsThrowWhenDisposedTest() { - var store = new RoleStore(new InMemoryContext(new DbContextOptionsBuilder().Options)); + var store = new RoleStore, IdentityRoleClaim>(new InMemoryContext(new DbContextOptionsBuilder().Options)); store.Dispose(); await Assert.ThrowsAsync(async () => await store.FindByIdAsync(null)); await Assert.ThrowsAsync(async () => await store.FindByNameAsync(null)); @@ -53,8 +53,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test [Fact] public async Task RoleStorePublicNullCheckTest() { - Assert.Throws("context", () => new RoleStore(null)); - var store = new RoleStore(new InMemoryContext(new DbContextOptionsBuilder().Options)); + Assert.Throws("context", () => new RoleStore, IdentityRoleClaim>(null)); + var store = new RoleStore, IdentityRoleClaim>(new InMemoryContext(new DbContextOptionsBuilder().Options)); await Assert.ThrowsAsync("role", async () => await store.GetRoleIdAsync(null)); await Assert.ThrowsAsync("role", async () => await store.GetRoleNameAsync(null)); await Assert.ThrowsAsync("role", async () => await store.SetRoleNameAsync(null, null)); diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/TestIdentityFactory.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/TestIdentityFactory.cs index c8ba6c922f..203de9b14c 100644 --- a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/TestIdentityFactory.cs +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test/TestIdentityFactory.cs @@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test public static RoleManager CreateRoleManager(InMemoryContext context) { var services = CreateTestServices(); - services.AddSingleton>(new RoleStore(context)); + services.AddSingleton>(new RoleStore, IdentityRoleClaim>(context)); return services.BuildServiceProvider().GetRequiredService>(); } diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/SqlStoreTestBase.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/SqlStoreTestBase.cs index 5a78f6c50d..3f87134a8e 100644 --- a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/SqlStoreTestBase.cs +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/SqlStoreTestBase.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test @@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test where TRole : IdentityRole, new() where TKey : IEquatable { - private readonly ScratchDatabaseFixture _fixture; + protected readonly ScratchDatabaseFixture _fixture; protected SqlStoreTestBase(ScratchDatabaseFixture fixture) { @@ -64,7 +65,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test protected override Expression> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName); - public TestDbContext CreateContext() + public virtual TestDbContext CreateContext() { var db = DbUtil.Create(_fixture.ConnectionString); db.Database.EnsureCreated(); @@ -78,12 +79,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test protected override void AddUserStore(IServiceCollection services, object context = null) { - services.AddSingleton>(new UserStore((TestDbContext)context)); + services.AddSingleton>(new UserStore, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim>((TestDbContext)context, new IdentityErrorDescriber())); } protected override void AddRoleStore(IServiceCollection services, object context = null) { - services.AddSingleton>(new RoleStore((TestDbContext)context)); + services.AddSingleton>(new RoleStore, IdentityRoleClaim>((TestDbContext)context)); } protected override void SetUserPasswordHash(TUser user, string hashedPassword) @@ -95,26 +96,36 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test [FrameworkSkipCondition(RuntimeFrameworks.Mono)] [OSSkipCondition(OperatingSystems.Linux)] [OSSkipCondition(OperatingSystems.MacOSX)] - public void EnsureDefaultSchema() + public virtual void EnsureDefaultSchema() { - VerifyDefaultSchema(CreateContext()); + var db = DbUtil.Create(_fixture.ConnectionString); + var services = new ServiceCollection().AddSingleton(db); + services.AddIdentity().AddEntityFrameworkStoresLatest(); + var sp = services.BuildServiceProvider(); + var version = sp.GetRequiredService>().Value.Version; + Assert.Equal(IdentityStoreOptions.Version_Latest, version); + db.Version = version; + db.Database.EnsureDeleted(); + db.Database.EnsureCreated(); + VerifyDefaultSchema(db); } - internal static void VerifyDefaultSchema(TestDbContext dbContext) + protected virtual void VerifyDefaultSchema(TestDbContext dbContext) { var sqlConn = dbContext.Database.GetDbConnection(); using (var db = new SqlConnection(sqlConn.ConnectionString)) { db.Open(); - Assert.True(VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp", + VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp", "EmailConfirmed", "PhoneNumber", "PhoneNumberConfirmed", "TwoFactorEnabled", "LockoutEnabled", - "LockoutEnd", "AccessFailedCount", "ConcurrencyStamp", "NormalizedUserName", "NormalizedEmail")); - Assert.True(VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp")); - Assert.True(VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId")); - Assert.True(VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue")); - Assert.True(VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName")); - Assert.True(VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value")); + "LockoutEnd", "AccessFailedCount", "ConcurrencyStamp", "NormalizedUserName", "NormalizedEmail", + "CreateDate", "LastSignInDate", "LastPasswordChangeDate"); + VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp"); + VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId"); + VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue"); + VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName"); + VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value"); VerifyIndex(db, "AspNetRoles", "RoleNameIndex", isUnique: true); VerifyIndex(db, "AspNetUsers", "UserNameIndex", isUnique: true); @@ -123,7 +134,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test } } - internal static bool VerifyColumns(SqlConnection conn, string table, params string[] columns) + internal static void VerifyColumns(SqlConnection conn, string table, params string[] columns) { var count = 0; using ( @@ -136,12 +147,10 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test while (reader.Read()) { count++; - if (!columns.Contains(reader.GetString(0))) - { - return false; - } + Assert.True(columns.Contains(reader.GetString(0)), + "Unexpected column: " + reader.GetString(0)); } - return count == columns.Length; + Assert.Equal(count, columns.Length); } } } @@ -363,6 +372,67 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test Assert.Equal(1, (await manager.GetLoginsAsync(userByEmail)).Count); Assert.Equal(2, (await manager.GetRolesAsync(userByEmail)).Count); } + } + public abstract class SqlStoreTestBaseV1 : SqlStoreTestBase, IClassFixture + where TUser : IdentityUser, new() + where TRole : IdentityRole, new() + where TKey : IEquatable + { + protected SqlStoreTestBaseV1(ScratchDatabaseFixture fixture) : base(fixture) { } + + public override TestDbContext CreateContext() + { + var db = base.CreateContext(); + db.Version = IdentityStoreOptions.Version1_0; + return db; + } + + protected override void AddUserStore(IServiceCollection services, object context = null) + => services.AddSingleton>(new UserStoreV1, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim>((TestDbContext)context, new IdentityErrorDescriber())); + + protected override void AddRoleStore(IServiceCollection services, object context = null) + => services.AddSingleton>(new RoleStoreV1, IdentityRoleClaim>((TestDbContext)context)); + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public override void EnsureDefaultSchema() + { + var db = DbUtil.Create(_fixture.ConnectionString); + var services = new ServiceCollection().AddSingleton(db); + services.AddIdentity().AddEntityFrameworkStoresV1(); + var sp = services.BuildServiceProvider(); + var version = sp.GetRequiredService>().Value.Version; + Assert.Equal(IdentityStoreOptions.Version1_0, version); + db.Version = version; + db.Database.EnsureDeleted(); + db.Database.EnsureCreated(); + VerifyDefaultSchema(db); + } + + protected override void VerifyDefaultSchema(TestDbContext dbContext) + { + var sqlConn = dbContext.Database.GetDbConnection(); + + using (var db = new SqlConnection(sqlConn.ConnectionString)) + { + db.Open(); + VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp", + "EmailConfirmed", "PhoneNumber", "PhoneNumberConfirmed", "TwoFactorEnabled", "LockoutEnabled", + "LockoutEnd", "AccessFailedCount", "ConcurrencyStamp", "NormalizedUserName", "NormalizedEmail"); + VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp"); + VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId"); + VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue"); + VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName"); + VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value"); + + VerifyIndex(db, "AspNetRoles", "RoleNameIndex", isUnique: true); + VerifyIndex(db, "AspNetUsers", "UserNameIndex", isUnique: true); + VerifyIndex(db, "AspNetUsers", "EmailIndex"); + db.Close(); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreGuidKeyTest.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreGuidTest.cs similarity index 50% rename from test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreGuidKeyTest.cs rename to test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreGuidTest.cs index 9887736d89..892b2ab669 100644 --- a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreGuidKeyTest.cs +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreGuidTest.cs @@ -25,21 +25,20 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test } } - public class UserStoreGuidTest : SqlStoreTestBase + public class UserStoreGuidV1Test : SqlStoreTestBaseV1 { - public UserStoreGuidTest(ScratchDatabaseFixture fixture) + public UserStoreGuidV1Test(ScratchDatabaseFixture fixture) : base(fixture) + { } + + public class ApplicationUserStore : UserStoreV1, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> { + public ApplicationUserStore(TestDbContext context) : base(context, new IdentityErrorDescriber()) { } } - public class ApplicationUserStore : UserStore + public class ApplicationRoleStore : RoleStoreV1, IdentityRoleClaim> { - public ApplicationUserStore(TestDbContext context) : base(context) { } - } - - public class ApplicationRoleStore : RoleStore - { - public ApplicationRoleStore(TestDbContext context) : base(context) { } + public ApplicationRoleStore(TestDbContext context) : base(context, new IdentityErrorDescriber()) { } } protected override void AddUserStore(IServiceCollection services, object context = null) @@ -60,4 +59,40 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test var builder = services.AddIdentity().AddEntityFrameworkStores(); } } + + public class UserStoreGuidTest : SqlStoreTestBase + { + public UserStoreGuidTest(ScratchDatabaseFixture fixture) + : base(fixture) + { } + + public class ApplicationUserStore : UserStore, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> + { + public ApplicationUserStore(TestDbContext context) : base(context, new IdentityErrorDescriber()) { } + } + + public class ApplicationRoleStore : RoleStore, IdentityRoleClaim> + { + public ApplicationRoleStore(TestDbContext context) : base(context, new IdentityErrorDescriber()) { } + } + + protected override void AddUserStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new ApplicationUserStore((TestDbContext)context)); + } + + protected override void AddRoleStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new ApplicationRoleStore((TestDbContext)context)); + } + + [Fact] + public void AddEntityFrameworkStoresCanInferKey() + { + var services = new ServiceCollection(); + // This used to throw + var builder = services.AddIdentity().AddEntityFrameworkStores(); + } + } + } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreIntKeyTest.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreIntKeyTest.cs deleted file mode 100644 index 265a467b93..0000000000 --- a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreIntKeyTest.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test -{ - public class IntUser : IdentityUser - { - public IntUser() - { - UserName = Guid.NewGuid().ToString(); - } - } - - public class IntRole : IdentityRole - { - public IntRole() - { - Name = Guid.NewGuid().ToString(); - } - } - - public class UserStoreIntTest : SqlStoreTestBase - { - public UserStoreIntTest(ScratchDatabaseFixture fixture) - : base(fixture) - { - } - - [Fact] - public void AddEntityFrameworkStoresCanInferKey() - { - var services = new ServiceCollection(); - // This used to throw - var builder = services.AddIdentity().AddEntityFrameworkStores(); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreIntTest.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreIntTest.cs new file mode 100644 index 0000000000..cb461a6920 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreIntTest.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test +{ + public class IntUser : IdentityUser + { + public IntUser() => UserName = Guid.NewGuid().ToString(); + } + + public class IntRole : IdentityRole + { + public IntRole() => Name = Guid.NewGuid().ToString(); + } + + public class UserStoreIntTest : SqlStoreTestBase + { + public UserStoreIntTest(ScratchDatabaseFixture fixture) : base(fixture) { } + + [Fact] + public void AddEntityFrameworkStoresCanInferKey() + { + // This used to throw + var builder = new ServiceCollection().AddIdentity().AddEntityFrameworkStores(); + } + } + + public class UserStoreIntV1Test : SqlStoreTestBaseV1 + { + public UserStoreIntV1Test(ScratchDatabaseFixture fixture) : base(fixture) { } + + [Fact] + public void AddEntityFrameworkStoresCanInferKey() + { + // This used to throw + var builder = new ServiceCollection().AddIdentity().AddEntityFrameworkStores(); + } + } + +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreTest.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreTest.cs index 34a824d62e..878d2f57f3 100644 --- a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreTest.cs +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreTest.cs @@ -14,6 +14,11 @@ using Xunit; namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test { + internal class UserStore : UserStore, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> + { + public UserStore(IdentityDbContext context, IdentityErrorDescriber describer = null) : base(context, describer ?? new IdentityErrorDescriber()) { } + } + public class UserStoreTest : IdentitySpecificationTestBase, IClassFixture { private readonly ScratchDatabaseFixture _fixture; @@ -26,12 +31,6 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test protected override bool ShouldSkipDbTests() => TestPlatformHelper.IsMono || !TestPlatformHelper.IsWindows; - public class ApplicationDbContext : IdentityDbContext - { - public ApplicationDbContext(DbContextOptions options) : base(options) - { } - } - [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono)] [OSSkipCondition(OperatingSystems.Linux)] @@ -64,21 +63,14 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test return CreateContext(); } - public ApplicationDbContext CreateAppContext() - { - var db = DbUtil.Create(_fixture.ConnectionString); - db.Database.EnsureCreated(); - return db; - } - protected override void AddUserStore(IServiceCollection services, object context = null) { - services.AddSingleton>(new UserStore((IdentityDbContext)context)); + services.AddSingleton>(new UserStore((IdentityDbContext)context)); } protected override void AddRoleStore(IServiceCollection services, object context = null) { - services.AddSingleton>(new RoleStore((IdentityDbContext)context)); + services.AddSingleton>(new RoleStore, IdentityRoleClaim>((IdentityDbContext)context)); } [Fact] @@ -193,6 +185,20 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user)); } + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task CreateUserSetsCreateDate() + { + var manager = CreateManager(); + var guid = Guid.NewGuid().ToString(); + var user = new IdentityUser { UserName = "New" + guid }; + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.NotNull(await manager.GetCreateDateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user)); + } + [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono)] [OSSkipCondition(OperatingSystems.Linux)] @@ -422,6 +428,4 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test protected override Expression> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName); } - - public class ApplicationUser : IdentityUser { } } diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreV1Test.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreV1Test.cs new file mode 100644 index 0000000000..33a97f944c --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreV1Test.cs @@ -0,0 +1,418 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Test; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.AspNetCore.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test +{ + internal class UserStoreV1 : UserStoreV1, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> + { + public UserStoreV1(IdentityDbContext context, IdentityErrorDescriber describer = null) : base(context, describer ?? new IdentityErrorDescriber()) { } + } + + public class UserStoreV1Test : IdentitySpecificationTestBase, IClassFixture + { + private readonly ScratchDatabaseFixture _fixture; + + public UserStoreV1Test(ScratchDatabaseFixture fixture) + { + _fixture = fixture; + } + + protected override bool ShouldSkipDbTests() + => TestPlatformHelper.IsMono || !TestPlatformHelper.IsWindows; + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public void CanCreateUserUsingEF() + { + using (var db = CreateContext()) + { + var guid = Guid.NewGuid().ToString(); + db.Users.Add(new IdentityUser { Id = guid, UserName = guid }); + db.SaveChanges(); + Assert.True(db.Users.Any(u => u.UserName == guid)); + Assert.NotNull(db.Users.FirstOrDefault(u => u.UserName == guid)); + } + } + + public IdentityDbContext CreateContext(bool delete = false) + { + var db = DbUtil.Create(_fixture.ConnectionString); + db.Version = IdentityStoreOptions.Version1_0; + if (delete) + { + db.Database.EnsureDeleted(); + } + db.Database.EnsureCreated(); + return db; + } + + protected override object CreateTestContext() + { + return CreateContext(); + } + + protected override void AddUserStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new UserStoreV1((IdentityDbContext)context)); + } + + protected override void AddRoleStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new RoleStoreV1, IdentityRoleClaim>((IdentityDbContext)context)); + } + + [Fact] + public async Task SqlUserStoreMethodsThrowWhenDisposedTest() + { + var store = new UserStore(new IdentityDbContext(new DbContextOptionsBuilder().Options)); + store.Dispose(); + await Assert.ThrowsAsync(async () => await store.AddClaimsAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.AddLoginAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.AddToRoleAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.GetClaimsAsync(null)); + await Assert.ThrowsAsync(async () => await store.GetLoginsAsync(null)); + await Assert.ThrowsAsync(async () => await store.GetRolesAsync(null)); + await Assert.ThrowsAsync(async () => await store.IsInRoleAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.RemoveClaimsAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.RemoveLoginAsync(null, null, null)); + 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)); + await Assert.ThrowsAsync(async () => await store.CreateAsync(null)); + await Assert.ThrowsAsync(async () => await store.UpdateAsync(null)); + await Assert.ThrowsAsync(async () => await store.DeleteAsync(null)); + await Assert.ThrowsAsync( + async () => await store.SetEmailConfirmedAsync(null, true)); + await Assert.ThrowsAsync(async () => await store.GetEmailConfirmedAsync(null)); + await Assert.ThrowsAsync( + async () => await store.SetPhoneNumberConfirmedAsync(null, true)); + await Assert.ThrowsAsync( + async () => await store.GetPhoneNumberConfirmedAsync(null)); + } + + [Fact] + public async Task UserStorePublicNullCheckTest() + { + Assert.Throws("context", () => new UserStore(null)); + var store = new UserStore(new IdentityDbContext(new DbContextOptionsBuilder().Options)); + await Assert.ThrowsAsync("user", async () => await store.GetUserIdAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.GetUserNameAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.SetUserNameAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.CreateAsync(null)); + 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)); + await Assert.ThrowsAsync("user", async () => await store.GetRolesAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.AddLoginAsync(null, null)); + await + Assert.ThrowsAsync("user", async () => await store.RemoveLoginAsync(null, null, null)); + await Assert.ThrowsAsync("user", async () => await store.AddToRoleAsync(null, null)); + await + Assert.ThrowsAsync("user", + async () => await store.RemoveFromRoleAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.IsInRoleAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.GetPasswordHashAsync(null)); + await + Assert.ThrowsAsync("user", + async () => await store.SetPasswordHashAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.GetSecurityStampAsync(null)); + await Assert.ThrowsAsync("user", + async () => await store.SetSecurityStampAsync(null, null)); + await Assert.ThrowsAsync("login", async () => await store.AddLoginAsync(new IdentityUser("fake"), null)); + await Assert.ThrowsAsync("claims", + async () => await store.AddClaimsAsync(new IdentityUser("fake"), null)); + await Assert.ThrowsAsync("claims", + async () => await store.RemoveClaimsAsync(new IdentityUser("fake"), null)); + await Assert.ThrowsAsync("user", async () => await store.GetEmailConfirmedAsync(null)); + await Assert.ThrowsAsync("user", + async () => await store.SetEmailConfirmedAsync(null, true)); + await Assert.ThrowsAsync("user", async () => await store.GetEmailAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.SetEmailAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.GetPhoneNumberAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.SetPhoneNumberAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await store.GetPhoneNumberConfirmedAsync(null)); + await Assert.ThrowsAsync("user", + async () => await store.SetPhoneNumberConfirmedAsync(null, true)); + await Assert.ThrowsAsync("user", async () => await store.GetTwoFactorEnabledAsync(null)); + await Assert.ThrowsAsync("user", + async () => await store.SetTwoFactorEnabledAsync(null, true)); + await Assert.ThrowsAsync("user", async () => await store.GetAccessFailedCountAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.GetLockoutEnabledAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.SetLockoutEnabledAsync(null, false)); + await Assert.ThrowsAsync("user", async () => await store.GetLockoutEndDateAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.SetLockoutEndDateAsync(null, new DateTimeOffset())); + await Assert.ThrowsAsync("user", async () => await store.ResetAccessFailedCountAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.IncrementAccessFailedCountAsync(null)); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.AddToRoleAsync(new IdentityUser("fake"), null)); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.RemoveFromRoleAsync(new IdentityUser("fake"), null)); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.IsInRoleAsync(new IdentityUser("fake"), null)); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.AddToRoleAsync(new IdentityUser("fake"), "")); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.RemoveFromRoleAsync(new IdentityUser("fake"), "")); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.IsInRoleAsync(new IdentityUser("fake"), "")); + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task CanCreateUsingManager() + { + var manager = CreateManager(); + var guid = Guid.NewGuid().ToString(); + var user = new IdentityUser { UserName = "New" + guid }; + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user)); + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task TwoUsersSamePasswordDifferentHash() + { + var manager = CreateManager(); + var userA = new IdentityUser(Guid.NewGuid().ToString()); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(userA, "password")); + var userB = new IdentityUser(Guid.NewGuid().ToString()); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(userB, "password")); + + Assert.NotEqual(userA.PasswordHash, userB.PasswordHash); + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task AddUserToUnknownRoleFails() + { + var manager = CreateManager(); + var u = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(u)); + await Assert.ThrowsAsync( + async () => await manager.AddToRoleAsync(u, "bogus")); + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task ConcurrentUpdatesWillFail() + { + var user = CreateTestUser(); + using (var db = CreateContext()) + { + var manager = CreateManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateManager(db); + var manager2 = CreateManager(db2); + var user1 = await manager1.FindByIdAsync(user.Id); + var user2 = await manager2.FindByIdAsync(user.Id); + Assert.NotNull(user1); + Assert.NotNull(user2); + Assert.NotSame(user1, user2); + user1.UserName = Guid.NewGuid().ToString(); + user2.UserName = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user1)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(user2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task ConcurrentUpdatesWillFailWithDetachedUser() + { + var user = CreateTestUser(); + using (var db = CreateContext()) + { + var manager = CreateManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateManager(db); + var manager2 = CreateManager(db2); + var user2 = await manager2.FindByIdAsync(user.Id); + Assert.NotNull(user2); + Assert.NotSame(user, user2); + user.UserName = Guid.NewGuid().ToString(); + user2.UserName = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(user2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task DeleteAModifiedUserWillFail() + { + var user = CreateTestUser(); + using (var db = CreateContext()) + { + var manager = CreateManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateManager(db); + var manager2 = CreateManager(db2); + var user1 = await manager1.FindByIdAsync(user.Id); + var user2 = await manager2.FindByIdAsync(user.Id); + Assert.NotNull(user1); + Assert.NotNull(user2); + Assert.NotSame(user1, user2); + user1.UserName = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user1)); + IdentityResultAssert.IsFailure(await manager2.DeleteAsync(user2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task ConcurrentRoleUpdatesWillFail() + { + var role = new IdentityRole(Guid.NewGuid().ToString()); + using (var db = CreateContext()) + { + var manager = CreateRoleManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateRoleManager(db); + var manager2 = CreateRoleManager(db2); + var role1 = await manager1.FindByIdAsync(role.Id); + var role2 = await manager2.FindByIdAsync(role.Id); + Assert.NotNull(role1); + Assert.NotNull(role2); + Assert.NotSame(role1, role2); + role1.Name = Guid.NewGuid().ToString(); + role2.Name = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role1)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(role2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task ConcurrentRoleUpdatesWillFailWithDetachedRole() + { + var role = new IdentityRole(Guid.NewGuid().ToString()); + using (var db = CreateContext()) + { + var manager = CreateRoleManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateRoleManager(db); + var manager2 = CreateRoleManager(db2); + var role2 = await manager2.FindByIdAsync(role.Id); + Assert.NotNull(role); + Assert.NotNull(role2); + Assert.NotSame(role, role2); + role.Name = Guid.NewGuid().ToString(); + role2.Name = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(role2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task DeleteAModifiedRoleWillFail() + { + var role = new IdentityRole(Guid.NewGuid().ToString()); + using (var db = CreateContext()) + { + var manager = CreateRoleManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateRoleManager(db); + var manager2 = CreateRoleManager(db2); + var role1 = await manager1.FindByIdAsync(role.Id); + var role2 = await manager2.FindByIdAsync(role.Id); + Assert.NotNull(role1); + Assert.NotNull(role2); + Assert.NotSame(role1, role2); + role1.Name = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role1)); + IdentityResultAssert.IsFailure(await manager2.DeleteAsync(role2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + } + + protected override IdentityUser CreateTestUser(string namePrefix = "", string email = "", string phoneNumber = "", + bool lockoutEnabled = false, DateTimeOffset? lockoutEnd = default(DateTimeOffset?), bool useNamePrefixAsUserName = false) + { + return new IdentityUser + { + UserName = useNamePrefixAsUserName ? namePrefix : string.Format("{0}{1}", namePrefix, Guid.NewGuid()), + Email = email, + PhoneNumber = phoneNumber, + LockoutEnabled = lockoutEnabled, + LockoutEnd = lockoutEnd + }; + } + + protected override IdentityRole CreateTestRole(string roleNamePrefix = "", bool useRoleNamePrefixAsRoleName = false) + { + var roleName = useRoleNamePrefixAsRoleName ? roleNamePrefix : string.Format("{0}{1}", roleNamePrefix, Guid.NewGuid()); + return new IdentityRole(roleName); + } + + protected override void SetUserPasswordHash(IdentityUser user, string hashedPassword) + { + user.PasswordHash = hashedPassword; + } + + protected override Expression> UserNameEqualsPredicate(string userName) => u => u.UserName == userName; + + protected override Expression> RoleNameEqualsPredicate(string roleName) => r => r.Name == roleName; + + protected override Expression> RoleNameStartsWithPredicate(string roleName) => r => r.Name.StartsWith(roleName); + + protected override Expression> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName); + } +} diff --git a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreWithGenericsTest.cs b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreWithGenericsTest.cs index 89ba53590b..c9466abee7 100644 --- a/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreWithGenericsTest.cs +++ b/test/Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test/UserStoreWithGenericsTest.cs @@ -230,12 +230,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test { public string LoginContext { get; set; } - public UserStoreWithGenerics(ContextWithGenerics context, string loginContext) : base(context) + public UserStoreWithGenerics(ContextWithGenerics context, string loginContext) : base(context, new IdentityErrorDescriber()) { LoginContext = loginContext; } - protected override IdentityUserRoleWithDate CreateUserRole(IdentityUserWithGenerics user, MyIdentityRole role) + public override IdentityUserRoleWithDate CreateUserRole(IdentityUserWithGenerics user, MyIdentityRole role) { return new IdentityUserRoleWithDate() { @@ -245,12 +245,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test }; } - protected override IdentityUserClaimWithIssuer CreateUserClaim(IdentityUserWithGenerics user, Claim claim) + public override IdentityUserClaimWithIssuer CreateUserClaim(IdentityUserWithGenerics user, Claim claim) { return new IdentityUserClaimWithIssuer { UserId = user.Id, ClaimType = claim.Type, ClaimValue = claim.Value, Issuer = claim.Issuer }; } - protected override IdentityUserLoginWithContext CreateUserLogin(IdentityUserWithGenerics user, UserLoginInfo login) + public override IdentityUserLoginWithContext CreateUserLogin(IdentityUserWithGenerics user, UserLoginInfo login) { return new IdentityUserLoginWithContext { @@ -262,7 +262,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test }; } - protected override IdentityUserTokenWithStuff CreateUserToken(IdentityUserWithGenerics user, string loginProvider, string name, string value) + public override IdentityUserTokenWithStuff CreateUserToken(IdentityUserWithGenerics user, string loginProvider, string name, string value) { return new IdentityUserTokenWithStuff { @@ -283,7 +283,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test _loginContext = loginContext; } - protected override IdentityRoleClaimWithIssuer CreateRoleClaim(MyIdentityRole role, Claim claim) + public override IdentityRoleClaimWithIssuer CreateRoleClaim(MyIdentityRole role, Claim claim) { return new IdentityRoleClaimWithIssuer { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value, Issuer = claim.Issuer }; } diff --git a/test/Microsoft.AspNetCore.Identity.InMemory.Test/InMemoryStore.cs b/test/Microsoft.AspNetCore.Identity.InMemory.Test/InMemoryStore.cs index ea7424fbf3..83d4106c61 100644 --- a/test/Microsoft.AspNetCore.Identity.InMemory.Test/InMemoryStore.cs +++ b/test/Microsoft.AspNetCore.Identity.InMemory.Test/InMemoryStore.cs @@ -26,7 +26,8 @@ namespace Microsoft.AspNetCore.Identity.InMemory IRoleClaimStore, IUserAuthenticationTokenStore, IUserAuthenticatorKeyStore, - IUserTwoFactorRecoveryCodeStore + IUserTwoFactorRecoveryCodeStore, + IUserActivityStore where TRole : TestRole where TUser : TestUser { @@ -583,6 +584,39 @@ namespace Microsoft.AspNetCore.Identity.InMemory return false; } + public Task SetLastPasswordChangeDateAsync(TUser user, DateTimeOffset? changeDate, CancellationToken cancellationToken) + { + user.LastPasswordChangeDate = changeDate; + return Task.FromResult(0); + } + + public Task GetLastPasswordChangeDateAsync(TUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.LastPasswordChangeDate); + } + + public Task SetCreateDateAsync(TUser user, DateTimeOffset? creationDate, CancellationToken cancellationToken) + { + user.CreateDate = creationDate; + return Task.FromResult(0); + } + + public Task GetCreateDateAsync(TUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.CreateDate); + } + + public Task SetLastSignInDateAsync(TUser user, DateTimeOffset? lastSignIn, CancellationToken cancellationToken) + { + user.LastSignInDate = lastSignIn; + return Task.FromResult(0); + } + + public Task GetLastSignInDateAsync(TUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.LastSignInDate); + } + public IQueryable Roles { get { return _roles.Values.AsQueryable(); } diff --git a/test/Microsoft.AspNetCore.Identity.InMemory.Test/Microsoft.AspNetCore.Identity.InMemory.Test.csproj b/test/Microsoft.AspNetCore.Identity.InMemory.Test/Microsoft.AspNetCore.Identity.InMemory.Test.csproj index f48a04e335..cab1aad3ea 100644 --- a/test/Microsoft.AspNetCore.Identity.InMemory.Test/Microsoft.AspNetCore.Identity.InMemory.Test.csproj +++ b/test/Microsoft.AspNetCore.Identity.InMemory.Test/Microsoft.AspNetCore.Identity.InMemory.Test.csproj @@ -27,4 +27,7 @@ + + + diff --git a/test/Microsoft.AspNetCore.Identity.Test/SecurityStampValidatorTest.cs b/test/Microsoft.AspNetCore.Identity.Test/SecurityStampValidatorTest.cs index 8f56000d37..5c568b2be8 100644 --- a/test/Microsoft.AspNetCore.Identity.Test/SecurityStampValidatorTest.cs +++ b/test/Microsoft.AspNetCore.Identity.Test/SecurityStampValidatorTest.cs @@ -197,7 +197,7 @@ namespace Microsoft.AspNetCore.Identity.Test var signInManager = new Mock>(userManager.Object, contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock().Object); signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny())).Throws(new Exception("Shouldn't be called")); - signInManager.Setup(s => s.SignInAsync(user, false, null)).Throws(new Exception("Shouldn't be called")); + signInManager.Setup(s => s.SignInAsync(user, false, null, It.IsAny())).Throws(new Exception("Shouldn't be called")); var services = new ServiceCollection(); services.AddSingleton(options.Object); services.AddSingleton(signInManager.Object); diff --git a/test/Microsoft.AspNetCore.Identity.Test/SignInManagerTest.cs b/test/Microsoft.AspNetCore.Identity.Test/SignInManagerTest.cs index a5bf8a134f..3da9d31d78 100644 --- a/test/Microsoft.AspNetCore.Identity.Test/SignInManagerTest.cs +++ b/test/Microsoft.AspNetCore.Identity.Test/SignInManagerTest.cs @@ -202,6 +202,8 @@ namespace Microsoft.AspNetCore.Identity.Test manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); + manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable(); + manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); var context = new DefaultHttpContext(); var auth = MockAuth(context); @@ -226,6 +228,8 @@ namespace Microsoft.AspNetCore.Identity.Test manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); + manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable(); + manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); var context = new DefaultHttpContext(); var auth = MockAuth(context); @@ -252,6 +256,8 @@ namespace Microsoft.AspNetCore.Identity.Test manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); + manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable(); + manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); var context = new DefaultHttpContext(); var auth = MockAuth(context); @@ -375,6 +381,8 @@ namespace Microsoft.AspNetCore.Identity.Test manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, providerName ?? TokenOptions.DefaultAuthenticatorProvider, code)).ReturnsAsync(true).Verifiable(); manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); + manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable(); + manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); var context = new DefaultHttpContext(); var auth = MockAuth(context); @@ -419,6 +427,8 @@ namespace Microsoft.AspNetCore.Identity.Test var manager = SetupUserManager(user); manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable(); manager.Setup(m => m.RedeemTwoFactorRecoveryCodeAsync(user, bypassCode)).ReturnsAsync(IdentityResult.Success).Verifiable(); + manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable(); + manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); if (supportsLockout) { manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); @@ -456,11 +466,15 @@ namespace Microsoft.AspNetCore.Identity.Test } [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public async Task CanExternalSignIn(bool isPersistent, bool supportsLockout) + [InlineData(true, true, true)] + [InlineData(true, false, true)] + [InlineData(false, true, true)] + [InlineData(false, false, true)] + [InlineData(true, true, false)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + public async Task CanExternalSignIn(bool isPersistent, bool supportsLockout, bool supportsActivity) { // Setup var user = new TestUser { UserName = "Foo" }; @@ -473,6 +487,11 @@ namespace Microsoft.AspNetCore.Identity.Test manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); } manager.Setup(m => m.FindByLoginAsync(loginProvider, providerKey)).ReturnsAsync(user).Verifiable(); + if (supportsActivity) + { + manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable(); + manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); + } var context = new DefaultHttpContext(); var auth = MockAuth(context); @@ -518,8 +537,7 @@ namespace Microsoft.AspNetCore.Identity.Test { CallBase = true }; //signInManager.Setup(s => s.SignInAsync(user, It.Is(p => p.IsPersistent == isPersistent), //externalLogin? loginProvider : null)).Returns(Task.FromResult(0)).Verifiable(); - signInManager.Setup(s => s.SignInAsync(user, It.IsAny(), null)).Returns(Task.FromResult(0)).Verifiable(); - signInManager.Object.Context = context; + signInManager.Setup(s => s.SignInAsync(user, It.IsAny(), /*authmethod*/null, /*updateLastSignIn*/false)).Returns(Task.FromResult(0)).Verifiable(); // Act await signInManager.Object.RefreshSignInAsync(user); @@ -530,23 +548,39 @@ namespace Microsoft.AspNetCore.Identity.Test } [Theory] - [InlineData(true, true, true, true)] - [InlineData(true, true, false, true)] - [InlineData(true, false, true, true)] - [InlineData(true, false, false, true)] - [InlineData(false, true, true, true)] - [InlineData(false, true, false, true)] - [InlineData(false, false, true, true)] - [InlineData(false, false, false, true)] - [InlineData(true, true, true, false)] - [InlineData(true, true, false, false)] - [InlineData(true, false, true, false)] - [InlineData(true, false, false, false)] - [InlineData(false, true, true, false)] - [InlineData(false, true, false, false)] - [InlineData(false, false, true, false)] - [InlineData(false, false, false, false)] - public async Task CanTwoFactorSignIn(bool isPersistent, bool supportsLockout, bool externalLogin, bool rememberClient) + [InlineData(true, true, true, true, true)] + [InlineData(true, true, false, true, true)] + [InlineData(true, false, true, true, true)] + [InlineData(true, false, false, true, true)] + [InlineData(false, true, true, true, true)] + [InlineData(false, true, false, true, true)] + [InlineData(false, false, true, true, true)] + [InlineData(false, false, false, true, true)] + [InlineData(true, true, true, false, true)] + [InlineData(true, true, false, false, true)] + [InlineData(true, false, true, false, true)] + [InlineData(true, false, false, false, true)] + [InlineData(false, true, true, false, true)] + [InlineData(false, true, false, false, true)] + [InlineData(false, false, true, false, true)] + [InlineData(false, false, false, false, true)] + [InlineData(true, true, true, true, false)] + [InlineData(true, true, false, true, false)] + [InlineData(true, false, true, true, false)] + [InlineData(true, false, false, true, false)] + [InlineData(false, true, true, true, false)] + [InlineData(false, true, false, true, false)] + [InlineData(false, false, true, true, false)] + [InlineData(false, false, false, true, false)] + [InlineData(true, true, true, false, false)] + [InlineData(true, true, false, false, false)] + [InlineData(true, false, true, false, false)] + [InlineData(true, false, false, false, false)] + [InlineData(false, true, true, false, false)] + [InlineData(false, true, false, false, false)] + [InlineData(false, false, true, false, false)] + [InlineData(false, false, false, false, false)] + public async Task CanTwoFactorSignIn(bool isPersistent, bool supportsLockout, bool externalLogin, bool rememberClient, bool supportsUserActivity) { // Setup var user = new TestUser { UserName = "Foo" }; @@ -559,6 +593,11 @@ namespace Microsoft.AspNetCore.Identity.Test manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); } + if (supportsUserActivity) + { + manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable(); + manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); + } manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, provider, code)).ReturnsAsync(true).Verifiable(); var context = new DefaultHttpContext(); var auth = MockAuth(context); diff --git a/test/Microsoft.AspNetCore.Identity.Test/UserManagerTest.cs b/test/Microsoft.AspNetCore.Identity.Test/UserManagerTest.cs index 92ff64558f..9768362c14 100644 --- a/test/Microsoft.AspNetCore.Identity.Test/UserManagerTest.cs +++ b/test/Microsoft.AspNetCore.Identity.Test/UserManagerTest.cs @@ -580,6 +580,17 @@ namespace Microsoft.AspNetCore.Identity.Test Assert.Throws(() => manager.Users.Count()); } + [Fact] + public async Task UsersActivityMethodsFailWhenStoreNotImplemented() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserActivity); + await Assert.ThrowsAsync(() => manager.UpdateLastSignInDateAsync(null)); + await Assert.ThrowsAsync(() => manager.GetLastSignInDateAsync(null)); + await Assert.ThrowsAsync(() => manager.GetLastPasswordChangeDateAsync(null)); + await Assert.ThrowsAsync(() => manager.GetCreateDateAsync(null)); + } + [Fact] public async Task UsersEmailMethodsFailWhenStoreNotImplemented() { @@ -698,146 +709,6 @@ namespace Microsoft.AspNetCore.Identity.Test Assert.ThrowsAsync(() => manager.GenerateUserTokenAsync(new TestUser(), "A", "purpose")); } - [Fact] - public void TOTPTest() - { - //var verify = new TotpAuthenticatorVerification(); - //var secret = "abcdefghij"; - //var secret = Base32.FromBase32(authKey); - -// Assert.Equal(bytes, secret); - - //var code = verify.VerifyCode(secret, -1); - //Assert.Equal(code, 287004); - - - //var bytes = new byte[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o', (byte)'!', (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF }; - //var base32 = Base32.ToBase32(bytes); - // var code = Rfc6238AuthenticationService.GenerateCode(bytes); - // Assert.Equal(Rfc6238AuthenticationService.GenerateCode(bytes), Rfc6238AuthenticationService.CalculateOneTimePassword(new HMACSHA1(bytes))); - //Assert.True(Rfc6238AuthenticationService.ValidateCode(bytes, code)); - } - - public static byte[] ToBytes(string input) - { - if (string.IsNullOrEmpty(input)) - { - throw new ArgumentNullException("input"); - } - - input = input.TrimEnd('='); //remove padding characters - int byteCount = input.Length * 5 / 8; //this must be TRUNCATED - byte[] returnArray = new byte[byteCount]; - - byte curByte = 0, bitsRemaining = 8; - int mask = 0, arrayIndex = 0; - - foreach (char c in input) - { - int cValue = CharToValue(c); - - if (bitsRemaining > 5) - { - mask = cValue << (bitsRemaining - 5); - curByte = (byte)(curByte | mask); - bitsRemaining -= 5; - } - else - { - mask = cValue >> (5 - bitsRemaining); - curByte = (byte)(curByte | mask); - returnArray[arrayIndex++] = curByte; - curByte = (byte)(cValue << (3 + bitsRemaining)); - bitsRemaining += 3; - } - } - - //if we didn't end with a full byte - if (arrayIndex != byteCount) - { - returnArray[arrayIndex] = curByte; - } - - return returnArray; - } - - public static string ToString(byte[] input) - { - if (input == null || input.Length == 0) - { - throw new ArgumentNullException("input"); - } - - int charCount = (int)Math.Ceiling(input.Length / 5d) * 8; - char[] returnArray = new char[charCount]; - - byte nextChar = 0, bitsRemaining = 5; - int arrayIndex = 0; - - foreach (byte b in input) - { - nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining))); - returnArray[arrayIndex++] = ValueToChar(nextChar); - - if (bitsRemaining < 4) - { - nextChar = (byte)((b >> (3 - bitsRemaining)) & 31); - returnArray[arrayIndex++] = ValueToChar(nextChar); - bitsRemaining += 5; - } - - bitsRemaining -= 3; - nextChar = (byte)((b << bitsRemaining) & 31); - } - - //if we didn't end with a full char - if (arrayIndex != charCount) - { - returnArray[arrayIndex++] = ValueToChar(nextChar); - while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding - } - - return new string(returnArray); - } - - private static int CharToValue(char c) - { - var value = (int)c; - - //65-90 == uppercase letters - if (value < 91 && value > 64) - { - return value - 65; - } - //50-55 == numbers 2-7 - if (value < 56 && value > 49) - { - return value - 24; - } - //97-122 == lowercase letters - if (value < 123 && value > 96) - { - return value - 97; - } - - throw new ArgumentException("Character is not a Base32 character.", "c"); - } - - private static char ValueToChar(byte b) - { - if (b < 26) - { - return (char)(b + 65); - } - - if (b < 32) - { - return (char)(b + 24); - } - - throw new ArgumentException("Byte is not a value Base32 value.", "b"); - } - [Fact] public void UserManagerWillUseTokenProviderInstanceOverDefaults() { diff --git a/test/Shared/TestUser.cs b/test/Shared/TestUser.cs index d08e747a28..1e13a3cb93 100644 --- a/test/Shared/TestUser.cs +++ b/test/Shared/TestUser.cs @@ -124,6 +124,10 @@ namespace Microsoft.AspNetCore.Identity.Test /// public virtual int AccessFailedCount { get; set; } + public virtual DateTimeOffset? CreateDate { get; set; } + public virtual DateTimeOffset? LastSignInDate { get; set; } + public virtual DateTimeOffset? LastPasswordChangeDate { get; set; } + /// /// Navigation property ///