passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
730 lines (692 loc) • 30.2 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) Passbolt SA (https://www.passbolt.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 4.1.0
*/
import React from "react";
import PropTypes from "prop-types";
import { withAdministrationWorkspace } from "../../../contexts/AdministrationWorkspaceContext";
import { Trans, withTranslation } from "react-i18next";
import { withAppContext } from "../../../../shared/context/AppContext/AppContext";
import DisplayAdministrationRbacActions from "../DisplayAdministrationWorkspaceActions/DisplayAdministrationRbacsActions/DisplayAdministrationRbacActions";
import { withAdminRbac } from "../../../contexts/Administration/AdministrationRbacContext/AdministrationRbacContext";
import DisplayRbacSection from "./DisplayRbacSection";
import DisplayRbacItem from "./DisplayRbacItem";
import { uiActions } from "../../../../shared/services/rbacs/uiActionEnumeration";
import RolesCollection from "../../../../shared/models/entity/role/rolesCollection";
import RoleApiService from "../../../../shared/services/api/role/roleApiService";
import RbacApiService from "../../../../shared/services/api/rbac/rbacApiService";
import RbacsCollection from "../../../../shared/models/entity/rbac/rbacsCollection";
import RbacEntity from "../../../../shared/models/entity/rbac/rbacEntity";
import FileTextSVG from "../../../../img/svg/file_text.svg";
import { createSafePortal } from "../../../../shared/utils/portals";
import UserAddSVG from "../../../../img/svg/user_add.svg";
import MoreVerticalSVG from "../../../../img/svg/more_vertical.svg";
import DeleteSVG from "../../../../img/svg/delete.svg";
import EditSVG from "../../../../img/svg/edit.svg";
import CreateRole from "../CreateRole/CreateRole";
import { withDialog } from "../../../contexts/DialogContext";
import { withActionFeedback } from "../../../contexts/ActionFeedbackContext";
import { capitalizeFirstLetter } from "../../../../shared/utils/stringUtils";
import Dropdown from "../../Common/Dropdown/Dropdown";
import DropdownButton from "../../Common/Dropdown/DropdownButton";
import DropdownMenu from "../../Common/Dropdown/DropdownMenu";
import DropdownMenuItem from "../../Common/Dropdown/DropdownMenuItem";
import DeleteRole from "../DeleteRole/DeleteRole";
import EditRole from "../EditRole/EditRole";
import { actions } from "../../../../shared/services/rbacs/actionEnumeration";
import UserService from "../../../../shared/services/api/user/userService";
import DeleteRoleNotAllowed from "../DeleteRole/DeleteRoleNotAllowed";
import NotifyError from "../../Common/Error/NotifyError/NotifyError";
/**
* This component allows to display the internationalisation for the administration
*/
class DisplayRbacAdministration extends React.Component {
/**
* @inheritDoc
*/
constructor(props) {
super(props);
this.state = this.defaultState;
this.bindCallbacks();
const apiClientOptions = this.props.context.getApiClientOptions();
this.roleApiService = new this.props.RoleApiService(apiClientOptions);
this.rbacApiService = new this.props.RbacApiService(apiClientOptions);
this.userApiService = new this.props.UserApiService(apiClientOptions);
}
/**
* Returns the default component state
*/
get defaultState() {
return {
roles: null,
};
}
/**
* Bind callbacks
*/
bindCallbacks() {
this.updateRbacControlFunction = this.updateRbacControlFunction.bind(this);
this.handleAddRoleClick = this.handleAddRoleClick.bind(this);
this.createNewRole = this.createNewRole.bind(this);
this.handleDeleteRoleClick = this.handleDeleteRoleClick.bind(this);
this.deleteRole = this.deleteRole.bind(this);
this.handleRenameRoleClick = this.handleRenameRoleClick.bind(this);
this.renameRole = this.renameRole.bind(this);
}
/**
* ComponentDidMount
* Invoked immediately after component is inserted into the tree
* @return {void}
*/
async componentDidMount() {
this.findAndLoadData();
}
/**
* componentWillUnmount
* Use to clear the data from the form in case the user put something that needs to be cleared.
*/
componentWillUnmount() {
this.props.adminRbacContext.clearContext();
}
/**
* Loads all the necessary roles and all Rbac information.
*/
findAndLoadData() {
this.findAndLoadRoles();
this.findAndLoadRbacSettings();
}
/**
* Find and load the roles
* @returns {Promise<void>}
*/
async findAndLoadRoles() {
const apiResponse = await this.roleApiService.findAll();
const rolesDto = apiResponse.body;
const roles = new RolesCollection(rolesDto);
roles.filterOutGuestRole();
this.setState({ roles });
}
/**
* Find and load the rbac settings
* @returns {Promise<void>}
*/
async findAndLoadRbacSettings() {
const apiResponse = await this.rbacApiService.findAll({ ui_action: true, action: true });
const rbacsDto = apiResponse.body;
const rbacs = new RbacsCollection(rbacsDto, true);
this.props.adminRbacContext.setRbacs(rbacs);
}
/**
* Update a rbac setting.
* @param {RoleEntity} role The role to update the rbac for.
* @param {string} actionName The action to update the rbac for.
* @param {string} controlFunction The new control function for the rbac.
*/
updateRbacControlFunction(role, actionName, controlFunction) {
const rbacsUpdated = this.props.adminRbacContext.rbacsUpdated;
const rbac = this.props.adminRbacContext.rbacs.findRbacByRoleAndActionName(role, actionName);
// If the function is has the original one, remove it from the list of changes.
if (rbac.controlFunction === controlFunction) {
rbacsUpdated.remove(rbac);
} else {
// If the control function is not the rbac to the list of changes.
const clonedRbac = new RbacEntity(rbac.toDto({ ui_action: true, action: true }));
clonedRbac.controlFunction = controlFunction;
rbacsUpdated.pushOrReplace(clonedRbac);
}
this.props.adminRbacContext.setRbacsUpdated(rbacsUpdated);
}
/**
* Handles the click on the "Add role" button
* @param {React.Event} event
*/
handleAddRoleClick(event) {
event.preventDefault();
this.props.dialogContext.open(CreateRole, { onSubmit: this.createNewRole });
}
/**
* Creates a new role on the API.
* @param {RoleEntity} roleEntity
*/
async createNewRole(roleEntity) {
try {
await this.roleApiService.create(roleEntity.toDto());
this.findAndLoadData();
await this.props.actionFeedbackContext.displaySuccess(this.props.t("The role has been created successfully."));
} catch (error) {
this.props.dialogContext.open(NotifyError, { error });
}
}
/**
* Handle role delete click event
* @param {React.Event} event
* @param {RoleEntity} role
*/
async handleDeleteRoleClick(event, role) {
event.preventDefault();
// TODO: use findByRoleId when it's implemented on the API
const apiResponse = await this.userApiService.findAll();
const usersDto = apiResponse.body.filter((user) => user.role_id === role.id);
if (usersDto.length > 0) {
this.props.dialogContext.open(DeleteRoleNotAllowed, { role: role, usersCount: usersDto.length });
} else {
this.props.dialogContext.open(DeleteRole, { role: role, onSubmit: this.deleteRole });
}
}
/**
* Deletes the given role from the API
* @param {RoleEntity} roleEntity
*/
async deleteRole(roleEntity) {
try {
await this.roleApiService.delete(roleEntity.id);
this.findAndLoadData();
await this.props.actionFeedbackContext.displaySuccess(this.props.t("The role has been deleted successfully."));
} catch (error) {
this.props.dialogContext.open(NotifyError, { error });
}
}
/**
* Handle role rename click event
* @param {React.Event} event
* @param {RoleEntity} role
*/
async handleRenameRoleClick(event, role) {
event.preventDefault();
this.props.dialogContext.open(EditRole, { role: role, onSubmit: this.renameRole });
}
/**
* Rename the given role on the API
* @param {RoleEntity} roleEntity
*/
async renameRole(roleEntity) {
try {
await this.roleApiService.update(roleEntity.id, roleEntity.toUpdateDto());
this.findAndLoadData();
await this.props.actionFeedbackContext.displaySuccess(this.props.t("The role has been updated successfully."));
} catch (error) {
this.props.dialogContext.open(NotifyError, { error });
}
}
/**
* Is the user allowed to use the tags capability
* @returns {boolean}
*/
get canIUseTags() {
return this.props.context.siteSettings.canIUse("tags");
}
/**
* Is the user allowed to use the desktop export capability
* @returns {boolean}
*/
get canIUseDesktop() {
return this.props.context.siteSettings.canIUse("desktop");
}
/**
* Is the user allowed to use the mobile export capability
* @returns {boolean}
*/
get canIUseMobile() {
return this.props.context.siteSettings.canIUse("mobile");
}
/**
* Is the user allowed to use the folders capability
* @returns {boolean}
*/
get canIUseFolders() {
return this.props.context.siteSettings.canIUse("folders");
}
/**
* Is the user allowed to use preview password capability.
* @returns {boolean}
*/
get canIUsePreviewPassword() {
return this.props.context.siteSettings.canIUse("previewPassword");
}
/**
* Is the user allowed to use export capability.
* @returns {boolean}
*/
get canIUseExport() {
return this.props.context.siteSettings.canIUse("export");
}
/**
* Is the user allowed to use import capability.
* @returns {boolean}
*/
get canIUseImport() {
return this.props.context.siteSettings.canIUse("import");
}
/**
* Is the user allowed to use account recovery capability.
* @returns {boolean}
*/
get canIUseAccountRecovery() {
return this.props.context.siteSettings.canIUse("accountRecovery");
}
/**
* Check if the component is ready to display its rows.
* @returns {boolean}
*/
get isReady() {
return this.state.roles !== null;
}
/**
* Returns true if a new role can be added.
* @returns {boolean}
*/
get canAddNewRole() {
const schema = RolesCollection.getSchema();
const maxRoleCount = schema.maxItems - 1; // -1 as guest role is not part of this component collections but is counted on the API.
return this.state.roles !== null && this.state.roles.length < maxRoleCount;
}
/**
* Returns the role name translated if it is a reserved role name.
* @param {RoleEntity} roleEntity
* @returns {string}
*/
getTranslatedRoleName(roleEntity) {
if (!roleEntity.isAReservedRole()) {
return roleEntity.name;
}
return this.props.t(roleEntity.name);
}
/**
* Get the custom roles with user as first position
* @return {RolesCollection}
*/
get customizableRoles() {
const roles = [];
if (this.state.roles !== null) {
const userRoles = this.state.roles.items.find((role) => role.isUser());
const customRoles = this.state.roles.items.filter((role) => !role.isAdmin() && !role.isUser());
roles.push(userRoles, ...customRoles);
}
return new RolesCollection(roles, { validate: false });
}
/**
* Return a blank section
* @return {React.JSX.Element}
*/
blankColumnSectionForRoles() {
const rows = [];
this.state.roles?.items.forEach((item, index) => {
rows.push(
<div className="flex-item" key={index}>
</div>,
);
});
return <>{rows}</>;
}
/**
* Render the component
* @returns {JSX}
*/
render() {
const hasSaveWarning = this.props.adminRbacContext.hasSettingsChanges();
const customizableRoles = this.customizableRoles;
const rolesCount = this.state.roles?.length;
return (
<div className="row">
<div className="rbac-settings main-column">
<div className="main-content">
<h3 className="title">
<Trans>Role-Based Access Control</Trans>
</h3>
<div className="section-header">
<p>
<Trans>In this section you can define access controls for each user role.</Trans>
</p>
<button
type="button"
className="button"
onClick={this.handleAddRoleClick}
disabled={!this.canAddNewRole}
title={!this.canAddNewRole ? this.props.t("Maximum number of roles reached") : ""}
>
<UserAddSVG /> <Trans>Add role</Trans>
</button>
</div>
<form className="form">
<div className="flex-container outer">
<div className="flex-container inner header-flex">
<div className="flex-item first"> </div>
<div className="flex-item centered">
<span className="ellipsis" title={this.props.t("Admin")}>
<Trans>Admin</Trans>
</span>
</div>
{customizableRoles.items.map((role) => (
<div className="flex-item centered" key={role.id}>
<span className="ellipsis" title={capitalizeFirstLetter(this.getTranslatedRoleName(role))}>
{capitalizeFirstLetter(this.getTranslatedRoleName(role))}
</span>
{!role.isAReservedRole() && (
<Dropdown>
<DropdownButton className="more button-action-icon link no-border">
<MoreVerticalSVG />
</DropdownButton>
<DropdownMenu className="menu-action-contextual" direction="left">
<DropdownMenuItem>
<button
id="rename_role_action"
type="button"
className="no-border"
onClick={(event) => this.handleRenameRoleClick(event, role)}
>
<EditSVG />
<span>
<Trans>Rename</Trans>
</span>
</button>
</DropdownMenuItem>
<DropdownMenuItem>
<button
id="delete_role_action"
type="button"
className="no-border"
onClick={(event) => this.handleDeleteRoleClick(event, role)}
>
<DeleteSVG />
<span>
<Trans>Delete</Trans>
</span>
</button>
</DropdownMenuItem>
</DropdownMenu>
</Dropdown>
)}
</div>
))}
</div>
{this.isReady && (
<>
<div className="flex-container inner header-flex">
<div className="flex-item first">
<label>
<Trans>API Permissions</Trans>
</label>
</div>
{this.blankColumnSectionForRoles()}
</div>
<DisplayRbacSection label={this.props.t("Group management")} level={1} rolesCount={rolesCount}>
<DisplayRbacItem
label={this.props.t("Create a group")}
actionName={actions.GROUPS_ADD}
level={2}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
</DisplayRbacSection>
{this.canIUseAccountRecovery && (
<>
<DisplayRbacSection
label={this.props.t("Account recovery request")}
level={1}
rolesCount={rolesCount}
>
<DisplayRbacItem
label={this.props.t("Account recovery request view")}
actionName={actions.ACCOUNT_RECOVERY_REQUEST_VIEW}
level={2}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
<DisplayRbacItem
label={this.props.t("Account recovery request index")}
actionName={actions.ACCOUNT_RECOVERY_REQUEST_INDEX}
level={2}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
<DisplayRbacItem
label={this.props.t("Account recovery request review")}
actionName={actions.ACCOUNT_RECOVERY_RESPONSE_CREATE}
level={2}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
</DisplayRbacSection>
</>
)}
<div className="flex-container inner header-flex">
<div className="flex-item first">
<label>
<Trans>UI Permissions</Trans>
</label>
</div>
{this.blankColumnSectionForRoles()}
</div>
<DisplayRbacSection label={this.props.t("Resources")} level={1} rolesCount={rolesCount}>
{(this.canIUseImport || this.canIUseExport) && (
<DisplayRbacSection label={this.props.t("Import/Export")} level={2} rolesCount={rolesCount}>
{this.canIUseImport && (
<DisplayRbacItem
label={this.props.t("Can import")}
actionName={uiActions.RESOURCES_IMPORT}
level={3}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
)}
{this.canIUseExport && (
<DisplayRbacItem
label={this.props.t("Can export")}
actionName={uiActions.RESOURCES_EXPORT}
level={3}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
)}
</DisplayRbacSection>
)}
<DisplayRbacSection label={this.props.t("Password")} level={2} rolesCount={rolesCount}>
{this.canIUsePreviewPassword && (
<DisplayRbacItem
label={this.props.t("Can preview")}
actionName={uiActions.SECRETS_PREVIEW}
level={3}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
)}
<DisplayRbacItem
label={this.props.t("Can copy")}
actionName={uiActions.SECRETS_COPY}
level={3}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
</DisplayRbacSection>
<DisplayRbacSection label={this.props.t("Metadata")} level={2} rolesCount={rolesCount}>
<DisplayRbacItem
label={this.props.t("Can see password activities")}
actionName={uiActions.RESOURCES_SEE_ACTIVITIES}
level={3}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
<DisplayRbacItem
label={this.props.t("Can see password comments")}
actionName={uiActions.RESOURCES_SEE_COMMENTS}
level={3}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
</DisplayRbacSection>
{(this.canIUseFolders || this.canIUseTags) && (
<DisplayRbacSection label={this.props.t("Organization")} level={2} rolesCount={rolesCount}>
{this.canIUseFolders && (
<DisplayRbacItem
label={this.props.t("Can use folders")}
actionName={uiActions.FOLDERS_USE}
level={3}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
)}
{this.canIUseTags && (
<DisplayRbacItem
label={this.props.t("Can use tags")}
actionName={uiActions.TAGS_USE}
level={3}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
)}
</DisplayRbacSection>
)}
<DisplayRbacSection label={this.props.t("Sharing")} level={2} rolesCount={rolesCount}>
<DisplayRbacItem
label={this.props.t("Can see with whom passwords are shared with")}
actionName={uiActions.SHARE_VIEW_LIST}
level={3}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
<DisplayRbacItem
label={this.props.t("Can share folders")}
actionName={uiActions.SHARE_FOLDER}
level={3}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
</DisplayRbacSection>
</DisplayRbacSection>
<DisplayRbacSection label={this.props.t("Users")} level={1} rolesCount={rolesCount}>
<DisplayRbacItem
label={this.props.t("Can see users workspace")}
actionName={uiActions.USERS_VIEW_WORKSPACE}
level={2}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
</DisplayRbacSection>
{(this.canIUseMobile || this.canIUseDesktop) && (
<DisplayRbacSection label={this.props.t("User settings")} level={1} rolesCount={rolesCount}>
{this.canIUseMobile && (
<DisplayRbacItem
label={this.props.t("Can see mobile setup")}
actionName={uiActions.MOBILE_TRANSFER}
level={2}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
)}
{this.canIUseDesktop && (
<DisplayRbacItem
label={this.props.t("Can see desktop application setup")}
actionName={uiActions.DESKTOP_TRANSFER}
level={2}
rbacs={this.props.adminRbacContext.rbacs}
rbacsUpdated={this.props.adminRbacContext.rbacsUpdated}
roles={customizableRoles}
onChange={this.updateRbacControlFunction}
/>
)}
</DisplayRbacSection>
)}
</>
)}
</div>
</form>
</div>
{hasSaveWarning && (
<div className="warning message">
<div>
<p>
<Trans>Don't forget to save your settings to apply your modification.</Trans>
</p>
</div>
</div>
)}
</div>
<DisplayAdministrationRbacActions />
{createSafePortal(
<div className="sidebar-help-section">
<h3>
<Trans>Need help?</Trans>
</h3>
<p>
<Trans>Check out the Role Based Access Control documentation.</Trans>
</p>
<a
className="button"
href="https://passbolt.com/docs/admin/role-based-access-control/"
target="_blank"
rel="noopener noreferrer"
>
<FileTextSVG />
<span>
<Trans>Read RBAC doc</Trans>
</span>
</a>
</div>,
document.getElementById("administration-help-panel"),
)}
</div>
);
}
}
DisplayRbacAdministration.defaultProps = {
RoleApiService: RoleApiService,
RbacApiService: RbacApiService,
UserApiService: UserService,
};
DisplayRbacAdministration.propTypes = {
context: PropTypes.object, // The application context
administrationWorkspaceContext: PropTypes.object, // The administration workspace context
adminRbacContext: PropTypes.object, // The administration rbac context
dialogContext: PropTypes.object, // the dialog context
RoleApiService: PropTypes.func, // The role service to inject
RbacApiService: PropTypes.func, // The rbac service to inject
UserApiService: PropTypes.func, // The user service to inject
t: PropTypes.func, // The translation function
};
export default withAppContext(
withAdminRbac(
withAdministrationWorkspace(withDialog(withActionFeedback(withTranslation("common")(DisplayRbacAdministration)))),
),
);