From 5a02f4e330c6b3c708097f3f2aa95b036917573e Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Tue, 2 Jan 2018 14:44:00 -0800 Subject: [PATCH 1/8] Organizations: transform on confirmation --- .../Entities/DatabaseWrapper.cs | 29 ++++ .../Entities/EntitiesContext.cs | 4 +- src/NuGetGallery.Core/Entities/IDatabase.cs | 15 +++ .../Entities/IEntitiesContext.cs | 2 +- .../NuGetGallery.Core.csproj | 2 + src/NuGetGallery/App_Start/Routes.cs | 5 + .../Configuration/AppConfiguration.cs | 4 + .../Configuration/IAppConfiguration.cs | 2 + .../Controllers/UsersController.cs | 55 ++++++++ src/NuGetGallery/Extensions/UserExtensions.cs | 7 + .../AddMigrateToOrganization.Down.sql | 8 ++ .../AddMigrateToOrganization.Up.sql | 49 +++++++ ..._AddMigrateToOrganizationSproc.Designer.cs | 29 ++++ ...220037446_AddMigrateToOrganizationSproc.cs | 16 +++ ...0037446_AddMigrateToOrganizationSproc.resx | 126 ++++++++++++++++++ src/NuGetGallery/NuGetGallery.csproj | 11 ++ src/NuGetGallery/RouteNames.cs | 1 + src/NuGetGallery/Services/IUserService.cs | 2 + .../Services/PackageDeleteService.cs | 2 +- .../Services/TransformAccountException.cs | 23 ++++ src/NuGetGallery/Services/UserService.cs | 55 +++++++- src/NuGetGallery/Strings.Designer.cs | 63 +++++++++ src/NuGetGallery/Strings.resx | 21 +++ src/NuGetGallery/Telemetry/Obfuscator.cs | 2 +- .../Views/Users/AccountTransformFailed.cshtml | 17 +++ .../Extensions/UserExtensionsFacts.cs | 32 +++++ .../Services/DeleteAccountServiceFacts.cs | 2 +- .../Services/PackageDeleteServiceFacts.cs | 28 ++-- .../PackageOwnershipManagementServiceFacts.cs | 2 +- .../Services/ReflowPackageServiceFacts.cs | 2 +- .../Services/UserServiceFacts.cs | 118 ++++++++++++++++ .../TestUtils/FakeEntitiesContext.cs | 2 +- .../TestUtils/TestServiceUtility.cs | 9 +- 33 files changed, 718 insertions(+), 27 deletions(-) create mode 100644 src/NuGetGallery.Core/Entities/DatabaseWrapper.cs create mode 100644 src/NuGetGallery.Core/Entities/IDatabase.cs create mode 100644 src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Down.sql create mode 100644 src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql create mode 100644 src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.Designer.cs create mode 100644 src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.cs create mode 100644 src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.resx create mode 100644 src/NuGetGallery/Services/TransformAccountException.cs create mode 100644 src/NuGetGallery/Views/Users/AccountTransformFailed.cshtml diff --git a/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs b/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs new file mode 100644 index 0000000000..16f70cb1f4 --- /dev/null +++ b/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs @@ -0,0 +1,29 @@ +// 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.Data.Entity; +using System.Threading.Tasks; + +namespace NuGetGallery +{ + public class DatabaseWrapper : IDatabase + { + private Database _database; + + public DatabaseWrapper(Database database) + { + _database = database ?? throw new ArgumentNullException(nameof(database)); + } + + public Task ExecuteSqlCommandAsync(string sql, params object[] parameters) + { + return _database.ExecuteSqlCommandAsync(sql, parameters); + } + + public DbContextTransaction BeginTransaction() + { + return _database.BeginTransaction(); + } + } +} diff --git a/src/NuGetGallery.Core/Entities/EntitiesContext.cs b/src/NuGetGallery.Core/Entities/EntitiesContext.cs index 248421bb69..ed4a898245 100644 --- a/src/NuGetGallery.Core/Entities/EntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/EntitiesContext.cs @@ -79,9 +79,9 @@ public void SetCommandTimeout(int? seconds) ObjectContext.CommandTimeout = seconds; } - public Database GetDatabase() + public IDatabase GetDatabase() { - return Database; + return new DatabaseWrapper(Database); } #pragma warning disable 618 // TODO: remove Package.Authors completely once production services definitely no longer need it diff --git a/src/NuGetGallery.Core/Entities/IDatabase.cs b/src/NuGetGallery.Core/Entities/IDatabase.cs new file mode 100644 index 0000000000..6dad8a0d98 --- /dev/null +++ b/src/NuGetGallery.Core/Entities/IDatabase.cs @@ -0,0 +1,15 @@ +// 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.Data.Entity; +using System.Threading.Tasks; + +namespace NuGetGallery +{ + public interface IDatabase + { + Task ExecuteSqlCommandAsync(string sql, params object[] parameters); + + DbContextTransaction BeginTransaction(); + } +} diff --git a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs index 6f85ac73e9..13cd007ed5 100644 --- a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs @@ -22,6 +22,6 @@ public interface IEntitiesContext IDbSet Set() where T : class; void DeleteOnCommit(T entity) where T : class; void SetCommandTimeout(int? seconds); - Database GetDatabase(); + IDatabase GetDatabase(); } } \ No newline at end of file diff --git a/src/NuGetGallery.Core/NuGetGallery.Core.csproj b/src/NuGetGallery.Core/NuGetGallery.Core.csproj index 68aba0b8fd..05a66d48c6 100644 --- a/src/NuGetGallery.Core/NuGetGallery.Core.csproj +++ b/src/NuGetGallery.Core/NuGetGallery.Core.csproj @@ -171,6 +171,8 @@ + + diff --git a/src/NuGetGallery/App_Start/Routes.cs b/src/NuGetGallery/App_Start/Routes.cs index edf51e8ecf..6afec3868d 100644 --- a/src/NuGetGallery/App_Start/Routes.cs +++ b/src/NuGetGallery/App_Start/Routes.cs @@ -289,6 +289,11 @@ public static void RegisterUIRoutes(RouteCollection routes) "account/{action}", new { controller = "Users", action = "Account" }); + routes.MapRoute( + RouteName.TransformAccountConfirmation, + "account/transform/confirm/{accountName}/{token}", + new { controller = "Users", action = "ConfirmTransform" }); + routes.MapRoute( RouteName.ApiKeys, "account/apikeys", diff --git a/src/NuGetGallery/Configuration/AppConfiguration.cs b/src/NuGetGallery/Configuration/AppConfiguration.cs index 0243fe7f8c..76ba2501cd 100644 --- a/src/NuGetGallery/Configuration/AppConfiguration.cs +++ b/src/NuGetGallery/Configuration/AppConfiguration.cs @@ -66,6 +66,10 @@ public class AppConfiguration : IAppConfiguration public bool AsynchronousPackageValidationEnabled { get; set; } public bool BlockingAsynchronousPackageValidationEnabled { get; set; } + + [DefaultValue(null)] + [TypeConverter(typeof(StringArrayConverter))] + public string[] OrganizationsEnabledForDomains { get; set; } /// /// Gets the URI to the search service diff --git a/src/NuGetGallery/Configuration/IAppConfiguration.cs b/src/NuGetGallery/Configuration/IAppConfiguration.cs index ffde86ad36..323a89ac26 100644 --- a/src/NuGetGallery/Configuration/IAppConfiguration.cs +++ b/src/NuGetGallery/Configuration/IAppConfiguration.cs @@ -93,6 +93,8 @@ public interface IAppConfiguration : ICoreMessageServiceConfiguration /// bool BlockingAsynchronousPackageValidationEnabled { get; set; } + string[] OrganizationsEnabledForDomains { get; set; } + /// /// Gets the URI to the search service /// diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index 107691e279..abe43cb5c1 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Mail; @@ -98,6 +99,60 @@ public virtual ActionResult Account() { return AccountView(new AccountViewModel()); } + + [HttpGet] + [Authorize] + public virtual async Task ConfirmTransform(string accountName, string token) + { + var adminUser = GetCurrentUser(); + if (!adminUser.Confirmed) + { + TempData["TransformError"] = Strings.TransformAccount_AdminNotConfirmed; + return RedirectToAction("ConfirmationRequired"); + } + + var accountToTransform = _userService.FindByUsername(accountName); + if (accountToTransform == null) + { + TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_OrganizationAccountNotFound, accountName); + return View("AccountTransformFailed"); + } + + if (!CanTransformIntoOrganization(accountToTransform)) + { + TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_OrganizationAccountNotSupported, accountName); + return View("AccountTransformFailed"); + } + + try + { + await _userService.TransformToOrganizationAccount(accountToTransform, adminUser, token); + + TempData["Message"] = String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_Success, accountName); + + // todo: redirect to ManageOrganization (future work) + return RedirectToAction("Account"); + } + catch (Exception e) + { + TempData["TransformError"] = e.GetUserSafeMessage(); + return View("AccountTransformFailed"); + } + } + + private bool CanTransformIntoOrganization(User user) + { + if (!user.Confirmed || user.IsAdministrator()) + { + return false; + } + + var userDomain = user.ToMailAddress().Host; + return _config.OrganizationsEnabledForDomains.Contains(userDomain, StringComparer.OrdinalIgnoreCase); + } [HttpGet] [Authorize] diff --git a/src/NuGetGallery/Extensions/UserExtensions.cs b/src/NuGetGallery/Extensions/UserExtensions.cs index 91c6e44e23..b748c213ef 100644 --- a/src/NuGetGallery/Extensions/UserExtensions.cs +++ b/src/NuGetGallery/Extensions/UserExtensions.cs @@ -115,5 +115,12 @@ public static void SetAccountAsDeleted(this User user) user.FailedLoginCount = 0; user.IsDeleted = true; } + + public static string GetTenantId(this User user) + { + return user.Credentials + .SingleOrDefault(c => !string.IsNullOrEmpty(c.TenantId))? + .TenantId; + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Down.sql b/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Down.sql new file mode 100644 index 0000000000..23a742e544 --- /dev/null +++ b/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Down.sql @@ -0,0 +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. + +SET ANSI_NULLS ON + +IF OBJECT_ID('[dbo].[MigrateToOrganization]', 'P') IS NOT NULL + DROP PROCEDURE [dbo].[MigrateToOrganization] +GO \ No newline at end of file diff --git a/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql b/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql new file mode 100644 index 0000000000..31d67e171b --- /dev/null +++ b/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql @@ -0,0 +1,49 @@ +-- 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. + +SET ANSI_NULLS ON + +IF OBJECT_ID('[dbo].[MigrateToOrganization]', 'P') IS NOT NULL + DROP PROCEDURE [dbo].[MigrateToOrganization] +GO + +CREATE PROCEDURE [dbo].[MigrateToOrganization] +( + @orgKey INT, + @adminKey INT, + @token NVARCHAR(MAX) +) +AS +BEGIN + DECLARE @reqCount INT + + -- Ensure migration request exists + SELECT @reqCount = COUNT(*) + FROM [dbo].[OrganizationMigrationRequests] + WHERE NewOrganizationKey = @orgKey + AND AdminUserKey = @adminKey + AND ConfirmationToken = @token + IF @reqCount = 0 RETURN (0) + + BEGIN TRANSACTION + BEGIN TRY + -- Ensure Organizations do not have credentials or memberships + DELETE FROM [dbo].[Credentials] WHERE UserKey = @orgKey + DELETE FROM [dbo].[Memberships] WHERE MemberKey = @orgKey + DELETE FROM [dbo].[MembershipRequests] WHERE NewMemberKey = @orgKey + + -- Change to Organization account with single admin membership + INSERT INTO [dbo].[Organizations] ([Key]) VALUES (@orgKey) + INSERT INTO [dbo].[Memberships] (OrganizationKey, MemberKey, IsAdmin) VALUES (@orgKey, @adminKey, 1) + + -- Delete the migration request + DELETE FROM [dbo].[OrganizationMigrationRequests] WHERE NewOrganizationKey = @orgKey + + COMMIT TRANSACTION; + RETURN (1) + END TRY + BEGIN CATCH + ROLLBACK TRANSACTION + RETURN (0) + END CATCH +END \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.Designer.cs b/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.Designer.cs new file mode 100644 index 0000000000..31872b8b16 --- /dev/null +++ b/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.Designer.cs @@ -0,0 +1,29 @@ +// +namespace NuGetGallery.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class AddMigrateToOrganizationSproc : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddMigrateToOrganizationSproc)); + + string IMigrationMetadata.Id + { + get { return "201712220037446_AddMigrateToOrganizationSproc"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.cs b/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.cs new file mode 100644 index 0000000000..f998e0e293 --- /dev/null +++ b/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.cs @@ -0,0 +1,16 @@ +// 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 NuGetGallery.Migrations +{ + public partial class AddMigrateToOrganizationSproc : SqlResourceMigration + { + public AddMigrateToOrganizationSproc() + : base( + "NuGetGallery.Infrastructure.AddMigrateToOrganization.Up.sql", + "NuGetGallery.Infrastructure.AddMigrateToOrganization.Down.sql" + ) + { + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.resx b/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.resx new file mode 100644 index 0000000000..7e139f3069 --- /dev/null +++ b/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index 1ea1a5c3d0..3f4ff8c58c 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -800,6 +800,10 @@ 201712211850074_MembershipRequests.cs + + + 201712220037446_AddMigrateToOrganizationSproc.cs + @@ -905,6 +909,7 @@ + @@ -1720,6 +1725,8 @@ + + Always Designer @@ -1916,6 +1923,9 @@ 201712211850074_MembershipRequests.cs + + 201712220037446_AddMigrateToOrganizationSproc.cs + @@ -1985,6 +1995,7 @@ + diff --git a/src/NuGetGallery/RouteNames.cs b/src/NuGetGallery/RouteNames.cs index 9778144114..595043d95d 100644 --- a/src/NuGetGallery/RouteNames.cs +++ b/src/NuGetGallery/RouteNames.cs @@ -10,6 +10,7 @@ public static class RouteName public const string V2ApiFeed = "V2ApiFeed"; public const string ApiFeed = "ApiFeed"; public const string Account = "Account"; + public const string TransformAccountConfirmation = "ConfirmTransformAccount"; public const string ApiKeys = "ApiKeys"; public const string Profile = "Profile"; public const string DisplayPackage = "package-route"; diff --git a/src/NuGetGallery/Services/IUserService.cs b/src/NuGetGallery/Services/IUserService.cs index 01b324e2bf..e7751f2ea7 100644 --- a/src/NuGetGallery/Services/IUserService.cs +++ b/src/NuGetGallery/Services/IUserService.cs @@ -25,5 +25,7 @@ public interface IUserService Task CancelChangeEmailAddress(User user); Task> GetEmailAddressesForUserKeysAsync(IReadOnlyCollection distinctUserKeys); + + Task TransformToOrganizationAccount(User newOrganization, User adminUser, string token); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/PackageDeleteService.cs b/src/NuGetGallery/Services/PackageDeleteService.cs index 39c9138484..1ffad98ce5 100644 --- a/src/NuGetGallery/Services/PackageDeleteService.cs +++ b/src/NuGetGallery/Services/PackageDeleteService.cs @@ -224,7 +224,7 @@ public Task ReflowHardDeletedPackageAsync(string id, string version) return _auditingService.SaveAuditRecordAsync(auditRecord); } - protected virtual async Task ExecuteSqlCommandAsync(Database database, string sql, params object[] parameters) + protected virtual async Task ExecuteSqlCommandAsync(IDatabase database, string sql, params object[] parameters) { await database.ExecuteSqlCommandAsync(sql, parameters); } diff --git a/src/NuGetGallery/Services/TransformAccountException.cs b/src/NuGetGallery/Services/TransformAccountException.cs new file mode 100644 index 0000000000..6a53b9bd44 --- /dev/null +++ b/src/NuGetGallery/Services/TransformAccountException.cs @@ -0,0 +1,23 @@ +// 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.Globalization; + +namespace NuGetGallery +{ + [Serializable] + public class TransformAccountException + : Exception + { + public TransformAccountException(string message) + : base(message) + { + } + + public TransformAccountException(string message, params object[] args) + : base(string.Format(CultureInfo.CurrentCulture, message, args)) + { + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index 529ccb377a..8b3eacca8b 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -5,11 +5,13 @@ using System.Collections.Generic; using System.Data.Entity; using System.Linq; -using Crypto = NuGetGallery.CryptographyService; using NuGetGallery.Configuration; using NuGetGallery.Auditing; using System.Threading.Tasks; -using NuGetGallery.Migrations; +using NuGetGallery.Security; +using Crypto = NuGetGallery.CryptographyService; +using System.Data.SqlClient; +using System.Data; namespace NuGetGallery { @@ -19,6 +21,7 @@ public class UserService : IUserService public IEntityRepository UserRepository { get; protected set; } public IEntityRepository CredentialRepository { get; protected set; } public IAuditingService Auditing { get; protected set; } + public IEntitiesContext EntitiesContext { get; protected set; } protected UserService() { } @@ -26,13 +29,15 @@ public UserService( IAppConfiguration config, IEntityRepository userRepository, IEntityRepository credentialRepository, - IAuditingService auditing) + IAuditingService auditing, + IEntitiesContext entitiesContext) : this() { Config = config; UserRepository = userRepository; CredentialRepository = credentialRepository; Auditing = auditing; + EntitiesContext = entitiesContext; } public async Task ChangeEmailSubscriptionAsync(User user, bool emailAllowed, bool notifyPackagePushed) @@ -151,5 +156,49 @@ public async Task ConfirmEmailAddress(User user, string token) await UserRepository.CommitChangesAsync(); return true; } + + private const string ExecMigrateToOrganization = "EXEC [dbo].[MigrateToOrganization] @orgKey @adminKey @token"; + + public async Task TransformToOrganizationAccount(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 ArgumentNullException(nameof(token)); + } + + var tenantId = adminUser.GetTenantId(); + if (string.IsNullOrWhiteSpace(tenantId)) + { + // todo: add security policy to organization to enforce this (future work) + throw new TransformAccountException(Strings.TransformAccount_AdminDoesNotHaveTenantId); + } + + // Update from User to Organization account. Note that the type change will only be reflected in future + // requests, which use new EF context instances. + try + { + var database = EntitiesContext.GetDatabase(); + var result = await database.ExecuteSqlCommandAsync( + ExecMigrateToOrganization, + new SqlParameter("organizationKey", accountToTransform.Key), + new SqlParameter("adminKey", adminUser.Key), + new SqlParameter("token", token) + ); + + if (result == 0) + { + // Stored procedure returned failure, probably due to an unsatisfied migration request. + throw new TransformAccountException(Strings.TransformAccount_SaveFailed); + } + } + catch (Exception ex) when (ex is SqlException || ex is DataException) + { + // EF exception when saving account transformation to the database. + throw new TransformAccountException(Strings.TransformAccount_DatabaseError); + } + } } } diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index 91ae75a880..2c7f530659 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -1407,6 +1407,69 @@ public static string TokenExpirationShouldGiveUser1MinuteToChangePassword { } } + /// + /// Looks up a localized string similar to Failed to transform the account because you do not have an Azure AD sign-in with tenant ID.. + /// + public static string TransformAccount_AdminDoesNotHaveTenantId { + get { + return ResourceManager.GetString("TransformAccount_AdminDoesNotHaveTenantId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your account must be confirmed before you can create an organization.. + /// + public static string TransformAccount_AdminNotConfirmed { + get { + return ResourceManager.GetString("TransformAccount_AdminNotConfirmed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to transform the account due to a database error.. + /// + public static string TransformAccount_DatabaseError { + get { + return ResourceManager.GetString("TransformAccount_DatabaseError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to tranform account '{0}' because it was not found.. + /// + public static string TransformAccount_OrganizationAccountNotFound { + get { + return ResourceManager.GetString("TransformAccount_OrganizationAccountNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Account '{0}' cannot be transformed into an organization. Contact support for more information.. + /// + public static string TransformAccount_OrganizationAccountNotSupported { + get { + return ResourceManager.GetString("TransformAccount_OrganizationAccountNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to transform the account.. + /// + public static string TransformAccount_SaveFailed { + get { + return ResourceManager.GetString("TransformAccount_SaveFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Account '{0}' was successfully transformed into an organization.. + /// + public static string TransformAccount_Success { + get { + return ResourceManager.GetString("TransformAccount_Success", resourceCulture); + } + } + /// /// Looks up a localized string similar to User is not authorized. /// diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index 6c8ff632a3..ad0403ae12 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -688,4 +688,25 @@ For more information, please contact '{2}'. Package '{0}' has been locked. This means you cannot publish a new version or change the listing status of a published package version. Please contact support@nuget.org. + + Failed to transform the account because you do not have an Azure AD sign-in with tenant ID. + + + Your account must be confirmed before you can create an organization. + + + Failed to transform the account due to a database error. + + + Failed to tranform account '{0}' because it was not found. + + + Account '{0}' cannot be transformed into an organization. Contact support for more information. + + + Failed to transform the account. + + + Account '{0}' was successfully transformed into an organization. + \ No newline at end of file diff --git a/src/NuGetGallery/Telemetry/Obfuscator.cs b/src/NuGetGallery/Telemetry/Obfuscator.cs index a228135670..64081e04a7 100644 --- a/src/NuGetGallery/Telemetry/Obfuscator.cs +++ b/src/NuGetGallery/Telemetry/Obfuscator.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections; using System.Collections.Generic; namespace NuGetGallery @@ -21,6 +20,7 @@ internal static class Obfuscator "Packages/RejectPendingOwnershipRequest", "Packages/CancelPendingOwnershipRequest", "Users/Confirm", + "Users/ConfirmTransform", "Users/Delete", "Users/Profiles", "Users/ResetPassword"}; diff --git a/src/NuGetGallery/Views/Users/AccountTransformFailed.cshtml b/src/NuGetGallery/Views/Users/AccountTransformFailed.cshtml new file mode 100644 index 0000000000..2ece2cd552 --- /dev/null +++ b/src/NuGetGallery/Views/Users/AccountTransformFailed.cshtml @@ -0,0 +1,17 @@ +@{ + ViewBag.Title = "Transform Account"; + ViewBag.MdPageColumns = Constants.ColumnsFormMd; + Layout = "~/Views/Shared/Gallery/Layout.cshtml"; +} + +
+
+

+ Transform Account: Failed +

+ @if (TempData.ContainsKey("TransformError")) + { + @ViewHelpers.AlertWarning(@@TempData["TransformError"]) + } +
+
\ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Extensions/UserExtensionsFacts.cs b/tests/NuGetGallery.Facts/Extensions/UserExtensionsFacts.cs index 63e0f33a8d..e29e634e2c 100644 --- a/tests/NuGetGallery.Facts/Extensions/UserExtensionsFacts.cs +++ b/tests/NuGetGallery.Facts/Extensions/UserExtensionsFacts.cs @@ -6,6 +6,7 @@ using System.Security.Claims; using NuGetGallery.Authentication; using NuGetGallery.Framework; +using NuGetGallery.Infrastructure.Authentication; using Xunit; namespace NuGetGallery.Extensions @@ -223,5 +224,36 @@ public void WhenApiKeyWithNonMatchingOwnerScopes_ReturnsFalse() Assert.False(user.MatchesOwnerScope(credential)); } } + + public class TheGetTenantIdMethod + { + [Fact] + public void WhenHasTenant_ReturnsValue() + { + var user = new User() { Key = 1234 }; + user.Credentials.Add( + new CredentialBuilder().CreateExternalCredential( + issuer: "MicrosoftAccount", + value: "abc123", + identity: "TestUser", + tenantId: "zyx987")); + + Assert.Equal("zyx987", user.GetTenantId()); + } + + [Fact] + public void WhenNoTenant_ReturnsNull() + { + var user = new User() { Key = 1234 }; + user.Credentials.Add( + new CredentialBuilder().CreateExternalCredential( + issuer: "MicrosoftAccount", + value: "abc123", + identity: "TestUser", + tenantId: null)); + + Assert.Null(user.GetTenantId()); + } + } } } diff --git a/tests/NuGetGallery.Facts/Services/DeleteAccountServiceFacts.cs b/tests/NuGetGallery.Facts/Services/DeleteAccountServiceFacts.cs index 12fbd594ec..a77fa4f7af 100644 --- a/tests/NuGetGallery.Facts/Services/DeleteAccountServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/DeleteAccountServiceFacts.cs @@ -258,7 +258,7 @@ private Mock SetupEntitiesContext() { var mockContext = new Mock(); var dbContext = new Mock(); - mockContext.Setup(m => m.GetDatabase()).Returns(dbContext.Object.Database); + mockContext.Setup(m => m.GetDatabase()).Returns(new DatabaseWrapper(dbContext.Object.Database)); return mockContext; } diff --git a/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs index c1b41790fd..ee3d34cb34 100644 --- a/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackageDeleteServiceFacts.cs @@ -34,7 +34,7 @@ private static IPackageDeleteService CreateService( var dbContext = new Mock(); entitiesContext = entitiesContext ?? new Mock(); - entitiesContext.Setup(m => m.GetDatabase()).Returns(dbContext.Object.Database); + entitiesContext.Setup(m => m.GetDatabase()).Returns(new DatabaseWrapper(dbContext.Object.Database)); packageService = packageService ?? new Mock(); indexingService = indexingService ?? new Mock(); @@ -72,12 +72,12 @@ public TestPackageDeleteService(IEntityRepository packageRepository, IE { } - protected override async Task ExecuteSqlCommandAsync(Database database, string sql, params object[] parameters) + protected override async Task ExecuteSqlCommandAsync(IDatabase database, string sql, params object[] parameters) { await TestExecuteSqlCommandAsync(database, sql, parameters); } - public virtual Task TestExecuteSqlCommandAsync(Database database, string sql, params object[] parameters) + public virtual Task TestExecuteSqlCommandAsync(IDatabase database, string sql, params object[] parameters) { // do nothing - this method solely exists to make verifying SQL queries possible return Task.FromResult(0); @@ -335,9 +335,9 @@ public async Task WillDeletePackageAndRelatedEntities() var entitiesContext = new Mock(); var service = CreateService(packageRepository: packageRepository, entitiesContext: entitiesContext, setup: svc => { - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pa FROM PackageAuthors pa JOIN Packages p ON p.[Key] = pa.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pd FROM PackageDependencies pd JOIN Packages p ON p.[Key] = pd.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pf FROM PackageFrameworks pf JOIN Packages p ON p.[Key] = pf.Package_Key WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pa FROM PackageAuthors pa JOIN Packages p ON p.[Key] = pa.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pd FROM PackageDependencies pd JOIN Packages p ON p.[Key] = pd.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pf FROM PackageFrameworks pf JOIN Packages p ON p.[Key] = pf.Package_Key WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); }); var packageRegistration = new PackageRegistration(); packageRegistration.Packages.Add(new Package { Key = 124, PackageRegistration = packageRegistration, Version = "1.0.0", Hash = _packageHashForTests }); @@ -364,11 +364,11 @@ public async Task WillNotDeletePackageRegistrationWhenNoPackagesLeftAndDeleteEmp var entitiesContext = new Mock(); var service = CreateService(packageRepository: packageRepository, entitiesContext: entitiesContext, setup: svc => { - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pa FROM PackageAuthors pa JOIN Packages p ON p.[Key] = pa.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pd FROM PackageDependencies pd JOIN Packages p ON p.[Key] = pd.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pf FROM PackageFrameworks pf JOIN Packages p ON p.[Key] = pf.Package_Key WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pa FROM PackageAuthors pa JOIN Packages p ON p.[Key] = pa.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pd FROM PackageDependencies pd JOIN Packages p ON p.[Key] = pd.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pf FROM PackageFrameworks pf JOIN Packages p ON p.[Key] = pf.Package_Key WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), PackageDeleteService.DeletePackageRegistrationQuery, It.IsAny())).Callback(() => ranDeleteQuery = true).Returns(Task.FromResult(0)); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), PackageDeleteService.DeletePackageRegistrationQuery, It.IsAny())).Callback(() => ranDeleteQuery = true).Returns(Task.FromResult(0)); }); var packageRegistration = new PackageRegistration(); var package = new Package { Key = 123, PackageRegistration = packageRegistration, Version = "1.0.0", Hash = _packageHashForTests }; @@ -395,11 +395,11 @@ public async Task WillDeletePackageRegistrationWhenNoPackagesLeftAndDeleteEmptyP var entitiesContext = new Mock(); var service = CreateService(packageRepository: packageRepository, entitiesContext: entitiesContext, setup: svc => { - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pa FROM PackageAuthors pa JOIN Packages p ON p.[Key] = pa.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pd FROM PackageDependencies pd JOIN Packages p ON p.[Key] = pd.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pf FROM PackageFrameworks pf JOIN Packages p ON p.[Key] = pf.Package_Key WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pa FROM PackageAuthors pa JOIN Packages p ON p.[Key] = pa.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pd FROM PackageDependencies pd JOIN Packages p ON p.[Key] = pd.PackageKey WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), "DELETE pf FROM PackageFrameworks pf JOIN Packages p ON p.[Key] = pf.Package_Key WHERE p.[Key] = @key", It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); - svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), PackageDeleteService.DeletePackageRegistrationQuery, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + svc.Setup(x => x.TestExecuteSqlCommandAsync(It.IsAny(), PackageDeleteService.DeletePackageRegistrationQuery, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); }); var packageRegistration = new PackageRegistration(); var package = new Package { Key = 123, PackageRegistration = packageRegistration, Version = "1.0.0", Hash = _packageHashForTests }; diff --git a/tests/NuGetGallery.Facts/Services/PackageOwnershipManagementServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackageOwnershipManagementServiceFacts.cs index 410eb7e609..635ab364cb 100644 --- a/tests/NuGetGallery.Facts/Services/PackageOwnershipManagementServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackageOwnershipManagementServiceFacts.cs @@ -26,7 +26,7 @@ private static PackageOwnershipManagementService CreateService( { var dbContext = new Mock(); entitiesContext = entitiesContext ?? new Mock(); - entitiesContext.Setup(m => m.GetDatabase()).Returns(dbContext.Object.Database); + entitiesContext.Setup(m => m.GetDatabase()).Returns(new DatabaseWrapper(dbContext.Object.Database)); packageService = packageService ?? new Mock(); reservedNamespaceService = reservedNamespaceService ?? new Mock(); packageOwnerRequestService = packageOwnerRequestService ?? new Mock(); diff --git a/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs index 0df66e4e1c..6634329078 100644 --- a/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs @@ -27,7 +27,7 @@ private static ReflowPackageService CreateService( { var dbContext = new Mock(); entitiesContext = entitiesContext ?? new Mock(); - entitiesContext.Setup(m => m.GetDatabase()).Returns(dbContext.Object.Database); + entitiesContext.Setup(m => m.GetDatabase()).Returns(new DatabaseWrapper(dbContext.Object.Database)); packageService = packageService ?? new Mock(); packageFileService = packageFileService ?? new Mock(); diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 5a1d1ff2dd..d8b2064953 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -8,6 +8,10 @@ using NuGetGallery.Auditing; using Xunit; using NuGetGallery.TestUtils; +using Moq; +using System.Data.SqlClient; +using System.Data; +using NuGetGallery.Infrastructure.Authentication; namespace NuGetGallery { @@ -359,6 +363,120 @@ public async Task ThrowsArgumentExceptionForNullUser() await ContractAssert.ThrowsArgNullAsync(async () => await service.ChangeEmailSubscriptionAsync(null, emailAllowed: true, notifyPackagePushed: true), "user"); } } + + public class TheTransformToOrganizationAccountMethod + { + [Fact] + public async Task WhenAccountIsNull_ThrowsArgNullException() + { + await ContractAssert.ThrowsArgNullAsync( + async () => await new TestableUserService().TransformToOrganizationAccount(null, new User("admin"), "token"), + "accountToTransform"); + } + + [Fact] + public async Task WhenAdminIsNull_ThrowsArgNullException() + { + await ContractAssert.ThrowsArgNullAsync( + async () => await new TestableUserService().TransformToOrganizationAccount(new User("account"), null, "token"), + "adminUser"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public async Task WhenTokenIsMissing_ThrowsArgException(string token) + { + await ContractAssert.ThrowsArgNullAsync( + async () => await new TestableUserService().TransformToOrganizationAccount(new User("account"), new User("admin"), token), + "token"); + } + + [Fact] + public async Task WhenNoTenant_ThrowsTransformAccountException() + { + // Arrange + var account = new User("Account"); + var admin = new User("Admin"); + var service = new TestableUserService(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + async () => await service.TransformToOrganizationAccount(account, admin, "token")); + Assert.Equal(exception.Message, Strings.TransformAccount_AdminDoesNotHaveTenantId); + } + + [Fact] + public async Task WhenSqlException_ThrowsTransformAccountException() + { + // Arrange + var service = new TestableUserService(); + var account = new User("Account"); + var admin = new User("Admin"); + admin.Credentials.Add( + new CredentialBuilder().CreateExternalCredential( + issuer: "MicrosoftAccount", + value: "abc123", + identity: "Admin", + tenantId: "zyx987")); + + service.MockDatabase + .Setup(db => db.ExecuteSqlCommandAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new DataException()); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + async () => await service.TransformToOrganizationAccount(account, admin, "token")); + Assert.Equal(exception.Message, Strings.TransformAccount_DatabaseError); + } + + [Fact] + public async Task WhenSqlResultIsZero_ThrowsTransformAccountException() + { + // Arrange + var service = new TestableUserService(); + var account = new User("Account"); + var admin = new User("Admin"); + admin.Credentials.Add( + new CredentialBuilder().CreateExternalCredential( + issuer: "MicrosoftAccount", + value: "abc123", + identity: "Admin", + tenantId: "zyx987")); + + service.MockDatabase + .Setup(db => db.ExecuteSqlCommandAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(0)); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + async () => await service.TransformToOrganizationAccount(account, admin, "token")); + Assert.Equal(exception.Message, Strings.TransformAccount_SaveFailed); + } + + [Fact] + public async Task WhenSqlResultIsOne_ReturnsSuccess() + { + // Arrange + var service = new TestableUserService(); + var account = new User("Account"); + var admin = new User("Admin"); + admin.Credentials.Add( + new CredentialBuilder().CreateExternalCredential( + issuer: "MicrosoftAccount", + value: "abc123", + identity: "Admin", + tenantId: "zyx987")); + + service.MockDatabase + .Setup(db => db.ExecuteSqlCommandAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(1)); + + // Act + await service.TransformToOrganizationAccount(account, admin, "token"); + } + } } } diff --git a/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs b/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs index fc644d9970..2ef775280a 100644 --- a/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs +++ b/tests/NuGetGallery.Facts/TestUtils/FakeEntitiesContext.cs @@ -166,7 +166,7 @@ public void SetCommandTimeout(int? seconds) throw new NotSupportedException(); } - public Database GetDatabase() + public IDatabase GetDatabase() { throw new NotSupportedException(); } diff --git a/tests/NuGetGallery.Facts/TestUtils/TestServiceUtility.cs b/tests/NuGetGallery.Facts/TestUtils/TestServiceUtility.cs index edf58aab5c..bd9583d078 100644 --- a/tests/NuGetGallery.Facts/TestUtils/TestServiceUtility.cs +++ b/tests/NuGetGallery.Facts/TestUtils/TestServiceUtility.cs @@ -133,16 +133,22 @@ public class TestableUserService : UserService public Mock MockConfig { get; protected set; } public Mock> MockUserRepository { get; protected set; } public Mock> MockCredentialRepository { get; protected set; } + public Mock MockEntitiesContext { get; protected set; } + public Mock MockDatabase { get; protected set; } public TestableUserService() { Config = (MockConfig = new Mock()).Object; UserRepository = (MockUserRepository = new Mock>()).Object; CredentialRepository = (MockCredentialRepository = new Mock>()).Object; + EntitiesContext = (MockEntitiesContext = new Mock()).Object; Auditing = new TestAuditingService(); // Set ConfirmEmailAddress to a default of true MockConfig.Setup(c => c.ConfirmEmailAddresses).Returns(true); + + MockDatabase = new Mock(); + MockEntitiesContext.Setup(c => c.GetDatabase()).Returns(MockDatabase.Object); } } @@ -254,7 +260,8 @@ private Mock SetupEntitiesContext() { var mockContext = new Mock(); var dbContext = new Mock(); - mockContext.Setup(m => m.GetDatabase()).Returns(dbContext.Object.Database); + mockContext.Setup(m => m.GetDatabase()) + .Returns(new DatabaseWrapper(dbContext.Object.Database)); return mockContext; } From 930abd4b74f5774a6c36ec3a8eb6fefeb3676d97 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Wed, 3 Jan 2018 09:30:21 -0800 Subject: [PATCH 2/8] Tests and cleanup --- .../Controllers/UsersController.cs | 4 +- .../AddMigrateToOrganization.Up.sql | 58 +++--- src/NuGetGallery/Services/UserService.cs | 21 +- src/NuGetGallery/Web.config | 1 + .../Controllers/UsersControllerFacts.cs | 186 ++++++++++++++++++ .../Services/UserServiceFacts.cs | 11 +- 6 files changed, 233 insertions(+), 48 deletions(-) diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index abe43cb5c1..f366452be1 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -136,9 +136,9 @@ public virtual async Task ConfirmTransform(string accountName, str // todo: redirect to ManageOrganization (future work) return RedirectToAction("Account"); } - catch (Exception e) + catch (TransformAccountException e) { - TempData["TransformError"] = e.GetUserSafeMessage(); + TempData["TransformError"] = e.AsUserSafeException().GetUserSafeMessage(); return View("AccountTransformFailed"); } } diff --git a/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql b/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql index 31d67e171b..ca589289cc 100644 --- a/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql +++ b/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql @@ -4,46 +4,44 @@ SET ANSI_NULLS ON IF OBJECT_ID('[dbo].[MigrateToOrganization]', 'P') IS NOT NULL - DROP PROCEDURE [dbo].[MigrateToOrganization] + DROP PROCEDURE [dbo].[MigrateToOrganization] GO CREATE PROCEDURE [dbo].[MigrateToOrganization] ( - @orgKey INT, - @adminKey INT, - @token NVARCHAR(MAX) + @orgKey INT, + @adminKey INT, + @token NVARCHAR(MAX) ) AS BEGIN - DECLARE @reqCount INT + DECLARE @reqCount INT - -- Ensure migration request exists - SELECT @reqCount = COUNT(*) - FROM [dbo].[OrganizationMigrationRequests] - WHERE NewOrganizationKey = @orgKey - AND AdminUserKey = @adminKey - AND ConfirmationToken = @token - IF @reqCount = 0 RETURN (0) + -- Ensure migration request exists + SELECT @reqCount = COUNT(*) + FROM [dbo].[OrganizationMigrationRequests] + WHERE NewOrganizationKey = @orgKey + AND AdminUserKey = @adminKey + AND ConfirmationToken = @token + IF @reqCount = 0 RETURN - BEGIN TRANSACTION - BEGIN TRY - -- Ensure Organizations do not have credentials or memberships - DELETE FROM [dbo].[Credentials] WHERE UserKey = @orgKey - DELETE FROM [dbo].[Memberships] WHERE MemberKey = @orgKey - DELETE FROM [dbo].[MembershipRequests] WHERE NewMemberKey = @orgKey + BEGIN TRANSACTION + BEGIN TRY + -- Ensure Organizations do not have credentials or memberships + DELETE FROM [dbo].[Credentials] WHERE UserKey = @orgKey + DELETE FROM [dbo].[Memberships] WHERE MemberKey = @orgKey + DELETE FROM [dbo].[MembershipRequests] WHERE NewMemberKey = @orgKey - -- Change to Organization account with single admin membership - INSERT INTO [dbo].[Organizations] ([Key]) VALUES (@orgKey) - INSERT INTO [dbo].[Memberships] (OrganizationKey, MemberKey, IsAdmin) VALUES (@orgKey, @adminKey, 1) + -- Change to Organization account with single admin membership + INSERT INTO [dbo].[Organizations] ([Key]) VALUES (@orgKey) + INSERT INTO [dbo].[Memberships] (OrganizationKey, MemberKey, IsAdmin) VALUES (@orgKey, @adminKey, 1) - -- Delete the migration request - DELETE FROM [dbo].[OrganizationMigrationRequests] WHERE NewOrganizationKey = @orgKey + -- Delete the migration request + DELETE FROM [dbo].[OrganizationMigrationRequests] WHERE NewOrganizationKey = @orgKey - COMMIT TRANSACTION; - RETURN (1) - END TRY - BEGIN CATCH - ROLLBACK TRANSACTION - RETURN (0) - END CATCH + COMMIT TRANSACTION; + END TRY + BEGIN CATCH + ROLLBACK TRANSACTION + END CATCH END \ No newline at end of file diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index 8b3eacca8b..119cd5cc7e 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -3,20 +3,21 @@ using System; using System.Collections.Generic; +using System.Data; using System.Data.Entity; +using System.Data.SqlClient; using System.Linq; -using NuGetGallery.Configuration; -using NuGetGallery.Auditing; using System.Threading.Tasks; -using NuGetGallery.Security; +using NuGetGallery.Auditing; +using NuGetGallery.Configuration; using Crypto = NuGetGallery.CryptographyService; -using System.Data.SqlClient; -using System.Data; namespace NuGetGallery { public class UserService : IUserService { + private const string ExecMigrateToOrganization = "EXEC @result = [dbo].[MigrateToOrganization] @orgKey, @adminKey, @token"; + public IAppConfiguration Config { get; protected set; } public IEntityRepository UserRepository { get; protected set; } public IEntityRepository CredentialRepository { get; protected set; } @@ -156,8 +157,6 @@ public async Task ConfirmEmailAddress(User user, string token) await UserRepository.CommitChangesAsync(); return true; } - - private const string ExecMigrateToOrganization = "EXEC [dbo].[MigrateToOrganization] @orgKey @adminKey @token"; public async Task TransformToOrganizationAccount(User accountToTransform, User adminUser, string token) { @@ -172,7 +171,7 @@ public async Task TransformToOrganizationAccount(User accountToTransform, User a var tenantId = adminUser.GetTenantId(); if (string.IsNullOrWhiteSpace(tenantId)) { - // todo: add security policy to organization to enforce this (future work) + // todo: add security policy to organization below to enforce this (future work, with manage organization) throw new TransformAccountException(Strings.TransformAccount_AdminDoesNotHaveTenantId); } @@ -183,12 +182,14 @@ public async Task TransformToOrganizationAccount(User accountToTransform, User a var database = EntitiesContext.GetDatabase(); var result = await database.ExecuteSqlCommandAsync( ExecMigrateToOrganization, - new SqlParameter("organizationKey", accountToTransform.Key), + new SqlParameter("orgKey", accountToTransform.Key), new SqlParameter("adminKey", adminUser.Key), new SqlParameter("token", token) ); - if (result == 0) + // For ExecuteSqlCommandAsync result, see SqlDataReader.RecordsAffected. + // Result was -1 (found no migration requests with select) or 0 (no insert, update or delete). + if (result <= 0) { // Stored procedure returned failure, probably due to an unsatisfied migration request. throw new TransformAccountException(Strings.TransformAccount_SaveFailed); diff --git a/src/NuGetGallery/Web.config b/src/NuGetGallery/Web.config index ec450e6e8b..88c0f3edfc 100644 --- a/src/NuGetGallery/Web.config +++ b/src/NuGetGallery/Web.config @@ -41,6 +41,7 @@ + diff --git a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs index 2bcfd80225..c0f7eab2d9 100644 --- a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Mail; @@ -13,6 +14,7 @@ using NuGetGallery.Areas.Admin.Models; using NuGetGallery.Areas.Admin.ViewModels; using NuGetGallery.Authentication; +using NuGetGallery.Configuration; using NuGetGallery.Framework; using NuGetGallery.Infrastructure.Authentication; using Xunit; @@ -2229,6 +2231,190 @@ public async Task RequestDeleteAccountAsync(bool successOnSentRequest) Assert.Equal(!successOnSentRequest, tempData); } } + + public class TheConfirmTransformAction : TestContainer + { + [Fact] + public async Task WhenAdminIsNotConfirmed_ShowsError() + { + // Arrange + var controller = GetController(); + var currentUser = new User() { UnconfirmedEmailAddress = "unconfirmed@example.com" }; + controller.SetCurrentUser(currentUser); + + // Act + var result = await controller.ConfirmTransform("account", "token"); + + // Assert + Assert.NotNull(result); + Assert.Equal(Strings.TransformAccount_AdminNotConfirmed, controller.TempData["TransformError"]); + } + + [Fact] + public async Task WhenAccountToTransformIsNotFound_ShowsError() + { + // Arrange + var controller = GetController(); + var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; + controller.SetCurrentUser(currentUser); + + // Act + var result = await controller.ConfirmTransform("account", "token"); + + // Assert + Assert.NotNull(result); + Assert.Equal( + String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountNotFound, "account"), + controller.TempData["TransformError"]); + } + + [Fact] + public async Task WhenAccountToTransformIsNotConfirmed_ShowsError() + { + // Arrange + var configurationService = GetConfigurationService(); + configurationService.Current.OrganizationsEnabledForDomains = new string[] { "example.com" }; + + var controller = GetController(); + var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; + controller.SetCurrentUser(currentUser); + + GetMock() + .Setup(u => u.FindByUsername("account")) + .Returns(new User("account") + { + UnconfirmedEmailAddress = "unconfirmed@example.com" + }); + + // Act + var result = await controller.ConfirmTransform("account", "token"); + + // Assert + Assert.NotNull(result); + Assert.Equal( + String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountNotSupported, "account"), + controller.TempData["TransformError"]); + } + + [Fact] + public async Task WhenAccountToTransformIsAdmin_ShowsError() + { + // Arrange + var configurationService = GetConfigurationService(); + configurationService.Current.OrganizationsEnabledForDomains = new string[] { "example.com" }; + + var controller = GetController(); + var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; + controller.SetCurrentUser(currentUser); + + GetMock() + .Setup(u => u.FindByUsername("account")) + .Returns(new User("account") + { + EmailAddress = "account@example.com", + Roles = { + new Role() { Name = "Admins" } + } + }); + + // Act + var result = await controller.ConfirmTransform("account", "token"); + + // Assert + Assert.NotNull(result); + Assert.Equal( + String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountNotSupported, "account"), + controller.TempData["TransformError"]); + } + + [Fact] + public async Task WhenAccountToTransformIsNotInDomainWhitelist_ShowsError() + { + // Arrange + var configurationService = GetConfigurationService(); + configurationService.Current.OrganizationsEnabledForDomains = new string[] { "not_example.com" }; + + var controller = GetController(); + var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; + controller.SetCurrentUser(currentUser); + + GetMock() + .Setup(u => u.FindByUsername("account")) + .Returns(new User("account") + { + EmailAddress = "account@example.com" + }); + + // Act + var result = await controller.ConfirmTransform("account", "token"); + + // Assert + Assert.NotNull(result); + Assert.Equal( + String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountNotSupported, "account"), + controller.TempData["TransformError"]); + } + + [Fact] + public async Task WhenUserServiceThrowsException_ShowsError() + { + // Arrange + var configurationService = GetConfigurationService(); + configurationService.Current.OrganizationsEnabledForDomains = new string[] { "example.com" }; + + var controller = GetController(); + var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; + controller.SetCurrentUser(currentUser); + + GetMock() + .Setup(u => u.FindByUsername("account")) + .Returns(new User("account") + { + EmailAddress = "account@example.com" + }); + + GetMock() + .Setup(s => s.TransformToOrganizationAccount(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new TransformAccountException("Transform Failed!")); + + // Act + var result = await controller.ConfirmTransform("account", "token"); + + // Assert + Assert.NotNull(result); + Assert.Equal("Transform Failed!", controller.TempData["TransformError"]); + } + + [Fact] + public async Task WhenUserServiceReturnsSuccess_Redirects() + { + // Arrange + var configurationService = GetConfigurationService(); + configurationService.Current.OrganizationsEnabledForDomains = new string[] { "example.com" }; + + var controller = GetController(); + var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; + controller.SetCurrentUser(currentUser); + + GetMock() + .Setup(u => u.FindByUsername("account")) + .Returns(new User("account") + { + EmailAddress = "account@example.com" + }); + + GetMock() + .Setup(s => s.TransformToOrganizationAccount(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await controller.ConfirmTransform("account", "token"); + + // Assert + Assert.NotNull(result); + Assert.False(controller.TempData.ContainsKey("TransformError")); + } + } } } diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index d8b2064953..8f22c75ae7 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -2,16 +2,15 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Data; using System.Linq; using System.Threading.Tasks; -using NuGetGallery.Framework; -using NuGetGallery.Auditing; -using Xunit; -using NuGetGallery.TestUtils; using Moq; -using System.Data.SqlClient; -using System.Data; +using NuGetGallery.Auditing; +using NuGetGallery.Framework; using NuGetGallery.Infrastructure.Authentication; +using NuGetGallery.TestUtils; +using Xunit; namespace NuGetGallery { From 552e728698f0d7843041fd41682c3443179e64c8 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Wed, 3 Jan 2018 14:20:03 -0800 Subject: [PATCH 3/8] Fix query --- src/NuGetGallery/Services/UserService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index 119cd5cc7e..fd23bf1fae 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -16,7 +16,7 @@ namespace NuGetGallery { public class UserService : IUserService { - private const string ExecMigrateToOrganization = "EXEC @result = [dbo].[MigrateToOrganization] @orgKey, @adminKey, @token"; + private const string ExecMigrateToOrganization = "EXEC [dbo].[MigrateToOrganization] @orgKey, @adminKey, @token"; public IAppConfiguration Config { get; protected set; } public IEntityRepository UserRepository { get; protected set; } From c8978bb02758eacbd640e2a646a250b62b2532d7 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Wed, 3 Jan 2018 15:24:15 -0800 Subject: [PATCH 4/8] PR feedback --- src/NuGetGallery/App_Start/Routes.cs | 2 +- .../Controllers/UsersController.cs | 12 +++---- .../AddMigrateToOrganization.Up.sql | 21 ++++++++----- src/NuGetGallery/Services/UserService.cs | 15 +++++---- src/NuGetGallery/Strings.Designer.cs | 15 ++------- src/NuGetGallery/Strings.resx | 9 ++---- .../Controllers/UsersControllerFacts.cs | 31 +++++++++++++++++++ .../Services/UserServiceFacts.cs | 2 +- 8 files changed, 67 insertions(+), 40 deletions(-) diff --git a/src/NuGetGallery/App_Start/Routes.cs b/src/NuGetGallery/App_Start/Routes.cs index 6afec3868d..a98c889bc7 100644 --- a/src/NuGetGallery/App_Start/Routes.cs +++ b/src/NuGetGallery/App_Start/Routes.cs @@ -291,7 +291,7 @@ public static void RegisterUIRoutes(RouteCollection routes) routes.MapRoute( RouteName.TransformAccountConfirmation, - "account/transform/confirm/{accountName}/{token}", + "account/transform/confirm/{accountNameToTransform}/{token}", new { controller = "Users", action = "ConfirmTransform" }); routes.MapRoute( diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index f366452be1..9d16f7c7bc 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -102,7 +102,7 @@ public virtual ActionResult Account() [HttpGet] [Authorize] - public virtual async Task ConfirmTransform(string accountName, string token) + public virtual async Task ConfirmTransform(string accountNameToTransform, string token) { var adminUser = GetCurrentUser(); if (!adminUser.Confirmed) @@ -111,18 +111,18 @@ public virtual async Task ConfirmTransform(string accountName, str return RedirectToAction("ConfirmationRequired"); } - var accountToTransform = _userService.FindByUsername(accountName); + var accountToTransform = _userService.FindByUsername(accountNameToTransform); if (accountToTransform == null) { TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture, - Strings.TransformAccount_OrganizationAccountNotFound, accountName); + Strings.TransformAccount_OrganizationAccountNotFound, accountNameToTransform); return View("AccountTransformFailed"); } if (!CanTransformIntoOrganization(accountToTransform)) { TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture, - Strings.TransformAccount_OrganizationAccountNotSupported, accountName); + Strings.TransformAccount_OrganizationAccountNotSupported, accountNameToTransform); return View("AccountTransformFailed"); } @@ -131,7 +131,7 @@ public virtual async Task ConfirmTransform(string accountName, str await _userService.TransformToOrganizationAccount(accountToTransform, adminUser, token); TempData["Message"] = String.Format(CultureInfo.CurrentCulture, - Strings.TransformAccount_Success, accountName); + Strings.TransformAccount_Success, accountNameToTransform); // todo: redirect to ManageOrganization (future work) return RedirectToAction("Account"); @@ -145,7 +145,7 @@ public virtual async Task ConfirmTransform(string accountName, str private bool CanTransformIntoOrganization(User user) { - if (!user.Confirmed || user.IsAdministrator()) + if (!user.Confirmed || user is Organization || user.IsAdministrator()) { return false; } diff --git a/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql b/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql index ca589289cc..4d4ff97804 100644 --- a/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql +++ b/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql @@ -15,26 +15,31 @@ CREATE PROCEDURE [dbo].[MigrateToOrganization] ) AS BEGIN - DECLARE @reqCount INT + DECLARE @count INT -- Ensure migration request exists - SELECT @reqCount = COUNT(*) + SELECT @count = COUNT(*) FROM [dbo].[OrganizationMigrationRequests] WHERE NewOrganizationKey = @orgKey AND AdminUserKey = @adminKey AND ConfirmationToken = @token - IF @reqCount = 0 RETURN + IF @count = 0 RETURN + + -- Ensure account is not member of other organizations + SELECT @count = COUNT(*) FROM [dbo].[Memberships] WHERE MemberKey = @orgKey + IF @count > 0 RETURN + + SELECT @count = COUNT(*) FROM [dbo].[MembershipRequests] WHERE NewMemberKey = @orgKey + IF @count > 0 RETURN BEGIN TRANSACTION BEGIN TRY - -- Ensure Organizations do not have credentials or memberships - DELETE FROM [dbo].[Credentials] WHERE UserKey = @orgKey - DELETE FROM [dbo].[Memberships] WHERE MemberKey = @orgKey - DELETE FROM [dbo].[MembershipRequests] WHERE NewMemberKey = @orgKey - -- Change to Organization account with single admin membership INSERT INTO [dbo].[Organizations] ([Key]) VALUES (@orgKey) INSERT INTO [dbo].[Memberships] (OrganizationKey, MemberKey, IsAdmin) VALUES (@orgKey, @adminKey, 1) + + -- Remove organization credentials + DELETE FROM [dbo].[Credentials] WHERE UserKey = @orgKey -- Delete the migration request DELETE FROM [dbo].[OrganizationMigrationRequests] WHERE NewOrganizationKey = @orgKey diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index fd23bf1fae..f0be587a4f 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -157,7 +157,12 @@ public async Task ConfirmEmailAddress(User user, string token) await UserRepository.CommitChangesAsync(); return true; } - + + /// + /// Transforms a account into an account. Note that this must be done + /// with a stored procedure because EF does not support changing inheritance types. The change will take effect on + /// new EF contexts created after the transaction is committed (i.e., future requests). + /// public async Task TransformToOrganizationAccount(User accountToTransform, User adminUser, string token) { accountToTransform = accountToTransform ?? throw new ArgumentNullException(nameof(accountToTransform)); @@ -174,9 +179,7 @@ public async Task TransformToOrganizationAccount(User accountToTransform, User a // todo: add security policy to organization below to enforce this (future work, with manage organization) throw new TransformAccountException(Strings.TransformAccount_AdminDoesNotHaveTenantId); } - - // Update from User to Organization account. Note that the type change will only be reflected in future - // requests, which use new EF context instances. + try { var database = EntitiesContext.GetDatabase(); @@ -191,8 +194,8 @@ public async Task TransformToOrganizationAccount(User accountToTransform, User a // Result was -1 (found no migration requests with select) or 0 (no insert, update or delete). if (result <= 0) { - // Stored procedure returned failure, probably due to an unsatisfied migration request. - throw new TransformAccountException(Strings.TransformAccount_SaveFailed); + // Stored procedure check failed (i.e., migration request didn't match, or membership existed). + throw new TransformAccountException(Strings.TransformAccount_DatabaseError); } } catch (Exception ex) when (ex is SqlException || ex is DataException) diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index 2c7f530659..7fb7107d7a 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -1408,7 +1408,7 @@ public static string TokenExpirationShouldGiveUser1MinuteToChangePassword { } /// - /// Looks up a localized string similar to Failed to transform the account because you do not have an Azure AD sign-in with tenant ID.. + /// Looks up a localized string similar to Failed to transform the account because you do not have an Azure AD sign-in with tenant ID. Contact support@nuget.org for more details.. /// public static string TransformAccount_AdminDoesNotHaveTenantId { get { @@ -1426,7 +1426,7 @@ public static string TransformAccount_AdminNotConfirmed { } /// - /// Looks up a localized string similar to Failed to transform the account due to a database error.. + /// Looks up a localized string similar to Failed to transform the account due to an unexpected error. Contact support@nuget.org for more details.. /// public static string TransformAccount_DatabaseError { get { @@ -1444,7 +1444,7 @@ public static string TransformAccount_OrganizationAccountNotFound { } /// - /// Looks up a localized string similar to Account '{0}' cannot be transformed into an organization. Contact support for more information.. + /// Looks up a localized string similar to Account '{0}' cannot be transformed into an organization. Contact support@nuget.org for more details.. /// public static string TransformAccount_OrganizationAccountNotSupported { get { @@ -1452,15 +1452,6 @@ public static string TransformAccount_OrganizationAccountNotSupported { } } - /// - /// Looks up a localized string similar to Failed to transform the account.. - /// - public static string TransformAccount_SaveFailed { - get { - return ResourceManager.GetString("TransformAccount_SaveFailed", resourceCulture); - } - } - /// /// Looks up a localized string similar to Account '{0}' was successfully transformed into an organization.. /// diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index ad0403ae12..cdfbcc7e45 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -689,22 +689,19 @@ For more information, please contact '{2}'. Package '{0}' has been locked. This means you cannot publish a new version or change the listing status of a published package version. Please contact support@nuget.org. - Failed to transform the account because you do not have an Azure AD sign-in with tenant ID. + Failed to transform the account because you do not have an Azure AD sign-in with tenant ID. Contact support@nuget.org for more details. Your account must be confirmed before you can create an organization. - Failed to transform the account due to a database error. + Failed to transform the account due to an unexpected error. Contact support@nuget.org for more details. Failed to tranform account '{0}' because it was not found. - Account '{0}' cannot be transformed into an organization. Contact support for more information. - - - Failed to transform the account. + Account '{0}' cannot be transformed into an organization. Contact support@nuget.org for more details. Account '{0}' was successfully transformed into an organization. diff --git a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs index c0f7eab2d9..92be79b2fb 100644 --- a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs @@ -2296,6 +2296,37 @@ public async Task WhenAccountToTransformIsNotConfirmed_ShowsError() controller.TempData["TransformError"]); } + [Fact] + public async Task WhenAccountToTransformIsAlreadyOrganization_ShowsError() + { + // Arrange + var configurationService = GetConfigurationService(); + configurationService.Current.OrganizationsEnabledForDomains = new string[] { "example.com" }; + + var controller = GetController(); + var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; + controller.SetCurrentUser(currentUser); + + GetMock() + .Setup(u => u.FindByUsername("account")) + .Returns(new Organization("account") + { + EmailAddress = "account@example.com", + Roles = { + new Role() { Name = "Admins" } + } + }); + + // Act + var result = await controller.ConfirmTransform("account", "token"); + + // Assert + Assert.NotNull(result); + Assert.Equal( + String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountNotSupported, "account"), + controller.TempData["TransformError"]); + } + [Fact] public async Task WhenAccountToTransformIsAdmin_ShowsError() { diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 8f22c75ae7..b5a811962a 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -451,7 +451,7 @@ public async Task WhenSqlResultIsZero_ThrowsTransformAccountException() // Act & Assert var exception = await Assert.ThrowsAsync( async () => await service.TransformToOrganizationAccount(account, admin, "token")); - Assert.Equal(exception.Message, Strings.TransformAccount_SaveFailed); + Assert.Equal(exception.Message, Strings.TransformAccount_DatabaseError); } [Fact] From 8cba4dd7bb46b8c3e7c077d74c724f39f3077010 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Thu, 4 Jan 2018 15:00:04 -0800 Subject: [PATCH 5/8] PR feedback --- .../Entities/DatabaseWrapper.cs | 28 +++- src/NuGetGallery.Core/Entities/IDatabase.cs | 4 +- src/NuGetGallery.Core/Entities/User.cs | 1 + .../Extensions/EntitiesContextExtensions.cs | 40 +++++ .../MigrateUserToOrganization.sql | 34 ++++ .../NuGetGallery.Core.csproj | 4 + .../Controllers/UsersController.cs | 41 ++--- .../AddMigrateToOrganization.Down.sql | 8 - .../AddMigrateToOrganization.Up.sql | 52 ------ ..._AddMigrateToOrganizationSproc.Designer.cs | 29 ---- ...220037446_AddMigrateToOrganizationSproc.cs | 16 -- ...0037446_AddMigrateToOrganizationSproc.resx | 126 -------------- src/NuGetGallery/NuGetGallery.csproj | 9 - src/NuGetGallery/Services/IUserService.cs | 4 +- src/NuGetGallery/Services/UserService.cs | 60 +++---- src/NuGetGallery/Strings.Designer.cs | 57 +++++-- src/NuGetGallery/Strings.resx | 31 ++-- .../Controllers/UsersControllerFacts.cs | 158 ++++-------------- .../Services/UserServiceFacts.cs | 126 ++++++++------ .../TestUtils/ContractAssert.cs | 9 + 20 files changed, 326 insertions(+), 511 deletions(-) create mode 100644 src/NuGetGallery.Core/Extensions/EntitiesContextExtensions.cs create mode 100644 src/NuGetGallery.Core/Infrastructure/MigrateUserToOrganization.sql delete mode 100644 src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Down.sql delete mode 100644 src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql delete mode 100644 src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.Designer.cs delete mode 100644 src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.cs delete mode 100644 src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.resx diff --git a/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs b/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs index 16f70cb1f4..ce78aa8987 100644 --- a/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs +++ b/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs @@ -3,6 +3,8 @@ using System; using System.Data.Entity; +using System.IO; +using System.Reflection; using System.Threading.Tasks; namespace NuGetGallery @@ -15,7 +17,7 @@ public DatabaseWrapper(Database database) { _database = database ?? throw new ArgumentNullException(nameof(database)); } - + public Task ExecuteSqlCommandAsync(string sql, params object[] parameters) { return _database.ExecuteSqlCommandAsync(sql, parameters); @@ -25,5 +27,29 @@ public DbContextTransaction BeginTransaction() { return _database.BeginTransaction(); } + + /// + /// Execute an embedded resource SQL script. + /// + /// Resource name + /// SQL parameters + /// Resulting + public async Task ExecuteSqlResourceAsync(string name, params object[] parameters) + { + string sqlCommand; + + var assembly = Assembly.GetExecutingAssembly(); + using (var reader = new StreamReader(assembly.GetManifestResourceStream(name))) + { + sqlCommand = await reader.ReadToEndAsync(); + } + + if (!string.IsNullOrEmpty(sqlCommand)) + { + return await ExecuteSqlCommandAsync(sqlCommand, parameters); + } + + return 0; // no records affected + } } } diff --git a/src/NuGetGallery.Core/Entities/IDatabase.cs b/src/NuGetGallery.Core/Entities/IDatabase.cs index 6dad8a0d98..f541b070e1 100644 --- a/src/NuGetGallery.Core/Entities/IDatabase.cs +++ b/src/NuGetGallery.Core/Entities/IDatabase.cs @@ -8,8 +8,10 @@ namespace NuGetGallery { public interface IDatabase { + DbContextTransaction BeginTransaction(); + Task ExecuteSqlCommandAsync(string sql, params object[] parameters); - DbContextTransaction BeginTransaction(); + Task ExecuteSqlResourceAsync(string name, params object[] parameters); } } diff --git a/src/NuGetGallery.Core/Entities/User.cs b/src/NuGetGallery.Core/Entities/User.cs index f16c70cfaa..b4f7ded6c5 100644 --- a/src/NuGetGallery.Core/Entities/User.cs +++ b/src/NuGetGallery.Core/Entities/User.cs @@ -27,6 +27,7 @@ public User(string username) SecurityPolicies = new List(); ReservedNamespaces = new HashSet(); Organizations = new List(); + OrganizationRequests = new List(); Roles = new List(); Username = username; } diff --git a/src/NuGetGallery.Core/Extensions/EntitiesContextExtensions.cs b/src/NuGetGallery.Core/Extensions/EntitiesContextExtensions.cs new file mode 100644 index 0000000000..4f38932bb0 --- /dev/null +++ b/src/NuGetGallery.Core/Extensions/EntitiesContextExtensions.cs @@ -0,0 +1,40 @@ +// 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.Data.SqlClient; +using System.Threading.Tasks; + +namespace NuGetGallery +{ + public static class EntitiesContextExtensions + { + public static async Task 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; + } + + private static class MigrateUserToOrganization + { + public const string ResourceName = "NuGetGallery.Infrastructure.MigrateUserToOrganization.sql"; + public const string OrganizationKey = "organizationKey"; + public const string AdminKey = "adminKey"; + public const string ConfirmationToken = "token"; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery.Core/Infrastructure/MigrateUserToOrganization.sql b/src/NuGetGallery.Core/Infrastructure/MigrateUserToOrganization.sql new file mode 100644 index 0000000000..841f72cc58 --- /dev/null +++ b/src/NuGetGallery.Core/Infrastructure/MigrateUserToOrganization.sql @@ -0,0 +1,34 @@ +-- 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. + +SET ANSI_NULLS ON + +-- Transform User into Organization account. Must be done with inline SQL because EF does not support changing +-- types for entities that use inheritance. + +DECLARE @requestCount INT + +SELECT @requestCount = COUNT(*) +FROM [dbo].[OrganizationMigrationRequests] +WHERE NewOrganizationKey = @organizationKey + AND AdminUserKey = @adminKey + AND ConfirmationToken = @token + +IF @requestCount > 0 +BEGIN TRANSACTION +BEGIN TRY + -- Change to Organization account with single admin membership + INSERT INTO [dbo].[Organizations] ([Key]) VALUES (@organizationKey) + INSERT INTO [dbo].[Memberships] (OrganizationKey, MemberKey, IsAdmin) VALUES (@organizationKey, @adminKey, 1) + + -- Remove organization credentials + DELETE FROM [dbo].[Credentials] WHERE UserKey = @organizationKey + + -- Delete the migration request + DELETE FROM [dbo].[OrganizationMigrationRequests] WHERE NewOrganizationKey = @organizationKey + + COMMIT TRANSACTION; +END TRY +BEGIN CATCH + ROLLBACK TRANSACTION +END CATCH \ No newline at end of file diff --git a/src/NuGetGallery.Core/NuGetGallery.Core.csproj b/src/NuGetGallery.Core/NuGetGallery.Core.csproj index 05a66d48c6..3c19549e09 100644 --- a/src/NuGetGallery.Core/NuGetGallery.Core.csproj +++ b/src/NuGetGallery.Core/NuGetGallery.Core.csproj @@ -210,6 +210,7 @@ + @@ -270,6 +271,9 @@ Designer
+ + + ..\..\build diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index 9d16f7c7bc..af8a588498 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -107,7 +107,7 @@ public virtual async Task ConfirmTransform(string accountNameToTra var adminUser = GetCurrentUser(); if (!adminUser.Confirmed) { - TempData["TransformError"] = Strings.TransformAccount_AdminNotConfirmed; + TempData["TransformError"] = Strings.TransformAccount_NotConfirmed; return RedirectToAction("ConfirmationRequired"); } @@ -115,43 +115,30 @@ public virtual async Task ConfirmTransform(string accountNameToTra if (accountToTransform == null) { TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture, - Strings.TransformAccount_OrganizationAccountNotFound, accountNameToTransform); + Strings.TransformAccount_OrganizationAccountDoesNotExist, accountNameToTransform); return View("AccountTransformFailed"); } - if (!CanTransformIntoOrganization(accountToTransform)) + string errorReason; + if (!_userService.CanTransformUserToOrganization(accountToTransform, out errorReason)) { TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture, - Strings.TransformAccount_OrganizationAccountNotSupported, accountNameToTransform); + Strings.TransformAccount_FailedWithReason, accountNameToTransform, errorReason); return View("AccountTransformFailed"); } - - try - { - await _userService.TransformToOrganizationAccount(accountToTransform, adminUser, token); - - TempData["Message"] = String.Format(CultureInfo.CurrentCulture, - Strings.TransformAccount_Success, accountNameToTransform); - - // todo: redirect to ManageOrganization (future work) - return RedirectToAction("Account"); - } - catch (TransformAccountException e) + + if (!await _userService.TransformUserToOrganization(accountToTransform, adminUser, token)) { - TempData["TransformError"] = e.AsUserSafeException().GetUserSafeMessage(); + TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_Failed, accountNameToTransform); return View("AccountTransformFailed"); } - } - - private bool CanTransformIntoOrganization(User user) - { - if (!user.Confirmed || user is Organization || user.IsAdministrator()) - { - return false; - } + + TempData["Message"] = String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_Success, accountNameToTransform); - var userDomain = user.ToMailAddress().Host; - return _config.OrganizationsEnabledForDomains.Contains(userDomain, StringComparer.OrdinalIgnoreCase); + // todo: redirect to ManageOrganization (future work) + return RedirectToAction("Account"); } [HttpGet] diff --git a/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Down.sql b/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Down.sql deleted file mode 100644 index 23a742e544..0000000000 --- a/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Down.sql +++ /dev/null @@ -1,8 +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. - -SET ANSI_NULLS ON - -IF OBJECT_ID('[dbo].[MigrateToOrganization]', 'P') IS NOT NULL - DROP PROCEDURE [dbo].[MigrateToOrganization] -GO \ No newline at end of file diff --git a/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql b/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql deleted file mode 100644 index 4d4ff97804..0000000000 --- a/src/NuGetGallery/Infrastructure/AddMigrateToOrganization.Up.sql +++ /dev/null @@ -1,52 +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. - -SET ANSI_NULLS ON - -IF OBJECT_ID('[dbo].[MigrateToOrganization]', 'P') IS NOT NULL - DROP PROCEDURE [dbo].[MigrateToOrganization] -GO - -CREATE PROCEDURE [dbo].[MigrateToOrganization] -( - @orgKey INT, - @adminKey INT, - @token NVARCHAR(MAX) -) -AS -BEGIN - DECLARE @count INT - - -- Ensure migration request exists - SELECT @count = COUNT(*) - FROM [dbo].[OrganizationMigrationRequests] - WHERE NewOrganizationKey = @orgKey - AND AdminUserKey = @adminKey - AND ConfirmationToken = @token - IF @count = 0 RETURN - - -- Ensure account is not member of other organizations - SELECT @count = COUNT(*) FROM [dbo].[Memberships] WHERE MemberKey = @orgKey - IF @count > 0 RETURN - - SELECT @count = COUNT(*) FROM [dbo].[MembershipRequests] WHERE NewMemberKey = @orgKey - IF @count > 0 RETURN - - BEGIN TRANSACTION - BEGIN TRY - -- Change to Organization account with single admin membership - INSERT INTO [dbo].[Organizations] ([Key]) VALUES (@orgKey) - INSERT INTO [dbo].[Memberships] (OrganizationKey, MemberKey, IsAdmin) VALUES (@orgKey, @adminKey, 1) - - -- Remove organization credentials - DELETE FROM [dbo].[Credentials] WHERE UserKey = @orgKey - - -- Delete the migration request - DELETE FROM [dbo].[OrganizationMigrationRequests] WHERE NewOrganizationKey = @orgKey - - COMMIT TRANSACTION; - END TRY - BEGIN CATCH - ROLLBACK TRANSACTION - END CATCH -END \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.Designer.cs b/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.Designer.cs deleted file mode 100644 index 31872b8b16..0000000000 --- a/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.Designer.cs +++ /dev/null @@ -1,29 +0,0 @@ -// -namespace NuGetGallery.Migrations -{ - using System.CodeDom.Compiler; - using System.Data.Entity.Migrations; - using System.Data.Entity.Migrations.Infrastructure; - using System.Resources; - - [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] - public sealed partial class AddMigrateToOrganizationSproc : IMigrationMetadata - { - private readonly ResourceManager Resources = new ResourceManager(typeof(AddMigrateToOrganizationSproc)); - - string IMigrationMetadata.Id - { - get { return "201712220037446_AddMigrateToOrganizationSproc"; } - } - - string IMigrationMetadata.Source - { - get { return null; } - } - - string IMigrationMetadata.Target - { - get { return Resources.GetString("Target"); } - } - } -} diff --git a/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.cs b/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.cs deleted file mode 100644 index f998e0e293..0000000000 --- a/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.cs +++ /dev/null @@ -1,16 +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. - -namespace NuGetGallery.Migrations -{ - public partial class AddMigrateToOrganizationSproc : SqlResourceMigration - { - public AddMigrateToOrganizationSproc() - : base( - "NuGetGallery.Infrastructure.AddMigrateToOrganization.Up.sql", - "NuGetGallery.Infrastructure.AddMigrateToOrganization.Down.sql" - ) - { - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.resx b/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.resx deleted file mode 100644 index 7e139f3069..0000000000 --- a/src/NuGetGallery/Migrations/201712220037446_AddMigrateToOrganizationSproc.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 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 - - - dbo - - \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index 3f4ff8c58c..d14c3553ef 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -800,10 +800,6 @@ 201712211850074_MembershipRequests.cs - - - 201712220037446_AddMigrateToOrganizationSproc.cs - @@ -1725,8 +1721,6 @@ - - Always Designer @@ -1923,9 +1917,6 @@ 201712211850074_MembershipRequests.cs - - 201712220037446_AddMigrateToOrganizationSproc.cs - diff --git a/src/NuGetGallery/Services/IUserService.cs b/src/NuGetGallery/Services/IUserService.cs index e7751f2ea7..cee39c8c50 100644 --- a/src/NuGetGallery/Services/IUserService.cs +++ b/src/NuGetGallery/Services/IUserService.cs @@ -26,6 +26,8 @@ public interface IUserService Task> GetEmailAddressesForUserKeysAsync(IReadOnlyCollection distinctUserKeys); - Task TransformToOrganizationAccount(User newOrganization, User adminUser, string token); + bool CanTransformUserToOrganization(User accountToTransform, out string errorReason); + + Task TransformUserToOrganization(User accountToTransform, User adminUser, string token); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index f0be587a4f..ed501a7192 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -16,8 +16,6 @@ namespace NuGetGallery { public class UserService : IUserService { - private const string ExecMigrateToOrganization = "EXEC [dbo].[MigrateToOrganization] @orgKey, @adminKey, @token"; - public IAppConfiguration Config { get; protected set; } public IEntityRepository UserRepository { get; protected set; } public IEntityRepository CredentialRepository { get; protected set; } @@ -158,50 +156,42 @@ public async Task ConfirmEmailAddress(User user, string token) return true; } - /// - /// Transforms a account into an account. Note that this must be done - /// with a stored procedure because EF does not support changing inheritance types. The change will take effect on - /// new EF contexts created after the transaction is committed (i.e., future requests). - /// - public async Task TransformToOrganizationAccount(User accountToTransform, User adminUser, string token) + public bool CanTransformUserToOrganization(User accountToTransform, out string errorReason) { - accountToTransform = accountToTransform ?? throw new ArgumentNullException(nameof(accountToTransform)); - adminUser = adminUser?? throw new ArgumentNullException(nameof(adminUser)); - - if (string.IsNullOrWhiteSpace(token)) + errorReason = null; + + if (!accountToTransform.Confirmed) { - throw new ArgumentNullException(nameof(token)); + errorReason = Strings.TransformAccount_FailedReasonNotConfirmedUser; } - - var tenantId = adminUser.GetTenantId(); - if (string.IsNullOrWhiteSpace(tenantId)) + else if (accountToTransform is Organization) + { + errorReason = Strings.TransformAccount_FailedReasonIsOrganization; + } + else if (accountToTransform.Organizations.Any() || accountToTransform.OrganizationRequests.Any()) { - // todo: add security policy to organization below to enforce this (future work, with manage organization) - throw new TransformAccountException(Strings.TransformAccount_AdminDoesNotHaveTenantId); + errorReason = Strings.TransformAccount_FailedReasonHasMemberships; } + else if (!Config.OrganizationsEnabledForDomains.Contains(accountToTransform.ToMailAddress().Host, StringComparer.OrdinalIgnoreCase)) + { + errorReason = Strings.TransformAccount_FailedReasonNotInDomainWhitelist; + } + + return errorReason == null; + } + + public async Task TransformUserToOrganization(User accountToTransform, User adminUser, string token) + { + // todo: check for tenantId and add organization policy to enforce this (future work, with manage organization) try { - var database = EntitiesContext.GetDatabase(); - var result = await database.ExecuteSqlCommandAsync( - ExecMigrateToOrganization, - new SqlParameter("orgKey", accountToTransform.Key), - new SqlParameter("adminKey", adminUser.Key), - new SqlParameter("token", token) - ); - - // For ExecuteSqlCommandAsync result, see SqlDataReader.RecordsAffected. - // Result was -1 (found no migration requests with select) or 0 (no insert, update or delete). - if (result <= 0) - { - // Stored procedure check failed (i.e., migration request didn't match, or membership existed). - throw new TransformAccountException(Strings.TransformAccount_DatabaseError); - } + return await EntitiesContext.TransformUserToOrganization(accountToTransform, adminUser, token); } catch (Exception ex) when (ex is SqlException || ex is DataException) { - // EF exception when saving account transformation to the database. - throw new TransformAccountException(Strings.TransformAccount_DatabaseError); + // todo: log exception + return false; } } } diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index 7fb7107d7a..b3ad120f33 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -1408,47 +1408,74 @@ public static string TokenExpirationShouldGiveUser1MinuteToChangePassword { } /// - /// Looks up a localized string similar to Failed to transform the account because you do not have an Azure AD sign-in with tenant ID. Contact support@nuget.org for more details.. + /// Looks up a localized string similar to Failed to transform account '{0}'. Contact support@nuget.org for more details.. /// - public static string TransformAccount_AdminDoesNotHaveTenantId { + public static string TransformAccount_Failed { get { - return ResourceManager.GetString("TransformAccount_AdminDoesNotHaveTenantId", resourceCulture); + return ResourceManager.GetString("TransformAccount_Failed", resourceCulture); } } /// - /// Looks up a localized string similar to Your account must be confirmed before you can create an organization.. + /// Looks up a localized string similar to account should not belong to any organizations. /// - public static string TransformAccount_AdminNotConfirmed { + public static string TransformAccount_FailedReasonHasMemberships { get { - return ResourceManager.GetString("TransformAccount_AdminNotConfirmed", resourceCulture); + return ResourceManager.GetString("TransformAccount_FailedReasonHasMemberships", resourceCulture); } } /// - /// Looks up a localized string similar to Failed to transform the account due to an unexpected error. Contact support@nuget.org for more details.. + /// Looks up a localized string similar to account is already an organization. /// - public static string TransformAccount_DatabaseError { + public static string TransformAccount_FailedReasonIsOrganization { get { - return ResourceManager.GetString("TransformAccount_DatabaseError", resourceCulture); + return ResourceManager.GetString("TransformAccount_FailedReasonIsOrganization", resourceCulture); } } /// - /// Looks up a localized string similar to Failed to tranform account '{0}' because it was not found.. + /// Looks up a localized string similar to account should be a confirmed user. /// - public static string TransformAccount_OrganizationAccountNotFound { + public static string TransformAccount_FailedReasonNotConfirmedUser { get { - return ResourceManager.GetString("TransformAccount_OrganizationAccountNotFound", resourceCulture); + return ResourceManager.GetString("TransformAccount_FailedReasonNotConfirmedUser", resourceCulture); } } /// - /// Looks up a localized string similar to Account '{0}' cannot be transformed into an organization. Contact support@nuget.org for more details.. + /// Looks up a localized string similar to account does not support organizations. /// - public static string TransformAccount_OrganizationAccountNotSupported { + public static string TransformAccount_FailedReasonNotInDomainWhitelist { get { - return ResourceManager.GetString("TransformAccount_OrganizationAccountNotSupported", resourceCulture); + return ResourceManager.GetString("TransformAccount_FailedReasonNotInDomainWhitelist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to transform account '{0}' with reason '{1}'. Contact support@nuget.org for more details.. + /// + public static string TransformAccount_FailedWithReason { + get { + return ResourceManager.GetString("TransformAccount_FailedWithReason", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You must confirm the email address for this account in order to complete this request.. + /// + public static string TransformAccount_NotConfirmed { + get { + return ResourceManager.GetString("TransformAccount_NotConfirmed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Organization account '{0}' does not exist.. + /// + public static string TransformAccount_OrganizationAccountDoesNotExist { + get { + return ResourceManager.GetString("TransformAccount_OrganizationAccountDoesNotExist", resourceCulture); } } diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index cdfbcc7e45..0eeda7bf10 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -688,22 +688,31 @@ For more information, please contact '{2}'. Package '{0}' has been locked. This means you cannot publish a new version or change the listing status of a published package version. Please contact support@nuget.org. - - Failed to transform the account because you do not have an Azure AD sign-in with tenant ID. Contact support@nuget.org for more details. + + You must confirm the email address for this account in order to complete this request. - - Your account must be confirmed before you can create an organization. + + Failed to transform account '{0}'. Contact support@nuget.org for more details. - - Failed to transform the account due to an unexpected error. Contact support@nuget.org for more details. + + Organization account '{0}' does not exist. - - Failed to tranform account '{0}' because it was not found. - - - Account '{0}' cannot be transformed into an organization. Contact support@nuget.org for more details. + + Failed to transform account '{0}' with reason '{1}'. Contact support@nuget.org for more details. Account '{0}' was successfully transformed into an organization. + + account should not belong to any organizations + + + account should be a confirmed user + + + account does not support organizations + + + account is already an organization + \ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs index 92be79b2fb..8d0d6f42fe 100644 --- a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs @@ -2247,7 +2247,7 @@ public async Task WhenAdminIsNotConfirmed_ShowsError() // Assert Assert.NotNull(result); - Assert.Equal(Strings.TransformAccount_AdminNotConfirmed, controller.TempData["TransformError"]); + Assert.Equal(Strings.TransformAccount_NotConfirmed, controller.TempData["TransformError"]); } [Fact] @@ -2264,130 +2264,63 @@ public async Task WhenAccountToTransformIsNotFound_ShowsError() // Assert Assert.NotNull(result); Assert.Equal( - String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountNotFound, "account"), + String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountDoesNotExist, "account"), controller.TempData["TransformError"]); } [Fact] - public async Task WhenAccountToTransformIsNotConfirmed_ShowsError() + public async Task WhenCanTransformReturnsFalse_ShowsError() { // Arrange - var configurationService = GetConfigurationService(); - configurationService.Current.OrganizationsEnabledForDomains = new string[] { "example.com" }; - - var controller = GetController(); - var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; - controller.SetCurrentUser(currentUser); - - GetMock() - .Setup(u => u.FindByUsername("account")) - .Returns(new User("account") - { - UnconfirmedEmailAddress = "unconfirmed@example.com" - }); + var accountToTransform = "account"; + var controller = CreateController(accountToTransform, canTransformErrorReason: "error"); // Act - var result = await controller.ConfirmTransform("account", "token"); + var result = await controller.ConfirmTransform(accountToTransform, "token"); // Assert Assert.NotNull(result); Assert.Equal( - String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountNotSupported, "account"), + String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_FailedWithReason, "account", + "error"), controller.TempData["TransformError"]); } [Fact] - public async Task WhenAccountToTransformIsAlreadyOrganization_ShowsError() + public async Task WhenUserServiceReturnsFalse_ShowsError() { // Arrange - var configurationService = GetConfigurationService(); - configurationService.Current.OrganizationsEnabledForDomains = new string[] { "example.com" }; - - var controller = GetController(); - var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; - controller.SetCurrentUser(currentUser); - - GetMock() - .Setup(u => u.FindByUsername("account")) - .Returns(new Organization("account") - { - EmailAddress = "account@example.com", - Roles = { - new Role() { Name = "Admins" } - } - }); + var accountToTransform = "account"; + var controller = CreateController(accountToTransform, success: false); // Act - var result = await controller.ConfirmTransform("account", "token"); - - // Assert - Assert.NotNull(result); - Assert.Equal( - String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountNotSupported, "account"), - controller.TempData["TransformError"]); - } - - [Fact] - public async Task WhenAccountToTransformIsAdmin_ShowsError() - { - // Arrange - var configurationService = GetConfigurationService(); - configurationService.Current.OrganizationsEnabledForDomains = new string[] { "example.com" }; - - var controller = GetController(); - var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; - controller.SetCurrentUser(currentUser); - - GetMock() - .Setup(u => u.FindByUsername("account")) - .Returns(new User("account") - { - EmailAddress = "account@example.com", - Roles = { - new Role() { Name = "Admins" } - } - }); - - // Act - var result = await controller.ConfirmTransform("account", "token"); + var result = await controller.ConfirmTransform(accountToTransform, "token"); // Assert Assert.NotNull(result); Assert.Equal( - String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountNotSupported, "account"), + String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_Failed, "account"), controller.TempData["TransformError"]); } [Fact] - public async Task WhenAccountToTransformIsNotInDomainWhitelist_ShowsError() + public async Task WhenUserServiceReturnsSuccess_Redirects() { // Arrange - var configurationService = GetConfigurationService(); - configurationService.Current.OrganizationsEnabledForDomains = new string[] { "not_example.com" }; - - var controller = GetController(); - var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; - controller.SetCurrentUser(currentUser); - - GetMock() - .Setup(u => u.FindByUsername("account")) - .Returns(new User("account") - { - EmailAddress = "account@example.com" - }); + var accountToTransform = "account"; + var controller = CreateController(accountToTransform, success: true); // Act - var result = await controller.ConfirmTransform("account", "token"); + var result = await controller.ConfirmTransform(accountToTransform, "token"); // Assert Assert.NotNull(result); - Assert.Equal( - String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_OrganizationAccountNotSupported, "account"), - controller.TempData["TransformError"]); + Assert.False(controller.TempData.ContainsKey("TransformError")); } - [Fact] - public async Task WhenUserServiceThrowsException_ShowsError() + private UsersController CreateController(string accountToTransform, string canTransformErrorReason = "", bool success = true) { // Arrange var configurationService = GetConfigurationService(); @@ -2396,54 +2329,23 @@ public async Task WhenUserServiceThrowsException_ShowsError() var controller = GetController(); var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; controller.SetCurrentUser(currentUser); - - GetMock() - .Setup(u => u.FindByUsername("account")) - .Returns(new User("account") - { - EmailAddress = "account@example.com" - }); - - GetMock() - .Setup(s => s.TransformToOrganizationAccount(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(new TransformAccountException("Transform Failed!")); - - // Act - var result = await controller.ConfirmTransform("account", "token"); - - // Assert - Assert.NotNull(result); - Assert.Equal("Transform Failed!", controller.TempData["TransformError"]); - } - - [Fact] - public async Task WhenUserServiceReturnsSuccess_Redirects() - { - // Arrange - var configurationService = GetConfigurationService(); - configurationService.Current.OrganizationsEnabledForDomains = new string[] { "example.com" }; - var controller = GetController(); - var currentUser = new User("OrgAdmin") { EmailAddress = "orgadmin@example.com" }; - controller.SetCurrentUser(currentUser); - GetMock() - .Setup(u => u.FindByUsername("account")) - .Returns(new User("account") + .Setup(u => u.FindByUsername(accountToTransform)) + .Returns(new User(accountToTransform) { - EmailAddress = "account@example.com" + EmailAddress = $"{accountToTransform}@example.com" }); GetMock() - .Setup(s => s.TransformToOrganizationAccount(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.CompletedTask); + .Setup(u => u.CanTransformUserToOrganization(It.IsAny(), out canTransformErrorReason)) + .Returns(string.IsNullOrEmpty(canTransformErrorReason)); - // Act - var result = await controller.ConfirmTransform("account", "token"); + GetMock() + .Setup(s => s.TransformUserToOrganization(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(success)); - // Assert - Assert.NotNull(result); - Assert.False(controller.TempData.ContainsKey("TransformError")); + return controller; } } } diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index b5a811962a..0a00937163 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -363,75 +363,96 @@ public async Task ThrowsArgumentExceptionForNullUser() } } - public class TheTransformToOrganizationAccountMethod + public class TheCanTransformToOrganizationMethod { [Fact] - public async Task WhenAccountIsNull_ThrowsArgNullException() + public void WhenAccountIsNotConfirmed_ReturnsFalse() { - await ContractAssert.ThrowsArgNullAsync( - async () => await new TestableUserService().TransformToOrganizationAccount(null, new User("admin"), "token"), - "accountToTransform"); + // Arrange + var service = new TestableUserService(); + var unconfirmedUser = new User() { UnconfirmedEmailAddress = "unconfirmed@example.com" }; + + // Act + string errorReason; + var result = service.CanTransformUserToOrganization(unconfirmedUser, out errorReason); + + // Assert + Assert.False(result); + Assert.Equal(errorReason, Strings.TransformAccount_FailedReasonNotConfirmedUser); } [Fact] - public async Task WhenAdminIsNull_ThrowsArgNullException() + public void WhenAccountIsOrganization_ReturnsFalse() { - await ContractAssert.ThrowsArgNullAsync( - async () => await new TestableUserService().TransformToOrganizationAccount(new User("account"), null, "token"), - "adminUser"); + // Arrange + var service = new TestableUserService(); + var fakes = new Fakes(); + + // Act + string errorReason; + var result = service.CanTransformUserToOrganization(fakes.Organization, out errorReason); + + // Assert + Assert.False(result); + Assert.Equal(errorReason, Strings.TransformAccount_FailedReasonIsOrganization); } - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(null)] - public async Task WhenTokenIsMissing_ThrowsArgException(string token) + [Fact] + public void WhenAccountHasMemberships_ReturnsFalse() { - await ContractAssert.ThrowsArgNullAsync( - async () => await new TestableUserService().TransformToOrganizationAccount(new User("account"), new User("admin"), token), - "token"); + // Arrange + var service = new TestableUserService(); + var fakes = new Fakes(); + + // Act + string errorReason; + var result = service.CanTransformUserToOrganization(fakes.OrganizationCollaborator, out errorReason); + + // Assert + Assert.False(result); + Assert.Equal(errorReason, Strings.TransformAccount_FailedReasonHasMemberships); } [Fact] - public async Task WhenNoTenant_ThrowsTransformAccountException() + public void WhenAccountIsNotInWhitelist_ReturnsFalse() { // Arrange - var account = new User("Account"); - var admin = new User("Admin"); var service = new TestableUserService(); + service.MockConfig.SetupGet(c => c.OrganizationsEnabledForDomains).Returns(new[] { "notexample.com" }); + var fakes = new Fakes(); - // Act & Assert - var exception = await Assert.ThrowsAsync( - async () => await service.TransformToOrganizationAccount(account, admin, "token")); - Assert.Equal(exception.Message, Strings.TransformAccount_AdminDoesNotHaveTenantId); + // Act + string errorReason; + var result = service.CanTransformUserToOrganization(fakes.User, out errorReason); + + // Assert + Assert.False(result); + Assert.Equal(errorReason, Strings.TransformAccount_FailedReasonNotInDomainWhitelist); } [Fact] - public async Task WhenSqlException_ThrowsTransformAccountException() + public void WhenAccountIsInWhitelist_ReturnsTrue() { // Arrange var service = new TestableUserService(); - var account = new User("Account"); - var admin = new User("Admin"); - admin.Credentials.Add( - new CredentialBuilder().CreateExternalCredential( - issuer: "MicrosoftAccount", - value: "abc123", - identity: "Admin", - tenantId: "zyx987")); + service.MockConfig.SetupGet(c => c.OrganizationsEnabledForDomains).Returns(new[] { "example.com" }); + var fakes = new Fakes(); - service.MockDatabase - .Setup(db => db.ExecuteSqlCommandAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new DataException()); + // Act + string errorReason; + var result = service.CanTransformUserToOrganization(fakes.User, out errorReason); - // Act & Assert - var exception = await Assert.ThrowsAsync( - async () => await service.TransformToOrganizationAccount(account, admin, "token")); - Assert.Equal(exception.Message, Strings.TransformAccount_DatabaseError); + // Assert + Assert.True(result); } + } - [Fact] - public async Task WhenSqlResultIsZero_ThrowsTransformAccountException() + public class TheTransformToOrganizationAccountMethod + { + [Theory] + [InlineData(0)] + [InlineData(-1)] + public async Task WhenSqlResultIsZeroOrLess_ReturnsFalse(int affectedRecords) { // Arrange var service = new TestableUserService(); @@ -445,17 +466,18 @@ public async Task WhenSqlResultIsZero_ThrowsTransformAccountException() tenantId: "zyx987")); service.MockDatabase - .Setup(db => db.ExecuteSqlCommandAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(0)); + .Setup(db => db.ExecuteSqlResourceAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(affectedRecords)); // Act & Assert - var exception = await Assert.ThrowsAsync( - async () => await service.TransformToOrganizationAccount(account, admin, "token")); - Assert.Equal(exception.Message, Strings.TransformAccount_DatabaseError); + var result = await service.TransformUserToOrganization(account, admin, "token"); + Assert.False(result); } - [Fact] - public async Task WhenSqlResultIsOne_ReturnsSuccess() + [Theory] + [InlineData(1)] + [InlineData(3)] + public async Task WhenSqlResultIsPositive_ReturnsTrue(int affectedRecords) { // Arrange var service = new TestableUserService(); @@ -469,11 +491,11 @@ public async Task WhenSqlResultIsOne_ReturnsSuccess() tenantId: "zyx987")); service.MockDatabase - .Setup(db => db.ExecuteSqlCommandAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(1)); + .Setup(db => db.ExecuteSqlResourceAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(affectedRecords)); // Act - await service.TransformToOrganizationAccount(account, admin, "token"); + Assert.True(await service.TransformUserToOrganization(account, admin, "token")); } } } diff --git a/tests/NuGetGallery.Facts/TestUtils/ContractAssert.cs b/tests/NuGetGallery.Facts/TestUtils/ContractAssert.cs index fc2f320679..56d6d40dd1 100644 --- a/tests/NuGetGallery.Facts/TestUtils/ContractAssert.cs +++ b/tests/NuGetGallery.Facts/TestUtils/ContractAssert.cs @@ -42,5 +42,14 @@ public static void ThrowsArgException(Action act, string paramName, string messa message + Environment.NewLine + $"Parameter name: {paramName}", argEx.Message); } + + public static async void ThrowsArgExceptionAsync(Func act, string paramName, string message = "") + { + var argEx = await Assert.ThrowsAsync(async () => await act()); + Assert.Equal(paramName, argEx.ParamName); + Assert.Equal( + message + Environment.NewLine + $"Parameter name: {paramName}", + argEx.Message); + } } } From 12df6a387783a2d17f3a9229804eadd2fcb6d675 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Thu, 4 Jan 2018 15:07:38 -0800 Subject: [PATCH 6/8] Cleanup --- src/NuGetGallery/Extensions/UserExtensions.cs | 7 ----- src/NuGetGallery/NuGetGallery.csproj | 1 - .../Services/TransformAccountException.cs | 23 -------------- src/NuGetGallery/Web.config | 2 +- .../Extensions/UserExtensionsFacts.cs | 31 ------------------- 5 files changed, 1 insertion(+), 63 deletions(-) delete mode 100644 src/NuGetGallery/Services/TransformAccountException.cs diff --git a/src/NuGetGallery/Extensions/UserExtensions.cs b/src/NuGetGallery/Extensions/UserExtensions.cs index b748c213ef..91c6e44e23 100644 --- a/src/NuGetGallery/Extensions/UserExtensions.cs +++ b/src/NuGetGallery/Extensions/UserExtensions.cs @@ -115,12 +115,5 @@ public static void SetAccountAsDeleted(this User user) user.FailedLoginCount = 0; user.IsDeleted = true; } - - public static string GetTenantId(this User user) - { - return user.Credentials - .SingleOrDefault(c => !string.IsNullOrEmpty(c.TenantId))? - .TenantId; - } } } \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index d14c3553ef..649da41f3e 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -905,7 +905,6 @@ - diff --git a/src/NuGetGallery/Services/TransformAccountException.cs b/src/NuGetGallery/Services/TransformAccountException.cs deleted file mode 100644 index 6a53b9bd44..0000000000 --- a/src/NuGetGallery/Services/TransformAccountException.cs +++ /dev/null @@ -1,23 +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 System.Globalization; - -namespace NuGetGallery -{ - [Serializable] - public class TransformAccountException - : Exception - { - public TransformAccountException(string message) - : base(message) - { - } - - public TransformAccountException(string message, params object[] args) - : base(string.Format(CultureInfo.CurrentCulture, message, args)) - { - } - } -} \ No newline at end of file diff --git a/src/NuGetGallery/Web.config b/src/NuGetGallery/Web.config index 88c0f3edfc..b49e72ac85 100644 --- a/src/NuGetGallery/Web.config +++ b/src/NuGetGallery/Web.config @@ -41,7 +41,7 @@ - + diff --git a/tests/NuGetGallery.Facts/Extensions/UserExtensionsFacts.cs b/tests/NuGetGallery.Facts/Extensions/UserExtensionsFacts.cs index e29e634e2c..fa74252ede 100644 --- a/tests/NuGetGallery.Facts/Extensions/UserExtensionsFacts.cs +++ b/tests/NuGetGallery.Facts/Extensions/UserExtensionsFacts.cs @@ -224,36 +224,5 @@ public void WhenApiKeyWithNonMatchingOwnerScopes_ReturnsFalse() Assert.False(user.MatchesOwnerScope(credential)); } } - - public class TheGetTenantIdMethod - { - [Fact] - public void WhenHasTenant_ReturnsValue() - { - var user = new User() { Key = 1234 }; - user.Credentials.Add( - new CredentialBuilder().CreateExternalCredential( - issuer: "MicrosoftAccount", - value: "abc123", - identity: "TestUser", - tenantId: "zyx987")); - - Assert.Equal("zyx987", user.GetTenantId()); - } - - [Fact] - public void WhenNoTenant_ReturnsNull() - { - var user = new User() { Key = 1234 }; - user.Credentials.Add( - new CredentialBuilder().CreateExternalCredential( - issuer: "MicrosoftAccount", - value: "abc123", - identity: "TestUser", - tenantId: null)); - - Assert.Null(user.GetTenantId()); - } - } } } From 551ce83060b252e73565180b5978222790b02bae Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Mon, 8 Jan 2018 14:33:15 -0800 Subject: [PATCH 7/8] PR feedback --- .../Entities/DatabaseWrapper.cs | 6 +++--- .../Configuration/IAppConfiguration.cs | 3 +++ .../Controllers/UsersController.cs | 3 +-- src/NuGetGallery/Services/UserService.cs | 17 ++++++++++++----- src/NuGetGallery/Strings.Designer.cs | 19 +++++-------------- src/NuGetGallery/Strings.resx | 13 +++++-------- .../Views/Users/AccountTransformFailed.cshtml | 19 ++++++++++--------- .../Controllers/UsersControllerFacts.cs | 6 +----- .../Services/UserServiceFacts.cs | 13 +++++++++---- 9 files changed, 49 insertions(+), 50 deletions(-) diff --git a/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs b/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs index ce78aa8987..4f7dfc4ef9 100644 --- a/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs +++ b/src/NuGetGallery.Core/Entities/DatabaseWrapper.cs @@ -44,12 +44,12 @@ public async Task ExecuteSqlResourceAsync(string name, params object[] para sqlCommand = await reader.ReadToEndAsync(); } - if (!string.IsNullOrEmpty(sqlCommand)) + if (string.IsNullOrEmpty(sqlCommand)) { - return await ExecuteSqlCommandAsync(sqlCommand, parameters); + throw new ArgumentException($"SQL resource '{name}' is empty.", "name"); } - return 0; // no records affected + return await ExecuteSqlCommandAsync(sqlCommand, parameters); } } } diff --git a/src/NuGetGallery/Configuration/IAppConfiguration.cs b/src/NuGetGallery/Configuration/IAppConfiguration.cs index 323a89ac26..4f12e4215c 100644 --- a/src/NuGetGallery/Configuration/IAppConfiguration.cs +++ b/src/NuGetGallery/Configuration/IAppConfiguration.cs @@ -93,6 +93,9 @@ public interface IAppConfiguration : ICoreMessageServiceConfiguration /// bool BlockingAsynchronousPackageValidationEnabled { get; set; } + /// + /// Whitelist of domains for which the Organizations feature is enabled. + /// string[] OrganizationsEnabledForDomains { get; set; } /// diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index af8a588498..d0008999cc 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -122,8 +122,7 @@ public virtual async Task ConfirmTransform(string accountNameToTra string errorReason; if (!_userService.CanTransformUserToOrganization(accountToTransform, out errorReason)) { - TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture, - Strings.TransformAccount_FailedWithReason, accountNameToTransform, errorReason); + TempData["TransformError"] = errorReason; return View("AccountTransformFailed"); } diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index ed501a7192..c00c1520ac 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -6,6 +6,7 @@ using System.Data; using System.Data.Entity; using System.Data.SqlClient; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using NuGetGallery.Auditing; @@ -159,22 +160,28 @@ public async Task ConfirmEmailAddress(User user, string token) public bool CanTransformUserToOrganization(User accountToTransform, out string errorReason) { errorReason = null; + var enabledDomains = Config.OrganizationsEnabledForDomains; if (!accountToTransform.Confirmed) { - errorReason = Strings.TransformAccount_FailedReasonNotConfirmedUser; + errorReason = String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_FailedReasonNotConfirmedUser, accountToTransform.Username); } else if (accountToTransform is Organization) { - errorReason = Strings.TransformAccount_FailedReasonIsOrganization; + errorReason = String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_FailedReasonIsOrganization, accountToTransform.Username); } else if (accountToTransform.Organizations.Any() || accountToTransform.OrganizationRequests.Any()) { - errorReason = Strings.TransformAccount_FailedReasonHasMemberships; + errorReason = String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_FailedReasonHasMemberships, accountToTransform.Username); } - else if (!Config.OrganizationsEnabledForDomains.Contains(accountToTransform.ToMailAddress().Host, StringComparer.OrdinalIgnoreCase)) + else if (enabledDomains == null || + !enabledDomains.Contains(accountToTransform.ToMailAddress().Host, StringComparer.OrdinalIgnoreCase)) { - errorReason = Strings.TransformAccount_FailedReasonNotInDomainWhitelist; + errorReason = String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_FailedReasonNotInDomainWhitelist, accountToTransform.Username); } return errorReason == null; diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index b3ad120f33..1a5a96d427 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -1408,7 +1408,7 @@ public static string TokenExpirationShouldGiveUser1MinuteToChangePassword { } /// - /// Looks up a localized string similar to Failed to transform account '{0}'. Contact support@nuget.org for more details.. + /// Looks up a localized string similar to Unexpected error when transforming account '{0}'. Contact support@nuget.org for more details.. /// public static string TransformAccount_Failed { get { @@ -1417,7 +1417,7 @@ public static string TransformAccount_Failed { } /// - /// Looks up a localized string similar to account should not belong to any organizations. + /// Looks up a localized string similar to Account '{0}' should not belong to any organizations.. /// public static string TransformAccount_FailedReasonHasMemberships { get { @@ -1426,7 +1426,7 @@ public static string TransformAccount_FailedReasonHasMemberships { } /// - /// Looks up a localized string similar to account is already an organization. + /// Looks up a localized string similar to Account '{0}' is already an organization.. /// public static string TransformAccount_FailedReasonIsOrganization { get { @@ -1435,7 +1435,7 @@ public static string TransformAccount_FailedReasonIsOrganization { } /// - /// Looks up a localized string similar to account should be a confirmed user. + /// Looks up a localized string similar to Account '{0}' should be a confirmed user.. /// public static string TransformAccount_FailedReasonNotConfirmedUser { get { @@ -1444,7 +1444,7 @@ public static string TransformAccount_FailedReasonNotConfirmedUser { } /// - /// Looks up a localized string similar to account does not support organizations. + /// Looks up a localized string similar to Account '{0}' does not support organizations.. /// public static string TransformAccount_FailedReasonNotInDomainWhitelist { get { @@ -1452,15 +1452,6 @@ public static string TransformAccount_FailedReasonNotInDomainWhitelist { } } - /// - /// Looks up a localized string similar to Failed to transform account '{0}' with reason '{1}'. Contact support@nuget.org for more details.. - /// - public static string TransformAccount_FailedWithReason { - get { - return ResourceManager.GetString("TransformAccount_FailedWithReason", resourceCulture); - } - } - /// /// Looks up a localized string similar to You must confirm the email address for this account in order to complete this request.. /// diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index 0eeda7bf10..fe4cf25798 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -692,27 +692,24 @@ For more information, please contact '{2}'. You must confirm the email address for this account in order to complete this request. - Failed to transform account '{0}'. Contact support@nuget.org for more details. + Unexpected error when transforming account '{0}'. Contact support@nuget.org for more details. Organization account '{0}' does not exist. - - Failed to transform account '{0}' with reason '{1}'. Contact support@nuget.org for more details. - Account '{0}' was successfully transformed into an organization. - account should not belong to any organizations + Account '{0}' should not belong to any organizations. - account should be a confirmed user + Account '{0}' should be a confirmed user. - account does not support organizations + Account '{0}' does not support organizations. - account is already an organization + Account '{0}' is already an organization. \ No newline at end of file diff --git a/src/NuGetGallery/Views/Users/AccountTransformFailed.cshtml b/src/NuGetGallery/Views/Users/AccountTransformFailed.cshtml index 2ece2cd552..06b96b70be 100644 --- a/src/NuGetGallery/Views/Users/AccountTransformFailed.cshtml +++ b/src/NuGetGallery/Views/Users/AccountTransformFailed.cshtml @@ -5,13 +5,14 @@ }
-
-

- Transform Account: Failed -

- @if (TempData.ContainsKey("TransformError")) - { - @ViewHelpers.AlertWarning(@@TempData["TransformError"]) - } -
+ @if (TempData.ContainsKey("TransformError")) + { +
+

+ Failed to transform to an organization +

+
+ @TempData["TransformError"] +
+ }
\ No newline at end of file diff --git a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs index 8d0d6f42fe..817de693d7 100644 --- a/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/UsersControllerFacts.cs @@ -2280,11 +2280,7 @@ public async Task WhenCanTransformReturnsFalse_ShowsError() // Assert Assert.NotNull(result); - Assert.Equal( - String.Format(CultureInfo.CurrentCulture, - Strings.TransformAccount_FailedWithReason, "account", - "error"), - controller.TempData["TransformError"]); + Assert.Equal("error", controller.TempData["TransformError"]); } [Fact] diff --git a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs index 0a00937163..c95302de8c 100644 --- a/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/UserServiceFacts.cs @@ -3,6 +3,7 @@ using System; using System.Data; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Moq; @@ -378,7 +379,8 @@ public void WhenAccountIsNotConfirmed_ReturnsFalse() // Assert Assert.False(result); - Assert.Equal(errorReason, Strings.TransformAccount_FailedReasonNotConfirmedUser); + Assert.Equal(errorReason, String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_FailedReasonNotConfirmedUser, unconfirmedUser.Username)); } [Fact] @@ -394,7 +396,8 @@ public void WhenAccountIsOrganization_ReturnsFalse() // Assert Assert.False(result); - Assert.Equal(errorReason, Strings.TransformAccount_FailedReasonIsOrganization); + Assert.Equal(errorReason, String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_FailedReasonIsOrganization, fakes.Organization.Username)); } [Fact] @@ -410,7 +413,8 @@ public void WhenAccountHasMemberships_ReturnsFalse() // Assert Assert.False(result); - Assert.Equal(errorReason, Strings.TransformAccount_FailedReasonHasMemberships); + Assert.Equal(errorReason, String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_FailedReasonHasMemberships, fakes.OrganizationCollaborator.Username)); } [Fact] @@ -427,7 +431,8 @@ public void WhenAccountIsNotInWhitelist_ReturnsFalse() // Assert Assert.False(result); - Assert.Equal(errorReason, Strings.TransformAccount_FailedReasonNotInDomainWhitelist); + Assert.Equal(errorReason, String.Format(CultureInfo.CurrentCulture, + Strings.TransformAccount_FailedReasonNotInDomainWhitelist, fakes.User.Username)); } [Fact] From 90a7d5b65c416b093aed7f993457327754d92d29 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Tue, 9 Jan 2018 13:14:25 -0800 Subject: [PATCH 8/8] PR feedback --- src/NuGetGallery/Services/UserService.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/NuGetGallery/Services/UserService.cs b/src/NuGetGallery/Services/UserService.cs index c00c1520ac..d4792a90b0 100644 --- a/src/NuGetGallery/Services/UserService.cs +++ b/src/NuGetGallery/Services/UserService.cs @@ -190,16 +190,8 @@ public bool CanTransformUserToOrganization(User accountToTransform, out string e public async Task TransformUserToOrganization(User accountToTransform, User adminUser, string token) { // todo: check for tenantId and add organization policy to enforce this (future work, with manage organization) - - try - { - return await EntitiesContext.TransformUserToOrganization(accountToTransform, adminUser, token); - } - catch (Exception ex) when (ex is SqlException || ex is DataException) - { - // todo: log exception - return false; - } + + return await EntitiesContext.TransformUserToOrganization(accountToTransform, adminUser, token); } } }