-
Notifications
You must be signed in to change notification settings - Fork 643
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
Changes from 8 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
5a02f4e
Organizations: transform on confirmation
chenriksson 930abd4
Tests and cleanup
chenriksson 552e728
Fix query
chenriksson c8978bb
PR feedback
chenriksson 8cba4dd
PR feedback
chenriksson 12df6a3
Cleanup
chenriksson a881e1d
[Organizations]: UI for creating migration request
chenriksson 0e0ad2d
Test fixes
chenriksson 2924613
PR feedback
chenriksson 98445be
Merge from dev
chenriksson 2d153fd
Merge from dev
chenriksson 01e242b
PR feedback
chenriksson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
src/NuGetGallery.Core/Extensions/EntitiesContextExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
34
src/NuGetGallery.Core/Infrastructure/MigrateUserToOrganization.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Globalization; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Net.Mail; | ||
|
@@ -99,6 +100,105 @@ public virtual ActionResult Account() | |
return AccountView(new AccountViewModel()); | ||
} | ||
|
||
[HttpGet] | ||
[Authorize] | ||
public virtual ActionResult Transform() | ||
{ | ||
var accountToTransform = GetCurrentUser(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. null check for accountToTransform? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.