From 6ee8320c2b901800a4b4e24a82b46ff3067e1f52 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 1 Feb 2022 07:40:41 -0800 Subject: [PATCH] Ability to restrict a user's ability to change passwords (#1018) * Implemented a new role "Change Password". This role allows you to change your own password. By default, all users will have it. A user can have it removed arbitrarliy. Removed components that are no longer going to be used. * Cleaned up some code --- API/Constants/PolicyConstants.cs | 6 +- API/Controllers/AccountController.cs | 41 +--------- API/Data/MigrateChangePasswordRoles.cs | 18 +++++ API/Data/Repositories/UserRepository.cs | 6 ++ API/Extensions/IdentityServiceExtensions.cs | 1 + API/Startup.cs | 21 ++---- UI/Web/src/app/_services/account.service.ts | 4 + UI/Web/src/app/_services/member.service.ts | 3 - .../edit-rbs-modal.component.html | 23 ------ .../edit-rbs-modal.component.scss | 0 .../edit-rbs-modal.component.ts | 74 ------------------- UI/Web/src/app/admin/admin.module.ts | 2 - .../manage-users/manage-users.component.html | 4 - .../manage-users/manage-users.component.ts | 11 --- .../user-preferences.component.html | 5 +- .../user-preferences.component.ts | 3 + 16 files changed, 48 insertions(+), 174 deletions(-) create mode 100644 API/Data/MigrateChangePasswordRoles.cs delete mode 100644 UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.html delete mode 100644 UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.scss delete mode 100644 UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.ts diff --git a/API/Constants/PolicyConstants.cs b/API/Constants/PolicyConstants.cs index a57b279ec..baf18b55e 100644 --- a/API/Constants/PolicyConstants.cs +++ b/API/Constants/PolicyConstants.cs @@ -20,8 +20,12 @@ namespace API.Constants /// Used to give a user ability to download files from the server /// public const string DownloadRole = "Download"; + /// + /// Used to give a user ability to change their own password + /// + public const string ChangePasswordRole = "Change Password"; public static readonly ImmutableArray ValidRoles = - ImmutableArray.Create(AdminRole, PlebRole, DownloadRole); + ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole); } } diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 57dcf6b38..afb8f9ba7 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -73,7 +73,7 @@ namespace API.Controllers _logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName); var user = await _userManager.Users.SingleAsync(x => x.UserName == resetPasswordDto.UserName); - if (resetPasswordDto.UserName != User.GetUsername() && !User.IsInRole(PolicyConstants.AdminRole)) + if (resetPasswordDto.UserName != User.GetUsername() && !(User.IsInRole(PolicyConstants.AdminRole) || User.IsInRole(PolicyConstants.ChangePasswordRole))) return Unauthorized("You are not permitted to this operation."); var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password); @@ -245,45 +245,6 @@ namespace API.Controllers f => (string) f.GetValue(null)).Values.ToList(); } - /// - /// Sets the given roles to the user. - /// - /// - /// - [HttpPost("update-rbs")] - public async Task UpdateRoles(UpdateRbsDto updateRbsDto) - { - var user = await _userManager.Users - .Include(u => u.UserPreferences) - .SingleOrDefaultAsync(x => x.NormalizedUserName == updateRbsDto.Username.ToUpper()); - if (updateRbsDto.Roles.Contains(PolicyConstants.AdminRole) || - updateRbsDto.Roles.Contains(PolicyConstants.PlebRole)) - { - return BadRequest("Invalid Roles"); - } - - var existingRoles = (await _userManager.GetRolesAsync(user)) - .Where(s => s != PolicyConstants.AdminRole && s != PolicyConstants.PlebRole) - .ToList(); - - // Find what needs to be added and what needs to be removed - var rolesToRemove = existingRoles.Except(updateRbsDto.Roles); - var result = await _userManager.AddToRolesAsync(user, updateRbsDto.Roles); - - if (!result.Succeeded) - { - await _unitOfWork.RollbackAsync(); - return BadRequest("Something went wrong, unable to update user's roles"); - } - if ((await _userManager.RemoveFromRolesAsync(user, rolesToRemove)).Succeeded) - { - return Ok(); - } - - await _unitOfWork.RollbackAsync(); - return BadRequest("Something went wrong, unable to update user's roles"); - - } /// /// Resets the API Key assigned with a user diff --git a/API/Data/MigrateChangePasswordRoles.cs b/API/Data/MigrateChangePasswordRoles.cs new file mode 100644 index 000000000..1a5681f5a --- /dev/null +++ b/API/Data/MigrateChangePasswordRoles.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using API.Constants; +using API.Entities; +using Microsoft.AspNetCore.Identity; + +namespace API.Data; + +public static class MigrateChangePasswordRoles +{ + public static async Task Migrate(IUnitOfWork unitOfWork, UserManager userManager) + { + foreach (var user in await unitOfWork.UserRepository.GetAllUsers()) + { + await userManager.RemoveFromRoleAsync(user, "ChangePassword"); + await userManager.AddToRoleAsync(user, PolicyConstants.ChangePasswordRole); + } + } +} diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 120acfc53..9eb276995 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -52,6 +52,7 @@ public interface IUserRepository Task GetUserWithReadingListsByUsernameAsync(string username); Task> GetAllBookmarksByIds(IList bookmarkIds); Task GetUserByEmailAsync(string email); + Task> GetAllUsers(); } public class UserRepository : IUserRepository @@ -214,6 +215,11 @@ public class UserRepository : IUserRepository return await _context.AppUser.SingleOrDefaultAsync(u => u.Email.ToLower().Equals(email.ToLower())); } + public async Task> GetAllUsers() + { + return await _context.AppUser.ToListAsync(); + } + public async Task> GetAdminUsersAsync() { return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs index 763f3a5a4..16404949b 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -67,6 +67,7 @@ namespace API.Extensions { opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole)); opt.AddPolicy("RequireDownloadRole", policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole)); + opt.AddPolicy("RequireChangePasswordRole", policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole)); }); return services; diff --git a/API/Startup.cs b/API/Startup.cs index 6fd0d2ff0..fce12b69d 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Sockets; using System.Threading.Tasks; using API.Data; +using API.Entities; using API.Extensions; using API.Middleware; using API.Services; @@ -20,6 +21,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; @@ -140,27 +142,16 @@ namespace API Task.Run(async () => { // Apply all migrations on startup - // If we have pending migrations, make a backup first - //var isDocker = new OsInfo(Array.Empty()).IsDocker; var logger = serviceProvider.GetRequiredService>(); var context = serviceProvider.GetRequiredService(); - // var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); - // if (pendingMigrations.Any()) - // { - // logger.LogInformation("Performing backup as migrations are needed"); - // await backupService.BackupDatabase(); - // } - // - // await context.Database.MigrateAsync(); - // var roleManager = serviceProvider.GetRequiredService>(); - // - // await Seed.SeedRoles(roleManager); - // await Seed.SeedSettings(context, directoryService); - // await Seed.SeedUserApiKeys(context); + var userManager = serviceProvider.GetRequiredService>(); + await MigrateBookmarks.Migrate(directoryService, unitOfWork, logger, cacheService); + await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager); + var requiresCoverImageMigration = !Directory.Exists(directoryService.CoverImageDirectory); try { diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index c78eaaec6..0d5ee5ed1 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -41,6 +41,10 @@ export class AccountService implements OnDestroy { return user && user.roles.includes('Admin'); } + hasChangePasswordRole(user: User) { + return user && user.roles.includes('Change Password'); + } + hasDownloadRole(user: User) { return user && user.roles.includes('Download'); } diff --git a/UI/Web/src/app/_services/member.service.ts b/UI/Web/src/app/_services/member.service.ts index 5ba08dca4..2c28db2cc 100644 --- a/UI/Web/src/app/_services/member.service.ts +++ b/UI/Web/src/app/_services/member.service.ts @@ -36,9 +36,6 @@ export class MemberService { return this.httpClient.get(this.baseUrl + 'users/has-reading-progress?libraryId=' + librayId); } - updateMemberRoles(username: string, roles: string[]) { - return this.httpClient.post(this.baseUrl + 'account/update-rbs', {username, roles}); - } getPendingInvites() { return this.httpClient.get>(this.baseUrl + 'users/pending'); diff --git a/UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.html b/UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.html deleted file mode 100644 index 205195384..000000000 --- a/UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - diff --git a/UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.scss b/UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.ts b/UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.ts deleted file mode 100644 index 54f27f926..000000000 --- a/UI/Web/src/app/admin/_modals/edit-rbs-modal/edit-rbs-modal.component.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { Member } from 'src/app/_models/member'; -import { AccountService } from 'src/app/_services/account.service'; -import { MemberService } from 'src/app/_services/member.service'; - -// TODO: Remove this component, edit-user will take over - -@Component({ - selector: 'app-edit-rbs-modal', - templateUrl: './edit-rbs-modal.component.html', - styleUrls: ['./edit-rbs-modal.component.scss'] -}) -export class EditRbsModalComponent implements OnInit { - - @Input() member: Member | undefined; - allRoles: string[] = []; - selectedRoles: Array<{selected: boolean, data: string}> = []; - - constructor(public modal: NgbActiveModal, private accountService: AccountService, private memberService: MemberService) { } - - ngOnInit(): void { - this.accountService.getRoles().subscribe(roles => { - roles = roles.filter(item => item != 'Admin' && item != 'Pleb'); // Do not allow the user to modify Account RBS - this.allRoles = roles; - this.selectedRoles = roles.map(item => { - return {selected: false, data: item}; - }); - - this.preselect(); - }); - } - - close() { - this.modal.close(undefined); - } - - save() { - if (this.member?.username === undefined) { - return; - } - - const selectedRoles = this.selectedRoles.filter(item => item.selected).map(item => item.data); - this.memberService.updateMemberRoles(this.member?.username, selectedRoles).subscribe(() => { - if (this.member) { - this.member.roles = selectedRoles; - this.modal.close(this.member); - return; - } - this.modal.close(undefined); - }); - } - - reset() { - this.selectedRoles = this.allRoles.map(item => { - return {selected: false, data: item}; - }); - - - this.preselect(); - } - - preselect() { - if (this.member !== undefined) { - this.member.roles.forEach(role => { - const foundRole = this.selectedRoles.filter(item => item.data === role); - if (foundRole.length > 0) { - foundRole[0].selected = true; - } - }); - } - } - -} diff --git a/UI/Web/src/app/admin/admin.module.ts b/UI/Web/src/app/admin/admin.module.ts index 49bbf1153..8baed6a00 100644 --- a/UI/Web/src/app/admin/admin.module.ts +++ b/UI/Web/src/app/admin/admin.module.ts @@ -12,7 +12,6 @@ import { DirectoryPickerComponent } from './_modals/directory-picker/directory-p import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ResetPasswordModalComponent } from './_modals/reset-password-modal/reset-password-modal.component'; import { ManageSettingsComponent } from './manage-settings/manage-settings.component'; -import { EditRbsModalComponent } from './_modals/edit-rbs-modal/edit-rbs-modal.component'; import { ManageSystemComponent } from './manage-system/manage-system.component'; import { ChangelogComponent } from './changelog/changelog.component'; import { PipeModule } from '../pipe/pipe.module'; @@ -34,7 +33,6 @@ import { EditUserComponent } from './edit-user/edit-user.component'; DirectoryPickerComponent, ResetPasswordModalComponent, ManageSettingsComponent, - EditRbsModalComponent, ManageSystemComponent, ChangelogComponent, InviteUserComponent, diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.html b/UI/Web/src/app/admin/manage-users/manage-users.component.html index 4a591b850..8256f7f79 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.html +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.html @@ -62,10 +62,6 @@ {{role}} - diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.ts b/UI/Web/src/app/admin/manage-users/manage-users.component.ts index bfb534670..632e39208 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.ts +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.ts @@ -8,7 +8,6 @@ import { AccountService } from 'src/app/_services/account.service'; import { ToastrService } from 'ngx-toastr'; import { ResetPasswordModalComponent } from '../_modals/reset-password-modal/reset-password-modal.component'; import { ConfirmService } from 'src/app/shared/confirm.service'; -import { EditRbsModalComponent } from '../_modals/edit-rbs-modal/edit-rbs-modal.component'; import { Subject } from 'rxjs'; import { MessageHubService } from 'src/app/_services/message-hub.service'; import { InviteUserComponent } from '../invite-user/invite-user.component'; @@ -112,16 +111,6 @@ export class ManageUsersComponent implements OnInit, OnDestroy { } } - openEditRole(member: Member) { - const modalRef = this.modalService.open(EditRbsModalComponent); - modalRef.componentInstance.member = member; - modalRef.closed.subscribe((updatedMember: Member) => { - if (updatedMember !== undefined) { - member = updatedMember; - } - }); - } - inviteUser() { const modalRef = this.modalService.open(InviteUserComponent, {size: 'lg'}); modalRef.closed.subscribe((successful: boolean) => { diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html index d2c920bd0..e40eae35c 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html @@ -194,7 +194,7 @@ - +

Change your Password

+ +

You do not have permission to change your password. Reach out to the admin of the server.

+

All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.

diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index 4c7f7ff08..3aec4e8a2 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -11,6 +11,7 @@ import { AccountService } from 'src/app/_services/account.service'; import { NavService } from 'src/app/_services/nav.service'; import { ActivatedRoute } from '@angular/router'; import { SettingsService } from 'src/app/admin/settings.service'; +import { Member } from 'src/app/_models/member'; @Component({ selector: 'app-user-preferences', @@ -28,6 +29,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { passwordChangeForm: FormGroup = new FormGroup({}); user: User | undefined = undefined; isAdmin: boolean = false; + hasChangePasswordRole: boolean = false; passwordsMatch = false; resetPasswordErrors: string[] = []; @@ -85,6 +87,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { if (user) { this.user = user; this.isAdmin = this.accountService.hasAdminRole(user); + this.hasChangePasswordRole = this.accountService.hasChangePasswordRole(user); if (this.fontFamilies.indexOf(this.user.preferences.bookReaderFontFamily) < 0) { this.user.preferences.bookReaderFontFamily = 'default';