Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Organizations: transform account on confirmation #5228

Merged
merged 8 commits into from
Jan 9, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/NuGetGallery.Core/Entities/DatabaseWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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.IO;
using System.Reflection;
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<int> ExecuteSqlCommandAsync(string sql, params object[] parameters)
{
return _database.ExecuteSqlCommandAsync(sql, parameters);
}

public DbContextTransaction BeginTransaction()
{
return _database.BeginTransaction();
}

/// <summary>
/// Execute an embedded resource SQL script.
/// </summary>
/// <param name="name">Resource name</param>
/// <param name="parameters">SQL parameters</param>
/// <returns>Resulting <see cref="System.Data.SqlClient.SqlDataReader.RecordsAffected"/></returns>
public async Task<int> 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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't execute an empty command be indicative of a bug? I think we should throw here -- or let ExecuteSqlCommandAsync do its thing.

{
return await ExecuteSqlCommandAsync(sqlCommand, parameters);
}

return 0; // no records affected
}
}
}
4 changes: 2 additions & 2 deletions src/NuGetGallery.Core/Entities/EntitiesContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/NuGetGallery.Core/Entities/IDatabase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 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
{
DbContextTransaction BeginTransaction();

Task<int> ExecuteSqlCommandAsync(string sql, params object[] parameters);

Task<int> ExecuteSqlResourceAsync(string name, params object[] parameters);
}
}
2 changes: 1 addition & 1 deletion src/NuGetGallery.Core/Entities/IEntitiesContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ public interface IEntitiesContext
IDbSet<T> Set<T>() where T : class;
void DeleteOnCommit<T>(T entity) where T : class;
void SetCommandTimeout(int? seconds);
Database GetDatabase();
IDatabase GetDatabase();
}
}
1 change: 1 addition & 0 deletions src/NuGetGallery.Core/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public User(string username)
SecurityPolicies = new List<UserSecurityPolicy>();
ReservedNamespaces = new HashSet<ReservedNamespace>();
Organizations = new List<Membership>();
OrganizationRequests = new List<MembershipRequest>();
Roles = new List<Role>();
Username = username;
}
Expand Down
40 changes: 40 additions & 0 deletions src/NuGetGallery.Core/Extensions/EntitiesContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<bool> TransformUserToOrganization(this IEntitiesContext context, User accountToTransform, User adminUser, string token)
{
accountToTransform = accountToTransform ?? throw new ArgumentNullException(nameof(accountToTransform));
adminUser = adminUser ?? throw new ArgumentNullException(nameof(adminUser));

if (string.IsNullOrWhiteSpace(token))
{
throw new ArgumentException(nameof(token));
}

var database = context.GetDatabase();
var recordCount = await database.ExecuteSqlResourceAsync(
MigrateUserToOrganization.ResourceName,
new SqlParameter(MigrateUserToOrganization.OrganizationKey, accountToTransform.Key),
new SqlParameter(MigrateUserToOrganization.AdminKey, adminUser.Key),
new SqlParameter(MigrateUserToOrganization.ConfirmationToken, token));

return recordCount > 0;
}

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";
}
}
}
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions src/NuGetGallery.Core/NuGetGallery.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@
<Compile Include="Entities\Credential.cs" />
<Compile Include="Entities\CuratedFeed.cs" />
<Compile Include="Entities\CuratedPackage.cs" />
<Compile Include="Entities\DatabaseWrapper.cs" />
<Compile Include="Entities\IDatabase.cs" />
<Compile Include="Entities\Membership.cs" />
<Compile Include="Entities\Organization.cs" />
<Compile Include="Entities\AccountDelete.cs" />
Expand Down Expand Up @@ -208,6 +210,7 @@
<Compile Include="Entities\SuspendDbExecutionStrategy.cs" />
<Compile Include="Entities\UserSecurityPolicy.cs" />
<Compile Include="Entities\User.cs" />
<Compile Include="Extensions\EntitiesContextExtensions.cs" />
<Compile Include="Extensions\UserExtensionsCore.cs" />
<Compile Include="ICloudStorageStatusDependency.cs" />
<Compile Include="Infrastructure\AzureEntityList.cs" />
Expand Down Expand Up @@ -268,6 +271,9 @@
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Infrastructure\MigrateUserToOrganization.sql" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<NuGetBuildPath Condition="'$(NuGetBuildPath)' == ''">..\..\build</NuGetBuildPath>
Expand Down
5 changes: 5 additions & 0 deletions src/NuGetGallery/App_Start/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,11 @@ public static void RegisterUIRoutes(RouteCollection routes)
"account/{action}",
new { controller = "Users", action = "Account" });

routes.MapRoute(
RouteName.TransformAccountConfirmation,
"account/transform/confirm/{accountNameToTransform}/{token}",
new { controller = "Users", action = "ConfirmTransform" });

routes.MapRoute(
RouteName.ApiKeys,
"account/apikeys",
Expand Down
4 changes: 4 additions & 0 deletions src/NuGetGallery/Configuration/AppConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

/// <summary>
/// Gets the URI to the search service
Expand Down
2 changes: 2 additions & 0 deletions src/NuGetGallery/Configuration/IAppConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public interface IAppConfiguration : ICoreMessageServiceConfiguration
/// </summary>
bool BlockingAsynchronousPackageValidationEnabled { get; set; }

string[] OrganizationsEnabledForDomains { get; set; }

/// <summary>
/// Gets the URI to the search service
/// </summary>
Expand Down
42 changes: 42 additions & 0 deletions src/NuGetGallery/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Mail;
Expand Down Expand Up @@ -98,6 +99,47 @@ public virtual ActionResult Account()
{
return AccountView(new AccountViewModel());
}

[HttpGet]
[Authorize]
public virtual async Task<ActionResult> ConfirmTransform(string accountNameToTransform, string token)
{
var adminUser = GetCurrentUser();
if (!adminUser.Confirmed)
{
TempData["TransformError"] = Strings.TransformAccount_NotConfirmed;
return RedirectToAction("ConfirmationRequired");
}

var accountToTransform = _userService.FindByUsername(accountNameToTransform);
if (accountToTransform == null)
{
TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_OrganizationAccountDoesNotExist, accountNameToTransform);
return View("AccountTransformFailed");
}

string errorReason;
if (!_userService.CanTransformUserToOrganization(accountToTransform, out errorReason))
{
TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_FailedWithReason, accountNameToTransform, errorReason);
return View("AccountTransformFailed");
}

if (!await _userService.TransformUserToOrganization(accountToTransform, adminUser, token))
{
TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_Failed, accountNameToTransform);
return View("AccountTransformFailed");
}

TempData["Message"] = String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_Success, accountNameToTransform);

// todo: redirect to ManageOrganization (future work)
return RedirectToAction("Account");
}

[HttpGet]
[Authorize]
Expand Down
1 change: 1 addition & 0 deletions src/NuGetGallery/NuGetGallery.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,7 @@
<Content Include="Views\Authentication\SignInNuGetAccount.cshtml" />
<Content Include="Views\Packages\_VerifyForm.cshtml" />
<Content Include="Views\Packages\_VerifyMetadata.cshtml" />
<Content Include="Views\Users\AccountTransformFailed.cshtml" />
</ItemGroup>
<ItemGroup>
<CodeAnalysisDictionary Include="Properties\CodeAnalysisDictionary.xml" />
Expand Down
1 change: 1 addition & 0 deletions src/NuGetGallery/RouteNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 4 additions & 0 deletions src/NuGetGallery/Services/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,9 @@ public interface IUserService
Task CancelChangeEmailAddress(User user);

Task<IDictionary<int, string>> GetEmailAddressesForUserKeysAsync(IReadOnlyCollection<int> distinctUserKeys);

bool CanTransformUserToOrganization(User accountToTransform, out string errorReason);

Task<bool> TransformUserToOrganization(User accountToTransform, User adminUser, string token);
}
}
2 changes: 1 addition & 1 deletion src/NuGetGallery/Services/PackageDeleteService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading