sc-portal/app/src/app/security/security.component.ts

543 lines
18 KiB
TypeScript

import { Component, OnInit, OnDestroy } from '@angular/core';
import { CdkDragDrop, moveItemInArray, transferArrayItem, copyArrayItem, CdkDrag, CdkDropList } from '@angular/cdk/drag-drop';
import { SecurityService } from './helpers/security.service';
import { User } from './models/user';
import { first, mergeMap, switchMap, takeUntil, tap } from 'rxjs/operators';
import { forkJoin, Subject } from 'rxjs';
import { Role } from './models/role';
import { Policy } from './models/policy';
import { ToastrService } from 'ngx-toastr';
import { BsModalService } from 'ngx-bootstrap/modal';
import { UserEditorComponent } from './user-editor/user-editor.component';
import { PolicyEditorComponent } from './policy-editor/policy-editor.component';
import { RolePoliciesEditorComponent } from './role-policies-editor/role-policies-editor.component';
import { UserRolesEditorComponent } from './user-roles-editor/user-roles-editor.component';
import { ConfirmationDialogComponent } from '../components/confirmation-dialog/confirmation-dialog.component';
import { PromptDialogComponent } from '../components/prompt-dialog/prompt-dialog.component';
import { RolePolicy } from './models/role-policy';
import { RoleUser } from './models/role-user';
import { Title } from "@angular/platform-browser";
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-security',
templateUrl: './security.component.html',
styleUrls: ['./security.component.scss']
})
export class SecurityComponent implements OnInit, OnDestroy
{
users: User[];
roles: Role[];
policies: Policy[];
private destroy$ = new Subject();
// ----------------------------------------------------------------------------------------------------------------
constructor(private readonly securityService: SecurityService,
private readonly modalService: BsModalService,
private readonly toastr: ToastrService,
private readonly titleService: Title,
private readonly translationService: TranslateService)
{
translationService.get('security.title').pipe(first()).subscribe(x => titleService.setTitle(`Spearhead - ${x}`));
forkJoin({
users: securityService.getUsers(),
roles: securityService.getRoles(),
policies: securityService.getPolicies()
})
.subscribe(response =>
{
// Roles has links to both Users and Policies
this.roles = response.roles;
this.policies = response.policies;
const userRoles = {};
for (const role of response.roles)
{
for (const member of role.members)
{
userRoles[member.id] = userRoles[member.id] || [];
userRoles[member.id].push({
id: role.id,
name: role.name
});
}
}
this.users = response.users.map(x =>
{
let user = new User();
user = Object.assign(user, x);
user.roles = userRoles[x.id] || [];
return user;
});
});
}
// ----------------------------------------------------------------------------------------------------------------
drop = (event: CdkDragDrop<string[]>) =>
{
if (event.previousContainer === event.container)
//moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
return;
//copyArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex);
switch (event.previousContainer.id)
{
case 'policies':
return this.addPolicyToRole(event.item.data, this.roles.find(x => x.id === event.container.data['id']));
case 'roles':
return this.addRoleToUser(event.item.data, this.users.find(x => x.id === event.container.data['id']));
}
}
// ----------------------------------------------------------------------------------------------------------------
usersEnterPredicate = (drag: CdkDrag, drop: CdkDropList) =>
{
if (drag.dropContainer.id !== 'roles')
return false;
const user = this.users.find(x => x.id === drop.data.id);
return !user.roles.find(x => x.id === drag.data.id);
}
// ----------------------------------------------------------------------------------------------------------------
rolesEnterPredicate = (drag: CdkDrag, drop: CdkDropList) =>
{
if (drag.dropContainer.id !== 'policies')
return false;
const role = this.roles.find(x => x.id === drop.data.id);
return !role.policies.find(x => x.id === drag.data.id);
}
// ----------------------------------------------------------------------------------------------------------------
noReturnPredicate(drag: CdkDrag, drop: CdkDropList)
{
return false;
}
// ----------------------------------------------------------------------------------------------------------------
noSortPredicate()
{
return false;
}
// ----------------------------------------------------------------------------------------------------------------
showUserEditor(user?: User, changePassword = false)
{
if (user)
user.working = true;
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: { user, changePassword }
};
const modalRef = this.modalService.show(UserEditorComponent, modalConfig);
modalRef.setClass('modal-lg');
modalRef.content.save.pipe(first()).subscribe(x =>
{
if (changePassword)
{
this.toastr.info(`The password has been update for user "${user.login}"`);
}
else if (user)
{
this.toastr.info(`The details have been updated for user "${user.login}"`);
user = Object.assign(user, x);
}
else
{
this.users.push(x);
user.working = false;
}
});
}
// ----------------------------------------------------------------------------------------------------------------
showRoleEditor(role?: Role)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: {
value: role?.name,
required: true,
title: role ? 'Change role name' : 'Create role',
prompt: 'Type in the name for this role',
placeholder: 'Role name',
saveButtonText: role ? 'Save changes' : 'Create role'
}
};
const modalRef = this.modalService.show(PromptDialogComponent, modalConfig);
modalRef.content.save
.pipe(
first(),
switchMap(roleName =>
{
if (role)
return this.securityService.editRole(role.id, roleName);
return this.securityService.addRole(roleName);
})
)
.subscribe(x =>
{
const message = role
? `The "${role.name}" role has been renamed to "${x.name}"`
: `The "${x.name}" role has been created`;
if (role)
{
const index = this.roles.findIndex(p => p.id === x.id);
if (index >= 0)
this.roles.splice(index, 1, x);
// Also update the users that use this role
for (const user of this.users)
for (const userRole of user.roles)
if (role.id === userRole.id)
userRole.name = x.name;
}
else
this.roles.push(x);
this.toastr.info(message);
});
}
// ----------------------------------------------------------------------------------------------------------------
deleteRole(role: Role)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: {
prompt: `Are you sure you wish to permanently delete the "${role.name}" role?`,
confirmButtonText: 'Yes, delete this role',
declineButtonText: 'No, keep it',
confirmByDefault: false
}
};
const modalRef = this.modalService.show(ConfirmationDialogComponent, modalConfig);
modalRef.content.confirm.pipe(
first(),
switchMap(() => this.securityService.removeRole(role))
)
.subscribe(() =>
{
const index = this.roles.findIndex(p => p.id === role.id);
if (index >= 0)
this.roles.splice(index, 1);
// Also remove this role from all the associated users
for (const user of this.users)
user.roles = user.roles.filter(rp => rp.id !== role.id);
this.toastr.info(`The "${role.name}" role has been succesfuly removed`);
}, err =>
{
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Faild to remove the "${role.name}" role (${errorDetails})`);
});
}
// ----------------------------------------------------------------------------------------------------------------
showPolicyEditor(policy?: Policy)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: { policy }
};
const modalRef = this.modalService.show(PolicyEditorComponent, modalConfig);
modalRef.content.save.pipe(first()).subscribe(x =>
{
if (policy)
{
const index = this.policies.findIndex(p => p.id === policy.id);
if (index >= 0)
this.policies.splice(index, 1, x);
// Also update the roles that use this policy
for (const role of this.roles)
for (const rolePolicy of role.policies)
if (policy.id === rolePolicy.id)
rolePolicy.name = x.name;
}
else
{
this.policies = this.policies || [];
this.policies.push(x);
}
});
}
// ----------------------------------------------------------------------------------------------------------------
deletePolicy(policy: Policy)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: {
prompt: `Are you sure you wish to permanently delete the "${policy.name}" policy?`,
confirmButtonText: 'Yes, delete this policy',
declineButtonText: 'No, keep it',
confirmByDefault: false
}
};
const modalRef = this.modalService.show(ConfirmationDialogComponent, modalConfig);
modalRef.content.confirm.pipe(
first(),
switchMap(() => this.securityService.removePolicy(policy))
)
.subscribe(() =>
{
const index = this.policies.findIndex(p => p.id === policy.id);
if (index >= 0)
this.policies.splice(index, 1);
// Also remove this policy from all the associated roles
for (const role of this.roles)
role.policies = role.policies.filter(rp => rp.id !== policy.id);
this.toastr.info(`The "${policy.name}" policy has been succesfuly removed`);
}, err =>
{
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Faild to remove the "${policy.name}" policy ${errorDetails}`);
});
}
// ----------------------------------------------------------------------------------------------------------------
assignPolicyToRoles(policy: Policy)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: {}
};
const modalRef = this.modalService.show(RolePoliciesEditorComponent, modalConfig);
modalRef.content.save.pipe(first()).subscribe(x => { });
}
// ----------------------------------------------------------------------------------------------------------------
addPolicyToRole(policy: Policy, role: Role)
{
const rolePolicy = new RolePolicy();
rolePolicy.id = policy.id;
rolePolicy.name = policy.name;
// This causes the UI to add the item in the list. In case of an error we have a compensation action
// that will remove this item from the list. We do this to avoid having the user see an empty list
// while the server responds.
role.policies = role.policies || [];
role.policies.push(rolePolicy);
// We don't specify the second parameter of securityService.addPolicyToRole() because we've already
// updated the role's policies above
this.securityService.addPolicyToRole(role)
.subscribe(x =>
{
this.toastr.info(`The "${policy.name}" policy has been added to the "${role.name}" role`);
}, err =>
{
// Compensation action
const index = role.policies.findIndex(x => x.id === policy.id);
if (index >= 0)
role.policies.splice(index, 1);
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Failed to add the "${policy.name}" policy to the "${role.name}" role ${errorDetails}`);;
});
}
// ----------------------------------------------------------------------------------------------------------------
removePolicyFromRole(policy: Policy, role: Role)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: {
prompt: `Are you sure you wish to remove the "${policy.name}" policy from the "${role.name}" role?`,
confirmButtonText: 'Yes, remove it',
declineButtonText: 'No, keep it',
confirmByDefault: false
}
};
const modalRef = this.modalService.show(ConfirmationDialogComponent, modalConfig);
modalRef.content.confirm
.pipe(
first(),
switchMap(() => this.securityService.removePolicyFromRole(policy.id, role))
)
.subscribe(x =>
{
const index = role.policies.findIndex(rp => rp.id === policy.id);
if (index >= 0)
{
role.policies.splice(index, 1);
this.toastr.info(`The "${policy.name}" policy has been removed from the "${role.name}" role`);
}
}, err =>
{
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Failed to remove the "${policy.name}" policy from the "${role.name}" role ${errorDetails}`);
});
}
// ----------------------------------------------------------------------------------------------------------------
addRoleToUser(role: Role, user: User)
{
const roleUser = new RoleUser();
roleUser.id = user.id;
roleUser.type = 'subuser';
roleUser.default = !role.members.length;
// This causes the UI to add the item in the list. In case of an error we have a compensation action
// that will remove this item from the list. We do this to avoid having the user see an empty list
// while the server responds.
user.roles = user.roles || [];
user.roles.push(role);
this.toastr.info(`Adding the "${role.name}" role to the "${user.login}" user...`);
this.securityService.addRoleToUser(role, roleUser)
.pipe(
switchMap(() => this.securityService
.getRoleUntil(role, x => x.members?.some(m => m.id === roleUser.id))
.pipe(takeUntil(this.destroy$))
))
.subscribe(x =>
{
this.toastr.info(`The "${role.name}" role has been added to the "${user.login}" user`);
}, err =>
{
// TODO: Investigate further why this method returns a 500 error, even though it succeeds
// Compensation action
const index = user.roles.findIndex(x => x.id === role.id);
if (index >= 0)
user.roles.splice(index, 1);
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Failed to add the "${role.name}" role to the "${user.login}" user ${errorDetails}`);
});
}
// ----------------------------------------------------------------------------------------------------------------
removeRoleFromUser(roleUser: RoleUser, user: User)
{
const role = this.roles.find(x => x.id === roleUser.id);
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: {
prompt: `Are you sure you wish to remove the "${role.name}" role from the "${user.login}" user?`,
confirmButtonText: 'Yes, remove it',
declineButtonText: 'No, keep it',
confirmByDefault: false
}
};
const modalRef = this.modalService.show(ConfirmationDialogComponent, modalConfig);
modalRef.content.confirm
.pipe(
first(),
tap(() => this.toastr.info(`Removing the "${role.name}" role from the "${user.login}" user...`)),
switchMap(() => this.securityService.removeRoleFromUser(role, user.id)),
switchMap(() => this.securityService
.getRoleUntil(role, x => !x.members?.some(m => m.id === roleUser.id))
.pipe(takeUntil(this.destroy$))
)
)
.subscribe(x =>
{
let index = user.roles.findIndex(r => r.id === role.id);
if (index >= 0)
{
// Remove the role from the user's list of roles
user.roles.splice(index, 1);
// Also remove the role from the list of role members
index = role.members.findIndex(rm => rm.id === user.id);
if (index >= 0)
role.members.splice(index, 1);
this.toastr.info(`The "${role.name}" role has been removed from the "${user.login}" user`);
}
}, err =>
{
const errorDetails = err.error?.message ? `(${err.error.message})` : '';
this.toastr.error(`Failed to remove the "${role.name}" role from the "${user.login}" user ${errorDetails}`);
});
}
// ----------------------------------------------------------------------------------------------------------------
assignRoleToUsers(role: Role)
{
const modalConfig = {
ignoreBackdropClick: true,
keyboard: false,
animated: true,
initialState: {}
};
const modalRef = this.modalService.show(UserRolesEditorComponent, modalConfig);
modalRef.content.save.pipe(first()).subscribe(x => { });
}
// ----------------------------------------------------------------------------------------------------------------
get roleDropLists()
{
return this.roles ? new Array(this.roles.length).fill(0).map((x, i) => `role${i}`) : [];
}
// ----------------------------------------------------------------------------------------------------------------
get userDropLists()
{
return this.users ? new Array(this.users.length).fill(0).map((x, i) => `user${i}`) : [];
}
// ----------------------------------------------------------------------------------------------------------------
ngOnInit(): void
{
}
// ----------------------------------------------------------------------------------------------------------------
ngOnDestroy(): void
{
this.destroy$.next();
}
}