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]: UI for creating migration request #5241

Merged
merged 12 commits into from
Jan 9, 2018
Merged
Show file tree
Hide file tree
Changes from 8 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))
{
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";
}
}
}
34 changes: 34 additions & 0 deletions src/NuGetGallery.Core/Infrastructure/MigrateUserToOrganization.sql
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
10 changes: 10 additions & 0 deletions src/NuGetGallery/App_Start/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,16 @@ public static void RegisterUIRoutes(RouteCollection routes)
"account/{action}",
new { controller = "Users", action = "Account" });

routes.MapRoute(
RouteName.TransformAccount,
"account/transform",
new { controller = "Users", action = "Transform" });

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
100 changes: 100 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 @@ -99,6 +100,105 @@ public virtual ActionResult Account()
return AccountView(new AccountViewModel());
}

[HttpGet]
[Authorize]
public virtual ActionResult Transform()
Copy link
Contributor

@scottbommarito scottbommarito Jan 9, 2018

Choose a reason for hiding this comment

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

One additional thing I noticed--I think something like TransformAccountToOrganization would be more descriptive and potentially a better name for this action.

{
var accountToTransform = GetCurrentUser();
Copy link
Contributor

Choose a reason for hiding this comment

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

null check for accountToTransform?

Copy link
Member Author

Choose a reason for hiding this comment

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

The Authorize attribute should ensure that there is an authenticated user.

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

var transformRequest = accountToTransform.OrganizationMigrationRequest;
if (transformRequest != null)
{
TempData["Message"] = String.Format(CultureInfo.CurrentCulture, Strings.TransformAccount_RequestExists,
transformRequest.RequestDate.ToNuGetShortDateString(), transformRequest.AdminUser.Username);
}

return View(new TransformAccountViewModel());
}

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public virtual async Task<ActionResult> Transform(TransformAccountViewModel transformViewModel)
{
var accountToTransform = GetCurrentUser();
string errorReason;
if (!_userService.CanTransformUserToOrganization(accountToTransform, out errorReason))
{
TempData["TransformError"] = String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_FailedWithReason, accountToTransform.Username, errorReason);
return View("AccountTransformFailed");
}

var adminUser = _userService.FindByUsername(transformViewModel.AdminUsername);
if (adminUser == null)
{
ModelState.AddModelError("AdminUsername", String.Format(CultureInfo.CurrentCulture,
Strings.TransformAccount_AdminAccountDoesNotExist, transformViewModel.AdminUsername));
return View(transformViewModel);
}

if (!adminUser.Confirmed)
{
ModelState.AddModelError("AdminUsername", Strings.TransformAccount_AdminAccountNotConfirmed);
return View(transformViewModel);
}

await _userService.RequestTransformToOrganizationAccount(accountToTransform, adminUser);

// prompt for admin sign-on to confirm transformation
OwinContext.Authentication.SignOut();
return Redirect(Url.ConfirmTransformAccount(accountToTransform));
}

[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]
public virtual ActionResult DeleteRequest()
Expand Down
6 changes: 5 additions & 1 deletion src/NuGetGallery/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,13 @@ public static HtmlString ShowPasswordFor<TModel, TProperty>(this HtmlHelper<TMod
return html.PasswordFor(expression, htmlAttributes);
}

public static HtmlString ShowTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
public static HtmlString ShowTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, bool enabled = true)
{
var htmlAttributes = GetHtmlAttributes(html, expression);
if (!enabled)
{
htmlAttributes.Add("disabled", "true");
}
return html.TextBoxFor(expression, htmlAttributes);
}

Expand Down
3 changes: 3 additions & 0 deletions src/NuGetGallery/NuGetGallery.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,7 @@
<Compile Include="ViewModels\ScopeViewModel.cs" />
<Compile Include="ViewModels\PackageListSearchViewModel.cs" />
<Compile Include="ViewModels\ThirdPartyPackageManagerViewModel.cs" />
<Compile Include="ViewModels\TransformAccountViewModel.cs" />
<Compile Include="WebApi\PlainTextResult.cs" />
<Compile Include="WebApi\QueryResult.cs" />
<Compile Include="WebApi\QueryResultDefaults.cs" />
Expand Down Expand Up @@ -1985,6 +1986,8 @@
<Content Include="Views\Authentication\SignInNuGetAccount.cshtml" />
<Content Include="Views\Packages\_VerifyForm.cshtml" />
<Content Include="Views\Packages\_VerifyMetadata.cshtml" />
<Content Include="Views\Users\AccountTransformFailed.cshtml" />
<Content Include="Views\Users\Transform.cshtml" />
</ItemGroup>
<ItemGroup>
<CodeAnalysisDictionary Include="Properties\CodeAnalysisDictionary.xml" />
Expand Down
2 changes: 2 additions & 0 deletions src/NuGetGallery/RouteNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public static class RouteName
public const string V2ApiFeed = "V2ApiFeed";
public const string ApiFeed = "ApiFeed";
public const string Account = "Account";
public const string TransformAccount = "TransformAccount";
public const string TransformAccountConfirmation = "ConfirmTransformAccount";
public const string ApiKeys = "ApiKeys";
public const string Profile = "Profile";
public const string DisplayPackage = "package-route";
Expand Down
Loading