public async Task AddPackageOwnerAsync(PackageRegistration packageRegistration, User user, bool commitChanges = true) { if (packageRegistration == null) { throw new ArgumentNullException(nameof(packageRegistration)); } if (user == null) { throw new ArgumentNullException(nameof(user)); } if (commitChanges) { using (var strategy = new SuspendDbExecutionStrategy()) using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { await AddPackageOwnerTask(packageRegistration, user, commitChanges); transaction.Commit(); } } else { await AddPackageOwnerTask(packageRegistration, user, commitChanges); } await _auditingService.SaveAuditRecordAsync( new PackageRegistrationAuditRecord(packageRegistration, AuditedPackageRegistrationAction.AddOwner, user.Username)); }
public async Task AddPackageOwnerAsync(PackageRegistration packageRegistration, User user) { if (packageRegistration == null) { throw new ArgumentNullException(nameof(packageRegistration)); } if (user == null) { throw new ArgumentNullException(nameof(user)); } using (var strategy = new SuspendDbExecutionStrategy()) using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { Func <ReservedNamespace, bool> predicate = reservedNamespace => reservedNamespace.IsPrefix ? packageRegistration.Id.StartsWith(reservedNamespace.Value, StringComparison.OrdinalIgnoreCase) : packageRegistration.Id.Equals(reservedNamespace.Value, StringComparison.OrdinalIgnoreCase); var userOwnedMatchingNamespacesForId = user .ReservedNamespaces .Where(predicate); if (userOwnedMatchingNamespacesForId.Any()) { if (!packageRegistration.IsVerified) { await _packageService.UpdatePackageVerifiedStatusAsync(new List <PackageRegistration> { packageRegistration }, isVerified : true); } userOwnedMatchingNamespacesForId .ToList() .ForEach(mn => _reservedNamespaceService.AddPackageRegistrationToNamespace(mn.Value, packageRegistration)); // The 'AddPackageRegistrationToNamespace' does not commit its changes, so saving changes for consistency. await _entitiesContext.SaveChangesAsync(); } await _packageService.AddPackageOwnerAsync(packageRegistration, user); await DeletePackageOwnershipRequestAsync(packageRegistration, user); transaction.Commit(); } await _auditingService.SaveAuditRecordAsync( new PackageRegistrationAuditRecord(packageRegistration, AuditedPackageRegistrationAction.AddOwner, user.Username)); }
public async Task <Package> ReflowAsync(string id, string version) { var package = _packageService.FindPackageByIdAndVersionStrict(id, version); if (package == null) { return(null); } EntitiesConfiguration.SuspendExecutionStrategy = true; using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { // 1) Download package binary to memory using (var packageStream = await _packageFileService.DownloadPackageFileAsync(package)) { using (var packageArchive = new PackageArchiveReader(packageStream, leaveStreamOpen: false)) { // 2) Determine package metadata from binary var packageStreamMetadata = new PackageStreamMetadata { HashAlgorithm = Constants.Sha512HashAlgorithmId, Hash = CryptographyService.GenerateHash(packageStream.AsSeekableStream()), Size = packageStream.Length, }; var packageMetadata = PackageMetadata.FromNuspecReader(packageArchive.GetNuspecReader()); // 3) Clear referenced objects that will be reflowed ClearSupportedFrameworks(package); ClearAuthors(package); ClearDependencies(package); // 4) Reflow the package var listed = package.Listed; package = _packageService.EnrichPackageFromNuGetPackage( package, packageArchive, packageMetadata, packageStreamMetadata, package.User); package.LastEdited = DateTime.UtcNow; package.Listed = listed; // 5) Update IsLatest so that reflow can correct concurrent updates (see Gallery #2514) await _packageService.UpdateIsLatestAsync(package.PackageRegistration, commitChanges : false); // 6) Save and profit await _entitiesContext.SaveChangesAsync(); } } // Commit transaction transaction.Commit(); } EntitiesConfiguration.SuspendExecutionStrategy = false; return(package); }
/// <summary> /// Will clean-up the data related with an user account. /// The result will be: /// 1. The user will be removed as owner from its owned packages. /// 2. Any of the packages that become orphaned as its result will be unlisted if the unlistOrphanPackages is set to true. /// 3. Any owned namespaces will be released. /// 4. The user credentials will be cleaned. /// 5. The user data will be cleaned. /// </summary> /// <param name="userToBeDeleted">The user to be deleted.</param> /// <param name="admin">The admin that will perform the delete action.</param> /// <param name="signature">The admin signature.</param> /// <param name="unlistOrphanPackages">If the orphaned packages will unlisted.</param> /// <param name="commitAsTransaction">If the data will be persisted as a transaction.</param> /// <returns></returns> public async Task <DeleteUserAccountStatus> DeleteGalleryUserAccountAsync(User userToBeDeleted, User admin, string signature, bool unlistOrphanPackages, bool commitAsTransaction) { if (userToBeDeleted == null) { throw new ArgumentNullException(nameof(userToBeDeleted)); } if (admin == null) { throw new ArgumentNullException(nameof(admin)); } if (userToBeDeleted.IsDeleted) { return(new DeleteUserAccountStatus() { Success = false, Description = string.Format(Strings.AccountDelete_AccountAlreadyDeleted, userToBeDeleted.Username), AccountName = userToBeDeleted.Username }); } try { if (commitAsTransaction) { using (var strategy = new SuspendDbExecutionStrategy()) using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { await DeleteGalleryUserAccountImplAsync(userToBeDeleted, admin, signature, unlistOrphanPackages); transaction.Commit(); } } else { await DeleteGalleryUserAccountImplAsync(userToBeDeleted, admin, signature, unlistOrphanPackages); } return(new DeleteUserAccountStatus() { Success = true, Description = string.Format(Strings.AccountDelete_Success, userToBeDeleted.Username), AccountName = userToBeDeleted.Username }); } catch (Exception e) { QuietLog.LogHandledException(e); return(new DeleteUserAccountStatus() { Success = true, Description = string.Format(Strings.AccountDelete_Fail, userToBeDeleted.Username, e), AccountName = userToBeDeleted.Username }); } }
private async Task <DeleteUserAccountStatus> RunAccountDeletionTask(Func <Task> getTask, User userToBeDeleted, User requestingUser, bool commitAsTransaction) { try { // The support requests DB and gallery DB are different. // TransactionScope can be used for doing transaction actions across db on the same server but not on different servers. // The below code will clean the feature flags and suppport requests before the gallery data. // The order is important in order to allow the admin the opportunity to execute this step again. await _featureFlagService.RemoveUserAsync(userToBeDeleted); await RemoveSupportRequests(userToBeDeleted); if (commitAsTransaction) { using (var strategy = new SuspendDbExecutionStrategy()) using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { await getTask(); transaction.Commit(); } } else { await getTask(); } await _auditingService.SaveAuditRecordAsync(new DeleteAccountAuditRecord(username : userToBeDeleted.Username, status : DeleteAccountAuditRecord.ActionStatus.Success, action : AuditedDeleteAccountAction.DeleteAccount, adminUsername : requestingUser.Username)); return(new DeleteUserAccountStatus() { Success = true, Description = string.Format(CultureInfo.CurrentCulture, Strings.AccountDelete_Success, userToBeDeleted.Username), AccountName = userToBeDeleted.Username }); } catch (Exception e) { QuietLog.LogHandledException(e); return(new DeleteUserAccountStatus() { Success = false, Description = string.Format(CultureInfo.CurrentCulture, Strings.AccountDelete_Fail, userToBeDeleted.Username, e), AccountName = userToBeDeleted.Username }); } }
public async Task SoftDeletePackagesAsync(IEnumerable <Package> packages, User deletedBy, string reason, string signature) { using (var strategy = new SuspendDbExecutionStrategy()) using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { // Increase command timeout _entitiesContext.SetCommandTimeout(seconds: 300); // Keep package registrations var packageRegistrations = packages .GroupBy(p => p.PackageRegistration) .Select(g => g.First().PackageRegistration) .ToList(); // Backup the package binaries and remove from main storage // We're doing this early in the process as we need the metadata to still exist in the DB. await BackupPackageBinaries(packages); // Store the soft delete in the database var packageDelete = new PackageDelete { DeletedOn = DateTime.UtcNow, DeletedBy = deletedBy, Reason = reason, Signature = signature }; foreach (var package in packages) { /// We do not call <see cref="IPackageService.MarkPackageUnlistedAsync(Package, bool)"/> here /// because that writes an audit entry. Additionally, the latest bits are already updated by /// the package status change. package.Listed = false; await _packageService.UpdatePackageStatusAsync( package, PackageStatus.Deleted, commitChanges : false); packageDelete.Packages.Add(package); await _auditingService.SaveAuditRecordAsync(CreateAuditRecord(package, package.PackageRegistration, AuditedPackageAction.SoftDelete, reason)); _telemetryService.TrackPackageDelete(package, isHardDelete: false); } _packageDeletesRepository.InsertOnCommit(packageDelete); // Commit changes await _packageRepository.CommitChangesAsync(); await _packageDeletesRepository.CommitChangesAsync(); transaction.Commit(); } // Force refresh the index UpdateSearchIndex(); }
public async Task SoftDeletePackagesAsync(IEnumerable <Package> packages, User deletedBy, string reason, string signature) { EntitiesConfiguration.SuspendExecutionStrategy = true; using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { // Increase command timeout _entitiesContext.SetCommandTimeout(seconds: 300); // Keep package registrations var packageRegistrations = packages .GroupBy(p => p.PackageRegistration) .Select(g => g.First().PackageRegistration) .ToList(); // Backup the package binaries and remove from main storage // We're doing this early in the process as we need the metadata to still exist in the DB. await BackupPackageBinaries(packages); // Store the soft delete in the database var packageDelete = new PackageDelete { DeletedOn = DateTime.UtcNow, DeletedBy = deletedBy, Reason = reason, Signature = signature }; foreach (var package in packages) { package.Listed = false; package.Deleted = true; packageDelete.Packages.Add(package); await _auditingService.SaveAuditRecord(CreateAuditRecord(package, package.PackageRegistration, PackageAuditAction.SoftDeleted, reason)); } _packageDeletesRepository.InsertOnCommit(packageDelete); // Update latest versions await UpdateIsLatestAsync(packageRegistrations); // Commit changes await _packageRepository.CommitChangesAsync(); await _packageDeletesRepository.CommitChangesAsync(); transaction.Commit(); } EntitiesConfiguration.SuspendExecutionStrategy = false; // Force refresh the index UpdateSearchIndex(); }
public static async Task <bool> TransformUserToOrganization(this IEntitiesContext context, User accountToTransform, User adminUser, string token) { accountToTransform = accountToTransform ?? throw new ArgumentNullException(nameof(accountToTransform)); adminUser = adminUser ?? throw new ArgumentNullException(nameof(adminUser)); if (string.IsNullOrWhiteSpace(token)) { throw new ArgumentException(nameof(token)); } var database = context.GetDatabase(); var recordCount = await database.ExecuteSqlResourceAsync( MigrateUserToOrganization.ResourceName, new SqlParameter(MigrateUserToOrganization.OrganizationKey, accountToTransform.Key), new SqlParameter(MigrateUserToOrganization.AdminKey, adminUser.Key), new SqlParameter(MigrateUserToOrganization.ConfirmationToken, token)); return(recordCount > 0); }
/// <remarks> /// Normally we would use a large parameterized SQL query for this. /// Unfortunately, however, there is a maximum number of parameters for a SQL query (around 2,000-3,000). /// By writing a query containing the package keys directly we can remove this restriction. /// Furthermore, package keys are not user data, so there is no risk to writing a query in this way. /// </remarks> private async Task UpdatePackagesInBulkAsync(IReadOnlyList<int> packageKeys) { var query = string.Format( UpdateBulkPackagesQueryFormat, string.Join( ", ", packageKeys .OrderBy(k => k))); var result = await _entitiesContext .GetDatabase() .ExecuteSqlCommandAsync(query); // The query updates each row twice--once for the initial commit and a second time due to the trigger on LastEdited. var expectedResult = packageKeys.Count() * 2; if (result != expectedResult) { throw new InvalidOperationException( $"Updated an unexpected number of packages when performing a bulk update! " + $"Updated {result} packages instead of {expectedResult}."); } }
/// <summary> /// Will clean-up the data related with an user account. /// The result will be: /// 1. The user will be removed as owner from its owned packages. /// 2. Any of the packages that become orphaned as its result will be unlisted if the unlistOrphanPackages is set to true. /// 3. Any owned namespaces will be released. /// 4. The user credentials will be cleaned. /// 5. The user data will be cleaned. /// </summary> /// <param name="userToBeDeleted">The user to be deleted.</param> /// <param name="admin">The admin that will perform the delete action.</param> /// <param name="signature">The admin signature.</param> /// <param name="unlistOrphanPackages">If the orphaned packages will unlisted.</param> /// <param name="commitAsTransaction">If the data will be persisted as a transaction.</param> /// <returns></returns> public async Task <DeleteUserAccountStatus> DeleteGalleryUserAccountAsync(User userToBeDeleted, User admin, string signature, bool unlistOrphanPackages, bool commitAsTransaction) { if (userToBeDeleted == null) { throw new ArgumentNullException(nameof(userToBeDeleted)); } if (admin == null) { throw new ArgumentNullException(nameof(admin)); } if (userToBeDeleted.IsDeleted) { return(new DeleteUserAccountStatus() { Success = false, Description = string.Format(CultureInfo.CurrentCulture, Strings.AccountDelete_AccountAlreadyDeleted, userToBeDeleted.Username), AccountName = userToBeDeleted.Username }); } // The deletion of Organization and Organization member accounts is disabled for now. if (userToBeDeleted is Organization) { return(new DeleteUserAccountStatus() { Success = false, Description = string.Format(CultureInfo.CurrentCulture, Strings.AccountDelete_OrganizationDeleteNotImplemented, userToBeDeleted.Username), AccountName = userToBeDeleted.Username }); } else if (userToBeDeleted.Organizations.Any()) { return(new DeleteUserAccountStatus() { Success = false, Description = string.Format(CultureInfo.CurrentCulture, Strings.AccountDelete_OrganizationMemberDeleteNotImplemented, userToBeDeleted.Username), AccountName = userToBeDeleted.Username }); } try { // The support requests db and gallery db are different. // TransactionScope can be used for doing transaction actions across db on the same server but not on different servers. // The below code will clean first the suppport requests and after the gallery data. // The order is important in order to allow the admin the oportunity to execute this step again. await RemoveSupportRequests(userToBeDeleted); if (commitAsTransaction) { using (var strategy = new SuspendDbExecutionStrategy()) using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { await DeleteGalleryUserAccountImplAsync(userToBeDeleted, admin, signature, unlistOrphanPackages); transaction.Commit(); } } else { await DeleteGalleryUserAccountImplAsync(userToBeDeleted, admin, signature, unlistOrphanPackages); } await _auditingService.SaveAuditRecordAsync(new DeleteAccountAuditRecord(username : userToBeDeleted.Username, status : DeleteAccountAuditRecord.ActionStatus.Success, action : AuditedDeleteAccountAction.DeleteAccount, adminUsername : admin.Username)); return(new DeleteUserAccountStatus() { Success = true, Description = string.Format(CultureInfo.CurrentCulture, Strings.AccountDelete_Success, userToBeDeleted.Username), AccountName = userToBeDeleted.Username }); } catch (Exception e) { QuietLog.LogHandledException(e); return(new DeleteUserAccountStatus() { Success = true, Description = string.Format(CultureInfo.CurrentCulture, Strings.AccountDelete_Fail, userToBeDeleted.Username, e), AccountName = userToBeDeleted.Username }); } }
protected internal async virtual Task <bool> TryUpdateIsLatestInDatabase(IEntitiesContext context) { // Use the EF change tracker to identify changes made in TryUpdateIsLatestAsync which // need to be applied to the database below. // Note that the change tracker is not mocked which make this method hard to unit test. var changeTracker = context.GetChangeTracker(); var modifiedPackages = changeTracker.Entries <Package>().Where(p => p.State == EntityState.Modified).ToList(); if (modifiedPackages.Count == 0) { return(true); } // Apply changes to the database with an optimistic concurrency check to prevent multiple // threads (in the same or different gallery instance) from setting IsLatest/IsLatestStable // flag to true on different package versions. // To preserve existing behavior, we only want to reject concurrent updates which set the // IsLatest/IsLatestStable columns. For this reason, we must avoid the EF ConcurrencyCheck // attribute which could reject any package update or delete. var query = new StringBuilder("DECLARE @rowCount INT = 0"); foreach (var packageEntry in modifiedPackages) { // Set LastUpdated after all IsLatest/IsLatestStable changes are complete to ensure // that we don't update rows where IsLatest/IsLatestStable hasn't changed. packageEntry.Entity.LastUpdated = DateTime.UtcNow; var isLatest = packageEntry.Entity.IsLatest ? 1 : 0; var isLatestStable = packageEntry.Entity.IsLatestStable ? 1 : 0; var key = packageEntry.Entity.Key; var originalIsLatest = Boolean.Parse(packageEntry.OriginalValues["IsLatest"].ToString()) ? 1 : 0; var originalIsLatestStable = Boolean.Parse(packageEntry.OriginalValues["IsLatestStable"].ToString()) ? 1 : 0; query.AppendLine($"UPDATE [dbo].[Packages]"); query.AppendLine($"SET [IsLatest] = {isLatest}, [IsLatestStable] = {isLatestStable}, [LastUpdated] = GETUTCDATE()"); query.AppendLine($"WHERE [Key] = {key}"); // optimistic concurrency check to prevent concurrent sets of latest/latestStable query.AppendLine($" AND [IsLatest] = {originalIsLatest} AND [IsLatestStable] = {originalIsLatestStable}"); // ensure new latest/latestStable was not concurrently unlisted/deleted if (packageEntry.Entity.IsLatest || packageEntry.Entity.IsLatestStable) { query.AppendLine($" AND [Listed] = 1 AND [Deleted] = 0"); } query.AppendLine($"SET @rowCount = @rowCount + @@ROWCOUNT"); } query.AppendLine("SELECT @rowCount"); using (var transaction = context.GetDatabase().BeginTransaction(IsolationLevel.ReadCommitted)) { var rowCount = await context.GetDatabase().ExecuteSqlCommandAsync(query.ToString()); if (rowCount == modifiedPackages.Count) { transaction.Commit(); return(true); } else { // RowCount will not match if one or more updates failed the concurrency check. This // likely means another thread is trying to clear the current IsLatest/IsLatestStable. transaction.Rollback(); return(false); } } }
public async Task UpdateDeprecation( IReadOnlyList <Package> packages, PackageDeprecationStatus status, PackageRegistration alternatePackageRegistration, Package alternatePackage, string customMessage, User user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } if (packages == null || !packages.Any()) { throw new ArgumentException(nameof(packages)); } var registration = packages.First().PackageRegistration; if (packages.Select(p => p.PackageRegistrationKey).Distinct().Count() > 1) { throw new ArgumentException("All packages to deprecate must have the same ID.", nameof(packages)); } using (var strategy = new SuspendDbExecutionStrategy()) using (var transaction = _entitiesContext.GetDatabase().BeginTransaction()) { var shouldDelete = status == PackageDeprecationStatus.NotDeprecated; var deprecations = new List <PackageDeprecation>(); foreach (var package in packages) { var deprecation = package.Deprecations.SingleOrDefault(); if (shouldDelete) { if (deprecation != null) { package.Deprecations.Remove(deprecation); deprecations.Add(deprecation); } } else { if (deprecation == null) { deprecation = new PackageDeprecation { Package = package }; package.Deprecations.Add(deprecation); deprecations.Add(deprecation); } deprecation.Status = status; deprecation.DeprecatedByUser = user; deprecation.AlternatePackageRegistration = alternatePackageRegistration; deprecation.AlternatePackage = alternatePackage; deprecation.CustomMessage = customMessage; } } if (shouldDelete) { _entitiesContext.Deprecations.RemoveRange(deprecations); } else { _entitiesContext.Deprecations.AddRange(deprecations); } await _entitiesContext.SaveChangesAsync(); await _packageUpdateService.UpdatePackagesAsync(packages); transaction.Commit(); _telemetryService.TrackPackageDeprecate( packages, status, alternatePackageRegistration, alternatePackage, !string.IsNullOrWhiteSpace(customMessage)); foreach (var package in packages) { await _auditingService.SaveAuditRecordAsync( new PackageAuditRecord( package, status == PackageDeprecationStatus.NotDeprecated ? AuditedPackageAction.Undeprecate : AuditedPackageAction.Deprecate, status == PackageDeprecationStatus.NotDeprecated ? PackageUndeprecatedVia.Web : PackageDeprecatedVia.Web)); } } }