// 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; 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. /// /// 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 UserStore : UserStoreBase, IProtectedUserStore 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 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) : Task.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(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 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 Task.CompletedTask; } /// /// Remove a new user token. /// /// The token to be removed. /// protected override Task RemoveUserTokenAsync(TUserToken token) { UserTokens.Remove(token); return Task.CompletedTask; } } }