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

User Accounts and Custom User Settings Deployment #14208

Merged
merged 43 commits into from
Oct 21, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ca45294
User Accounts Deployment
SzymonSel Aug 25, 2023
0f80b89
CustomUserSettings deployment
SzymonSel Aug 25, 2023
2e38e17
Cleanup
SzymonSel Aug 25, 2023
69a2eb3
Code review changes
SzymonSel Aug 28, 2023
40a490f
Further changes
SzymonSel Aug 28, 2023
0625ffc
Formatting and bracket
SzymonSel Aug 28, 2023
e5476a6
Further refactor
SzymonSel Aug 28, 2023
6c7a4f8
Registration
SzymonSel Aug 28, 2023
153473c
User roles import
SzymonSel Aug 28, 2023
9044980
Vanilla js
SzymonSel Aug 28, 2023
d2a650c
Update src/OrchardCore.Modules/OrchardCore.Users/Views/Items/CustomUs…
SzymonSel Aug 29, 2023
f41f59b
Update src/OrchardCore.Modules/OrchardCore.Users/Views/Items/CustomUs…
SzymonSel Aug 29, 2023
08272f0
Ongoing refactoring hell
SzymonSel Aug 29, 2023
f390d4f
Bang bang
SzymonSel Aug 29, 2023
b852da8
Update src/OrchardCore.Modules/OrchardCore.Users/Deployment/AllUsersD…
SzymonSel Aug 31, 2023
2a9b759
Merge branch 'dev' into szymonsel/users-deployment
SzymonSel Sep 28, 2023
1cd46e6
Update src/OrchardCore.Modules/OrchardCore.Users/Views/Items/CustomUs…
SzymonSel Sep 28, 2023
1aaabdf
CR changes
SzymonSel Sep 28, 2023
1fc8b05
CR changes
SzymonSel Sep 28, 2023
f7a9538
CR Changes
SzymonSel Sep 28, 2023
94050b0
Quick refactor
SzymonSel Sep 28, 2023
bf4b21c
CR cleanup
SzymonSel Sep 28, 2023
173c933
CR cleanup
SzymonSel Sep 28, 2023
55e81e4
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
hishamco Oct 21, 2023
60fa9d4
Update src/OrchardCore.Modules/OrchardCore.Users/Recipes/UsersStep.cs
hishamco Oct 21, 2023
fddb800
Update src/OrchardCore.Modules/OrchardCore.Users/ViewModels/CustomUse…
hishamco Oct 21, 2023
fc58742
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
hishamco Oct 21, 2023
b51dc2c
Update src/OrchardCore.Modules/OrchardCore.Users/ViewModels/CustomUse…
hishamco Oct 21, 2023
480e01d
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
hishamco Oct 21, 2023
28ac1df
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
b37e14e
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
89d3589
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
f126fa9
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
hishamco Oct 21, 2023
5cd5a0c
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
b1ac1e1
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
2cef4ed
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
ed777b7
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
090d2d9
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
683d429
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
5fea5d8
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
5bd124f
Update src/OrchardCore.Modules/OrchardCore.Users/Models/UsersStepUser…
SzymonSel Oct 21, 2023
bf483bc
Update src/OrchardCore.Modules/OrchardCore.Users/Deployment/AllUsersD…
hishamco Oct 21, 2023
9cd2372
Apply suggestions from code review
hishamco Oct 21, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using OrchardCore.Deployment;
using OrchardCore.Users.Models;
using OrchardCore.Users.Recipes;
using YesSql;

namespace OrchardCore.Users.Deployment;

public class AllUsersDeploymentSource : IDeploymentSource
{
private readonly ISession _session;

public AllUsersDeploymentSource(ISession session)
{
_session = session;
}

public async Task ProcessDeploymentStepAsync(DeploymentStep step, DeploymentPlanResult result)
{
var allRolesStep = step as AllUsersDeploymentStep;

if (allRolesStep == null)
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
{
return;
}

SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
var allUsers = await _session.Query<User>().ListAsync();
var users = new JArray();
var tasks = new List<Task>();

foreach (var user in allUsers)
{
users.Add(JObject.FromObject(
new UsersStepUserModel
{
UserName = user.UserName,
UserId = user.UserId,
Id = user.Id,
Email = user.Email,
EmailConfirmed = user.EmailConfirmed,
PasswordHash = user.PasswordHash,
Copy link
Member

Choose a reason for hiding this comment

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

Just for info I'm working on #7891 to be able to securely exchange secrets as passwords, credentials for email, RSA keys in place of X509 certificates for OpenId and so on.

Not sure that it could be applied to user passwords, I would need to re-focus on #7891 (didn't work on it the last 2 weeks). Hmm, if it can be applied maybe in a separate PR if we agree to merge this one in the meantime.

Hmm, to deploy secrets we share a symetric key which is itself encrypted by an assymetric key (in fact one to encrypt data and onother one for signing), so maybe we could use the same technic to exchange users without having to encrypt their passwords differently.

This would mean that it is not incompatible with this PR and could be done afterwards, or maybe not, we could think that we first need #7891 and that this PR would need to use it, meaning that deploying users would need 2 RSA secrets to be defined, one for encryption and one for signing, as will be done to deploy secrets.

Copy link
Member

Choose a reason for hiding this comment

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

Does that mean we need to postpone merging this PR?

Copy link
Member

Choose a reason for hiding this comment

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

Not necessarily ;)

I'm not against merging this PR first, was just for info, I let others decide.

IsEnabled = user.IsEnabled,
hishamco marked this conversation as resolved.
Show resolved Hide resolved
NormalizedEmail = user.NormalizedEmail,
NormalizedUserName = user.NormalizedUserName,
SecurityStamp = user.SecurityStamp,
ResetToken = user.ResetToken,
PhoneNumber = user.PhoneNumber,
PhoneNumberConfirmed = user.PhoneNumberConfirmed,
TwoFactorEnabled = user.TwoFactorEnabled,
IsLockoutEnabled = user.IsLockoutEnabled,
AccessFailedCount = user.AccessFailedCount,
RoleNames = user.RoleNames,
}));
}

result.Steps.Add(new JObject(
new JProperty("name", "Users"),
new JProperty("Users", users)
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using OrchardCore.Deployment;

namespace OrchardCore.Users.Deployment;
/// <summary>
/// Adds users to a <see cref="DeploymentPlanResult"/>.
/// </summary>
public class AllUsersDeploymentStep : DeploymentStep
{
public AllUsersDeploymentStep()
{
Name = "AllUsers";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using OrchardCore.Deployment;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;

namespace OrchardCore.Users.Deployment;

public class AllUsersDeploymentStepDriver : DisplayDriver<DeploymentStep, AllUsersDeploymentStep>
{
public override IDisplayResult Display(AllUsersDeploymentStep step)
{
return
Combine(
View("AllUsersDeploymentStep_Summary", step).Location("Summary", "Content"),
View("AllUsersDeploymentStep_Thumbnail", step).Location("Thumbnail", "Content")
);
}

public override IDisplayResult Edit(AllUsersDeploymentStep step)
{
return View("AllUsersDeploymentStep_Edit", step).Location("Content");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using OrchardCore.Deployment;
using OrchardCore.Users.Models;
using OrchardCore.Users.Services;
using YesSql;

namespace OrchardCore.Users.Deployment;

public class CustomUserSettingsDeploymentSource : IDeploymentSource
{
private readonly CustomUserSettingsService _customUserSettingsService;
private readonly ISession _session;

public CustomUserSettingsDeploymentSource(CustomUserSettingsService customUserSettingsService, ISession session)
{
_customUserSettingsService = customUserSettingsService;
_session = session;
}

public async Task ProcessDeploymentStepAsync(DeploymentStep step, DeploymentPlanResult result)
{
var customUserSettingsStep = step as CustomUserSettingsDeploymentStep;
if (customUserSettingsStep == null)
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
{
return;
}

var settingsTypes = customUserSettingsStep.IncludeAll
? _customUserSettingsService.GetAllSettingsTypes().ToArray()
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
: _customUserSettingsService.GetSettingsTypes(customUserSettingsStep.SettingsTypeNames).ToArray();

foreach (var settingsType in settingsTypes)
{
if (!await _customUserSettingsService.CanUserCreateSettingsAsync(settingsType))
{
return;
}
}

var bigArray = new JArray();
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved

var allUsers = await _session.Query<User>().ListAsync();

foreach (var user in allUsers)
{
var myArray = new JArray();
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
foreach (var settingsType in settingsTypes)
{
var userSetting = await _customUserSettingsService.GetSettingsAsync(user, settingsType);
myArray.Add(JObject.FromObject(userSetting));
}

bigArray.Add(new JObject(
new JProperty("userId", user.UserId),
new JProperty("user-custom-user-settings", myArray)
));
}

// Adding custom user settings
result.Steps.Add(new JObject(
new JProperty("name", "custom-user-settings"),
new JProperty("custom-user-settings", bigArray)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using OrchardCore.Deployment;

namespace OrchardCore.Users.Deployment;

public class CustomUserSettingsDeploymentStep : DeploymentStep
{
public CustomUserSettingsDeploymentStep()
{
Name = "CustomUserSettings";
}

public bool IncludeAll { get; set; } = true;

public string[] SettingsTypeNames { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using OrchardCore.Deployment;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Users.Services;
using OrchardCore.Users.ViewModels;

namespace OrchardCore.Users.Deployment
{
public class CustomUserSettingsDeploymentStepDriver : DisplayDriver<DeploymentStep, CustomUserSettingsDeploymentStep>
{
private readonly CustomUserSettingsService _customUserSettingsService;

public CustomUserSettingsDeploymentStepDriver(CustomUserSettingsService customUserSettingsService)
{
_customUserSettingsService = customUserSettingsService;
}

public override IDisplayResult Display(CustomUserSettingsDeploymentStep step)
{
return
Combine(
View("CustomUserSettingsDeploymentStep_Fields_Summary", step).Location("Summary", "Content"),
View("CustomUserSettingsDeploymentStep_Fields_Thumbnail", step).Location("Thumbnail", "Content")
);
}

public override IDisplayResult Edit(CustomUserSettingsDeploymentStep step)
{
return Initialize<CustomUserSettingsDeploymentStepViewModel>("CustomUserSettingsDeploymentStep_Fields_Edit", model =>
{
model.IncludeAll = step.IncludeAll;
model.SettingsTypeNames = step.SettingsTypeNames;
model.AllSettingsTypeNames = _customUserSettingsService.GetAllSettingsTypeNames().ToArray();
}).Location("Content");
}

public override async Task<IDisplayResult> UpdateAsync(CustomUserSettingsDeploymentStep step, IUpdateModel updater)
{
step.SettingsTypeNames = Array.Empty<string>();

await updater.TryUpdateModelAsync(step,
Prefix,
x => x.SettingsTypeNames,
x => x.IncludeAll);
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved

if (step.IncludeAll)
{
step.SettingsTypeNames = Array.Empty<string>();
}

return Edit(step);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using OrchardCore.ContentManagement;
using OrchardCore.Recipes.Models;
using OrchardCore.Recipes.Services;
using OrchardCore.Users.Models;
using YesSql;

namespace OrchardCore.Users.Recipes
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// This recipe step updates the custom user settings.
/// </summary>
public class CustomUserSettingsStep : IRecipeStepHandler
{
private ISession _session;
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved

public CustomUserSettingsStep(ISession session)
{
_session = session;
}

public async Task ExecuteAsync(RecipeExecutionContext context)
{
if (!String.Equals(context.Name, "custom-user-settings", StringComparison.OrdinalIgnoreCase))
{
return;
}

var model = context.Step;

var customUserSettingsList = (JArray)(from property in model.Properties()
where property.Name != "name"
select property).FirstOrDefault().Value;
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved

var allUsers = (await _session.Query<User>().ListAsync());

foreach (JObject userCustomUserSettings in customUserSettingsList)
{
var userId = userCustomUserSettings.Properties().FirstOrDefault(p => p.Name == "userId").Value.ToString();
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved

var iUser = allUsers.FirstOrDefault(u => u.UserId == userId);
if (iUser == null)
{
continue;
}

var user = iUser as User;
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
var userSettings = (JArray)userCustomUserSettings.Properties().FirstOrDefault(p => p.Name == "user-custom-user-settings").Value;
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved

foreach (JObject userSetting in userSettings)
{
var ci = userSetting.ToObject<ContentItem>();
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
user.Properties[ci.ContentType] = userSetting;

}

_session.Save(user);
}
}
}
}
89 changes: 89 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Users/Recipes/UsersStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using OrchardCore.Recipes.Models;
using OrchardCore.Recipes.Services;
using OrchardCore.Users.Models;
using YesSql;

namespace OrchardCore.Users.Recipes;

public class UsersStep : IRecipeStepHandler
{
private readonly UserManager<IUser> _userManager;
private readonly ISession _session;

public UsersStep(
UserManager<IUser> userManager,
ISession session)
{
_userManager = userManager;
_session = session;
}

public async Task ExecuteAsync(RecipeExecutionContext context)
{
if (!String.Equals(context.Name, "Users", StringComparison.OrdinalIgnoreCase))
{
return;
}

var model = context.Step.ToObject<UsersStepModel>();

foreach (var importedUser in model.Users)
{
if (String.IsNullOrWhiteSpace(importedUser.UserName))
continue;

var iUser = await _userManager.FindByIdAsync(importedUser.UserId);

if (iUser is not User user)
{
user = new User { UserId = importedUser.UserId };
}
else
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
{
user = iUser as User;
}

user.Email = importedUser.Email;
user.UserName = importedUser.UserName;
user.EmailConfirmed = importedUser.EmailConfirmed;
user.PasswordHash = importedUser.PasswordHash;
user.IsEnabled = importedUser.IsEnabled;
hishamco marked this conversation as resolved.
Show resolved Hide resolved
user.NormalizedEmail = importedUser.NormalizedEmail;
user.NormalizedUserName = importedUser.NormalizedUserName;
user.SecurityStamp = importedUser.SecurityStamp;
user.ResetToken = importedUser.ResetToken;

_session.Save(user);
}
}

public class UsersStepModel
{
public UsersStepUserModel[] Users { get; set; }
}
}

public class UsersStepUserModel
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
{
public long Id { get; set; }
public string UserId { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public bool EmailConfirmed { get; set; }
public bool IsEnabled { get; set; } = true;
SzymonSel marked this conversation as resolved.
Show resolved Hide resolved
public string NormalizedEmail { get; set; }
public string NormalizedUserName { get; set; }
public string SecurityStamp { get; set; }
public string ResetToken { get; set; }
public string PhoneNumber { get; internal set; }
public bool PhoneNumberConfirmed { get; internal set; }
public bool TwoFactorEnabled { get; internal set; }
public bool IsLockoutEnabled { get; internal set; }
public int AccessFailedCount { get; internal set; }
public IList<string> RoleNames { get; internal set; }
}
Loading
Loading