Skip to content

Commit c9c22f4

Browse files
authored
Provicevko/create tech admin net6 (#764)
* Create SuperAdmin.sql Added sql file * Added MustChangePassword field to User model * Changed results * Added UserManagerAdditionalService * Added ChangePasswordLogin * Added using password regex from constants * Added tests * Added localizer messages * Updated NUnit version * Added MustChangePassword migration * namespace fix * Renamed admin role to techadmin role * AddedTechAdminWithMustChangePassword migration * fix migration issue * Updated SuperAdmin.sql * Net6 null check
1 parent 972bfe2 commit c9c22f4

File tree

27 files changed

+3117
-49
lines changed

27 files changed

+3117
-49
lines changed

Config/SuperAdmin.sql

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
USE OutOfSchoolDb;
2+
3+
INSERT INTO AspNetUsers
4+
(Id,
5+
CreatingTime,
6+
LastLogin,
7+
LastName,
8+
MiddleName,
9+
FirstName,
10+
Role,
11+
UserName,
12+
NormalizedUserName,
13+
Email,
14+
NormalizedEmail,
15+
EmailConfirmed,
16+
PasswordHash,
17+
SecurityStamp,
18+
ConcurrencyStamp,
19+
PhoneNumber,
20+
PhoneNumberConfirmed,
21+
TwoFactorEnabled,
22+
LockoutEnd,
23+
LockoutEnabled,
24+
AccessFailedCount,
25+
IsRegistered,
26+
MustChangePassword)
27+
VALUES
28+
('fe78ed30-7df9-412b-8f1c-a21f3175334f', #Id
29+
NOW(), #CreatingTime
30+
'0001-01-01 00:00:00.0000000', #LastLogin
31+
'ТехАдмін', #Last name
32+
'ТехАдмін', #Middle name
33+
'ТехАдмін', #First name
34+
'techadmin', #Role
35+
'[email protected]', #UserName
36+
'[email protected]', #NormalizedUserName
37+
38+
'[email protected]', #NormalizedEmail
39+
0, #EmailConfirmed
40+
'AQAAAAEAACcQAAAAEB+AG8yyfG66fAkFIJ1mMdDx5ChM/wxoUHIwBuJt4cuO+jDCrPMw7D9BLYRtkvbrpg==', #PasswordHash from password -> Kf%ms3nAp7%aH
41+
'CALDXWOCPDFBODKYAKGNLA4LH36H7HAC', #SecurityStamp
42+
'9a8f388b-c168-4ea1-ad5a-ae3245d7bd4f', #ConcurrencyStamp
43+
'123456789', #PhoneNumber without +380
44+
0, #PhoneNumberConfirmed
45+
0, #TwoFactorEnabled
46+
NULL, #LockoutEnd
47+
1, #LockoutEnabled
48+
0, #AccessFailedCount
49+
1, #IsRegistered
50+
1) #MustChangePassword
51+
ON DUPLICATE KEY UPDATE # to reset sensitive data
52+
Email='[email protected]', #Email
53+
PasswordHash = 'AQAAAAEAACcQAAAAEB+AG8yyfG66fAkFIJ1mMdDx5ChM/wxoUHIwBuJt4cuO+jDCrPMw7D9BLYRtkvbrpg==', #PasswordHash from password -> Kf%ms3nAp7%aH
54+
SecurityStamp = 'G4GRJFN7ET7EVVDQ4NEQQOY6SETRULJB', #SecurityStamp
55+
ConcurrencyStamp = 'ed689760-8ab0-45fa-8b13-ec431c093627', #ConcurrencyStamp
56+
MustChangePassword = 1; #MustChangePassword
57+
58+
INSERT INTO AspNetUserRoles
59+
(UserId, RoleId)
60+
VALUES
61+
((SELECT Id FROM AspNetUsers WHERE Role = 'techadmin' LIMIT 1) #UserId (tech admin)
62+
,(SELECT Id FROM AspNetRoles WHERE Name = 'techadmin' LIMIT 1)) #RoleId (tech admin)
63+

OutOfSchool/OutOfSchool.Common/Constants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@ public static class Constants
2929
public const string AddressSeparator = ", ";
3030

3131
public const string EnumErrorMessage = "{0} should be in enum range";
32+
33+
public const string AdminKeyword = "Admin";
3234
}

OutOfSchool/OutOfSchool.Common/PermissionModule/PermissionManagement/PermissionsSeeder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public static string SeedPermissions(string role)
7171
{
7272
switch (role.ToLower())
7373
{
74-
case "admin":
74+
case "techadmin":
7575
return SeedAdminPermissions.PackPermissionsIntoString();
7676

7777
case "provider":

OutOfSchool/OutOfSchool.DataAccess/Enums/Role.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ public enum Role
44
{
55
Provider,
66
Parent,
7-
Admin,
7+
TechAdmin,
88
MinistryAdmin,
99
}

OutOfSchool/OutOfSchool.DataAccess/Extensions/ModelBuilderExtension.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.EntityFrameworkCore;
2+
using OutOfSchool.Common;
23
using OutOfSchool.Common.PermissionsModule;
34
using OutOfSchool.Services.Enums;
45
using OutOfSchool.Services.Models;
@@ -67,9 +68,9 @@ public static void Seed(this ModelBuilder builder)
6768
new PermissionsForRole
6869
{
6970
Id = 1,
70-
RoleName = Role.Admin.ToString(),
71-
PackedPermissions = PermissionsSeeder.SeedPermissions(Role.Admin.ToString()),
72-
Description = "admin permissions",
71+
RoleName = Role.TechAdmin.ToString(),
72+
PackedPermissions = PermissionsSeeder.SeedPermissions(Role.TechAdmin.ToString()),
73+
Description = "techadmin permissions",
7374
},
7475
new PermissionsForRole
7576
{
@@ -88,8 +89,8 @@ public static void Seed(this ModelBuilder builder)
8889
new PermissionsForRole
8990
{
9091
Id = 4,
91-
RoleName = nameof(Role.Provider) + nameof(Role.Admin),
92-
PackedPermissions = PermissionsSeeder.SeedPermissions(nameof(Role.Provider) + nameof(Role.Admin)),
92+
RoleName = nameof(Role.Provider) + Constants.AdminKeyword,
93+
PackedPermissions = PermissionsSeeder.SeedPermissions(nameof(Role.Provider) + Constants.AdminKeyword),
9394
Description = "provider admin permissions",
9495
},
9596
new PermissionsForRole

OutOfSchool/OutOfSchool.DataAccess/Models/User.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,8 @@ public class User : IdentityUser, IKeyedEntity<string>
3636
// for permissions managing at login and check if user is original provider or its admin
3737
public bool IsDerived { get; set; } = false;
3838

39+
// If it's true then user must change his password before the logging into the system
40+
public bool MustChangePassword { get; set; }
41+
3942
public Gender Gender { get; set; }
4043
}

OutOfSchool/OutOfSchool.IdentityServer.Tests/Controllers/AuthControllerTests.cs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@
2424
using Microsoft.Extensions.Localization;
2525
using Microsoft.Extensions.Options;
2626
using OutOfSchool.IdentityServer.Config;
27+
using OutOfSchool.IdentityServer.Services.Interfaces;
28+
using OutOfSchool.Tests.Common.TestDataGenerators;
2729

2830
namespace OutOfSchool.IdentityServer.Tests.Controllers;
2931

3032
[TestFixture]
3133
public class AuthControllerTests
3234
{
3335
private readonly Mock<FakeUserManager> fakeUserManager;
36+
private readonly Mock<IUserManagerAdditionalService> fakeUserManagerAdditionalService;
3437
private readonly Mock<FakeSignInManager> fakeSignInManager;
3538
private readonly Mock<IIdentityServerInteractionService> fakeInteractionService;
3639
private readonly Mock<ILogger<AuthController>> fakeLogger;
@@ -42,6 +45,7 @@ public class AuthControllerTests
4245
public AuthControllerTests()
4346
{
4447
fakeUserManager = new Mock<FakeUserManager>();
48+
fakeUserManagerAdditionalService = new Mock<IUserManagerAdditionalService>();
4549
fakeInteractionService = new Mock<IIdentityServerInteractionService>();
4650
fakeSignInManager = new Mock<FakeSignInManager>();
4751
fakeLogger = new Mock<ILogger<AuthController>>();
@@ -67,6 +71,7 @@ public void Setup()
6771
.Returns(new LocalizedString("mock", "error"));
6872
authController = new AuthController(
6973
fakeUserManager.Object,
74+
fakeUserManagerAdditionalService.Object,
7075
fakeSignInManager.Object,
7176
fakeInteractionService.Object,
7277
fakeLogger.Object,
@@ -168,6 +173,183 @@ public async Task Login_WithLoginVMWithoutModelError_ReturnsActionResult(
168173
Assert.IsInstanceOf(type, result);
169174
}
170175

176+
[Test]
177+
public async Task Login_WhenUserMustChangePasswordAndEmailWithPasswordAreCorrect_ReturnsRedirectActionToChangePasswordLogin()
178+
{
179+
// Arrange
180+
var user = UserGenerator.Generate();
181+
var loginViewModel = CreateLoginViewModelFromData();
182+
user.MustChangePassword = true;
183+
SetupDefaultUserManagerFindByEmailAsync(user);
184+
SetupDefaultSignInManagerCheckPasswordSignInAsync(SignInResult.Success);
185+
186+
// Act
187+
var result = await authController.Login(loginViewModel);
188+
189+
// Assert
190+
Assert.IsInstanceOf(typeof(RedirectToActionResult), result);
191+
Assert.AreEqual(0, authController.ModelState.Count);
192+
Assert.AreEqual(nameof(AuthController.ChangePasswordLogin), (result as RedirectToActionResult)?.ActionName);
193+
}
194+
195+
[Test]
196+
public async Task Login_WhenUserMustChangePasswordAndEmailWithPasswordAreNotCorrect_ReturnsViewWithError()
197+
{
198+
// Arrange
199+
var user = UserGenerator.Generate();
200+
var loginViewModel = CreateLoginViewModelFromData();
201+
user.MustChangePassword = true;
202+
SetupDefaultUserManagerFindByEmailAsync(user);
203+
SetupDefaultSignInManagerCheckPasswordSignInAsync(SignInResult.Failed);
204+
205+
// Act
206+
var result = await authController.Login(loginViewModel);
207+
208+
// Assert
209+
Assert.IsInstanceOf(typeof(ViewResult), result);
210+
Assert.AreEqual(1, authController.ModelState.Count);
211+
}
212+
213+
[Test]
214+
public async Task Login_WhenUserMustNotChangePasswordAndEmailWithPasswordAreCorrectAndReturnUrlIsNotEmpty_ReturnsRedirectToReturnUrl()
215+
{
216+
// Arrange
217+
var user = UserGenerator.Generate();
218+
var loginViewModel = CreateLoginViewModelFromData(user.UserName);
219+
user.MustChangePassword = false;
220+
SetupDefaultUserManagerFindByEmailAsync(user);
221+
SetupDefaultUserManagerUpdateAsync(IdentityResult.Success);
222+
SetupDefaultSignInManagerPasswordSignInAsync(SignInResult.Success);
223+
224+
// Act
225+
var result = await authController.Login(loginViewModel);
226+
227+
// Assert
228+
Assert.IsInstanceOf(typeof(RedirectResult), result);
229+
Assert.AreEqual(0, authController.ModelState.Count);
230+
Assert.AreEqual(loginViewModel.ReturnUrl, (result as RedirectResult)?.Url);
231+
}
232+
233+
[Test]
234+
public async Task Login_WhenUserMustNotChangePasswordAndEmailWithPasswordAreCorrectAndReturnUrlIsEmpty_ReturnsRedirectToLogin()
235+
{
236+
// Arrange
237+
var user = UserGenerator.Generate();
238+
var loginViewModel = CreateLoginViewModelFromData(user.UserName, returnUrl: string.Empty);
239+
user.MustChangePassword = false;
240+
SetupDefaultUserManagerFindByEmailAsync(user);
241+
SetupDefaultUserManagerUpdateAsync(IdentityResult.Success);
242+
SetupDefaultSignInManagerPasswordSignInAsync(SignInResult.Success);
243+
244+
// Act
245+
var result = await authController.Login(loginViewModel);
246+
247+
// Assert
248+
Assert.IsInstanceOf(typeof(RedirectResult), result);
249+
Assert.AreEqual(0, authController.ModelState.Count);
250+
Assert.AreEqual(nameof(AuthController.Login), (result as RedirectResult)?.Url);
251+
}
252+
253+
[Test]
254+
public async Task ChangePasswordLogin_WhenUserMustChangePasswordAndEmailWithPasswordsAreCorrectAndReturnUrlIsNotEmpty_ReturnsRedirectToReturnUrl()
255+
{
256+
// Arrange
257+
var user = UserGenerator.Generate();
258+
user.MustChangePassword = true;
259+
var changePasswordLoginViewModel = CreateChangePasswordLoginViewModelFromData();
260+
SetupDefaultUserManagerFindByEmailAsync(user);
261+
fakeUserManagerAdditionalService.Setup(s =>
262+
s.ChangePasswordWithRequiredMustChangePasswordAsync(user, It.IsAny<string>(), It.IsAny<string>()))
263+
.ReturnsAsync(() => { user.MustChangePassword = false; return IdentityResult.Success;});
264+
SetupDefaultSignInManagerCheckPasswordSignInAsync(SignInResult.Success);
265+
fakeSignInManager.Setup(s => s.GetExternalAuthenticationSchemesAsync())
266+
.ReturnsAsync(new List<AuthenticationScheme>());
267+
SetupDefaultUserManagerUpdateAsync(IdentityResult.Success);
268+
SetupDefaultSignInManagerPasswordSignInAsync(SignInResult.Success);
269+
270+
// Act
271+
var result = await authController.ChangePasswordLogin(changePasswordLoginViewModel);
272+
273+
// Assert
274+
Assert.IsInstanceOf(typeof(RedirectResult), result);
275+
Assert.AreEqual(0, authController.ModelState.Count);
276+
Assert.AreEqual(changePasswordLoginViewModel.ReturnUrl, (result as RedirectResult)?.Url);
277+
}
278+
279+
[Test]
280+
public async Task ChangePasswordLogin_WhenUserMustChangePasswordAndEmailWithPasswordsAreCorrectAndReturnUrlIsEmpty_ReturnsRedirectToLogin()
281+
{
282+
// Arrange
283+
var user = UserGenerator.Generate();
284+
user.MustChangePassword = true;
285+
var changePasswordLoginViewModel = CreateChangePasswordLoginViewModelFromData(returnUrl: string.Empty);
286+
SetupDefaultUserManagerFindByEmailAsync(user);
287+
fakeUserManagerAdditionalService.Setup(s =>
288+
s.ChangePasswordWithRequiredMustChangePasswordAsync(user, It.IsAny<string>(), It.IsAny<string>()))
289+
.ReturnsAsync(() => { user.MustChangePassword = false; return IdentityResult.Success;});
290+
SetupDefaultSignInManagerCheckPasswordSignInAsync(SignInResult.Success);
291+
fakeSignInManager.Setup(s => s.GetExternalAuthenticationSchemesAsync())
292+
.ReturnsAsync(new List<AuthenticationScheme>());
293+
SetupDefaultUserManagerUpdateAsync(IdentityResult.Success);
294+
SetupDefaultSignInManagerPasswordSignInAsync(SignInResult.Success);
295+
296+
// Act
297+
var result = await authController.ChangePasswordLogin(changePasswordLoginViewModel);
298+
299+
// Assert
300+
Assert.IsInstanceOf(typeof(RedirectResult), result);
301+
Assert.AreEqual(0, authController.ModelState.Count);
302+
Assert.AreEqual(nameof(AuthController.Login), (result as RedirectResult)?.Url);
303+
}
304+
305+
[Test]
306+
public async Task ChangePasswordLogin_WhenUserMustNotChangePassword_ReturnsViewWithModelError()
307+
{
308+
// Arrange
309+
var user = UserGenerator.Generate();
310+
user.MustChangePassword = false;
311+
var changePasswordLoginViewModel = CreateChangePasswordLoginViewModelFromData(returnUrl: string.Empty);
312+
SetupDefaultUserManagerFindByEmailAsync(user);
313+
314+
// Act
315+
var result = await authController.ChangePasswordLogin(changePasswordLoginViewModel);
316+
317+
// Assert
318+
Assert.IsInstanceOf(typeof(ViewResult), result);
319+
Assert.AreEqual(1, authController.ModelState.Count);
320+
}
321+
322+
[Test] public async Task ChangePasswordLogin_WhenUserMustChangePasswordAndEmailWithPasswordAreNotCorrect_ReturnsViewWithModelError()
323+
{
324+
// Arrange
325+
var user = UserGenerator.Generate();
326+
user.MustChangePassword = true;
327+
var changePasswordLoginViewModel = CreateChangePasswordLoginViewModelFromData();
328+
SetupDefaultUserManagerFindByEmailAsync(user);
329+
SetupDefaultSignInManagerCheckPasswordSignInAsync(SignInResult.Failed);
330+
331+
// Act
332+
var result = await authController.ChangePasswordLogin(changePasswordLoginViewModel);
333+
334+
// Assert
335+
Assert.IsInstanceOf(typeof(ViewResult), result);
336+
Assert.AreEqual(1, authController.ModelState.Count);
337+
}
338+
339+
[Test] public async Task ChangePasswordLogin_WhenUserNotFound_ReturnsViewWithModelError()
340+
{
341+
// Arrange
342+
var changePasswordLoginViewModel = CreateChangePasswordLoginViewModelFromData();
343+
SetupDefaultUserManagerFindByEmailAsync(null);
344+
345+
// Act
346+
var result = await authController.ChangePasswordLogin(changePasswordLoginViewModel);
347+
348+
// Assert
349+
Assert.IsInstanceOf(typeof(ViewResult), result);
350+
Assert.AreEqual(1, authController.ModelState.Count);
351+
}
352+
171353
[Test]
172354
public void Register_WithoutReturnUrl_ReturnsViewResult()
173355
{
@@ -339,4 +521,51 @@ public static IEnumerable<AuthenticationScheme> EmptySchemes
339521
{
340522
get => new List<AuthenticationScheme>();
341523
}
524+
525+
private void SetupDefaultUserManagerFindByEmailAsync(User user)
526+
=> fakeUserManager.Setup(s => s.FindByEmailAsync(It.IsAny<string>())).ReturnsAsync(user);
527+
528+
private void SetupDefaultSignInManagerCheckPasswordSignInAsync(SignInResult signInResult)
529+
=> fakeSignInManager.Setup(s => s
530+
.CheckPasswordSignInAsync(It.IsAny<User>(), It.IsAny<string>(), false))
531+
.ReturnsAsync(signInResult);
532+
533+
private void SetupDefaultUserManagerUpdateAsync(IdentityResult identityResult)
534+
=> fakeUserManager.Setup(s => s
535+
.UpdateAsync(It.IsAny<User>())).ReturnsAsync(identityResult);
536+
537+
private void SetupDefaultSignInManagerPasswordSignInAsync(SignInResult signInResult)
538+
=> fakeSignInManager.Setup(s => s.PasswordSignInAsync(It.IsAny<string>(), It.IsAny<string>(), false, false))
539+
.ReturnsAsync(signInResult);
540+
private static LoginViewModel CreateLoginViewModelFromData(
541+
string email = "[email protected]",
542+
string password = "RandomPassword3%",
543+
string returnUrl = "RandomReturnUrl",
544+
IEnumerable<AuthenticationScheme> authenticationSchemes = default)
545+
{
546+
return new LoginViewModel
547+
{
548+
Username = email,
549+
Password = password,
550+
ReturnUrl = returnUrl,
551+
ExternalProviders = authenticationSchemes
552+
};
553+
}
554+
555+
private static ChangePasswordLoginViewModel CreateChangePasswordLoginViewModelFromData(
556+
string email = "[email protected]",
557+
string password = "RandomPassword5%",
558+
string newPassword = "RandomPassword3%",
559+
string confirmNewPassword = "RandomPassword3%",
560+
string returnUrl = "RandomReturnUrl")
561+
{
562+
return new ChangePasswordLoginViewModel
563+
{
564+
Email = email,
565+
CurrentPassword = password,
566+
NewPassword = newPassword,
567+
ConfirmNewPassword = confirmNewPassword,
568+
ReturnUrl = returnUrl
569+
};
570+
}
342571
}

0 commit comments

Comments
 (0)