UNPKG

passbolt-styleguide

Version:

Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.

493 lines (471 loc) 17.1 kB
/** * 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.5.0 */ import React from "react"; import { Trans, withTranslation } from "react-i18next"; import PropTypes from "prop-types"; import { withActionFeedback } from "../../../../contexts/ActionFeedbackContext"; import { withAdminSso } from "../../../../contexts/AdminSsoContext"; import { withAppContext } from "../../../../../shared/context/AppContext/AppContext"; import Select from "../../../Common/Select/Select"; import Password from "../../../../../shared/components/Password/Password"; import AzureSsoSettingsEntity from "../../../../../shared/models/entity/ssoSettings/AzureSsoSettingsEntity"; import CopySVG from "../../../../../img/svg/copy.svg"; import CalendarSVG from "../../../../../img/svg/calendar.svg"; import CaretDownSVG from "../../../../../img/svg/caret_down.svg"; import CaretRightSVG from "../../../../../img/svg/caret_right.svg"; import { withClipboard } from "../../../../contexts/Clipboard/ManagedClipboardServiceProvider"; /** * This component displays the Azure SSO settings form */ class AzureSsoProviderForm extends React.Component { /** * Constructor * @param {Object} props */ constructor(props) { super(props); this.state = this.defaultState; this.bindCallbacks(); this.createRefs(); } /** * Get default state * @returns {*} */ get defaultState() { return { advancedSettingsOpened: false, }; } /** * Bind callbacks methods */ bindCallbacks() { this.handleInputChange = this.handleInputChange.bind(this); this.handleCopyRedirectUrl = this.handleCopyRedirectUrl.bind(this); this.handleAdvancedSettingsCLick = this.handleAdvancedSettingsCLick.bind(this); } createRefs() { this.clientIdInputRef = React.createRef(); this.tenantIdInputRef = React.createRef(); this.clientSecretInputRef = React.createRef(); this.clientSecretExpiryInputRef = React.createRef(); } componentDidUpdate() { if (!this.props.adminSsoContext.consumeFocusOnError()) { return; } const errors = this.props.adminSsoContext.getErrors(); const fieldToFocus = this.getFirstFieldInError(errors, [ "client_id", "tenant_id", "client_secret", "client_secret_expiry", ]); switch (fieldToFocus) { case "client_id": this.clientIdInputRef.current.focus(); break; case "tenant_id": this.tenantIdInputRef.current.focus(); break; case "client_secret": this.clientSecretInputRef.current.focus(); break; case "client_secret_expiry": this.clientSecretExpiryInputRef.current.focus(); break; } } /** * Returns the first field with an error (first in the given list) * @param {EntityValidationError} errors * @param {Array<string>} fieldPriority the ordered list of field to check * @returns {string|null} */ getFirstFieldInError(errors, fieldPriority) { for (let i = 0; i < fieldPriority.length; i++) { const fieldName = fieldPriority[i]; if (errors.hasError(fieldName)) { return fieldName; } } return null; } /** * Handle form input changes. * @params {ReactEvent} The react event * @returns {void} */ handleInputChange(event) { const target = event.target; const value = target.type === "checkbox" ? target.checked : target.value; const name = target.name; this.props.adminSsoContext.setValue(name, value); } /** * Handle advanced settings panel button click */ handleAdvancedSettingsCLick() { this.setState({ advancedSettingsOpened: !this.state.advancedSettingsOpened, }); } /** * Handle the copy to clipboard button */ async handleCopyRedirectUrl() { await this.props.clipboardContext.copy( this.fullRedirectUrl, this.translate("The redirection URL has been copied to the clipboard."), ); } /** * Should input be disabled? True if state is loading or processing * @returns {boolean} */ hasAllInputDisabled() { return this.props.adminSsoContext.isProcessing(); } /** * Returns an array of string from the errors so React can display them. * @param {Object} errors * @returns {Array<string>} */ displayErrors(errors) { return Object.values(errors); } /** * Get the different URLs Azure supports for the URL select input. * @returns {Array<{value: string, label: string}} */ get availableUrlList() { return AzureSsoSettingsEntity.SUPPORTED_URLS.map((url) => ({ value: url, label: url, })); } /** * Get the different options for email claim select input. * @returns {Array<{value: string, label: string}} */ get emailClaimList() { return [ { value: "email", label: this.translate("Email") }, { value: "preferred_username", label: this.translate("Preferred username") }, { value: "upn", label: this.translate("UPN") }, ]; } /** * Get the different options for prompt select input. * @returns {Array<{value: string, label: string}} */ get promptOptionList() { return [ { value: "login", label: this.translate("Login") }, { value: "none", label: this.translate("None") }, ]; } /** * Get the full redirection URL; */ get fullRedirectUrl() { const trustedDomain = this.props.context.userSettings.getTrustedDomain(); return `${trustedDomain}/sso/azure/redirect`; } /** * Get the translate function * @returns {function(...[*]=)} */ get translate() { return this.props.t; } /** * Render the component * @returns {JSX} */ render() { const ssoContext = this.props.adminSsoContext; const ssoConfig = ssoContext.getSsoConfiguration(); const errors = ssoContext.getErrors(); return ( <> <div className={`select-wrapper input required ${this.hasAllInputDisabled() ? "disabled" : ""}`}> <label htmlFor="sso-azure-url-input"> <Trans>Login URL</Trans> </label> <Select id="sso-azure-url-input" name="url" items={this.availableUrlList} value={ssoConfig.url} onChange={this.handleInputChange} /> <p> <Trans> The Azure AD authentication endpoint. See{" "} <a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud#azure-ad-authentication-endpoints" rel="noopener noreferrer" target="_blank" > alternatives </a> . </Trans> </p> </div> <div className={`input text input-wrapper ${this.hasAllInputDisabled() ? "disabled" : ""}`}> <label> <Trans>Redirect URL</Trans> </label> <div className="button-inline"> <input id="sso-redirect-url-input" type="text" className="fluid form-element disabled" name="redirect_url" value={this.fullRedirectUrl} placeholder={this.translate("Redirect URL")} readOnly disabled={true} /> <button type="button" onClick={this.handleCopyRedirectUrl} className="copy-to-clipboard button button-icon"> <CopySVG /> </button> </div> <p> <Trans>The URL to provide to Azure when registering the application.</Trans> </p> </div> <div className={`input text required ${this.hasAllInputDisabled() ? "disabled" : ""}`}> <label> <Trans>Application (client) ID</Trans> </label> <input id="sso-azure-client-id-input" type="text" className="fluid form-element" name="client_id" ref={this.clientIdInputRef} value={ssoConfig.client_id} onChange={this.handleInputChange} placeholder={this.translate("Application (client) ID")} disabled={this.hasAllInputDisabled()} /> {errors?.hasError("client_id") && ( <div className="error-message">{this.displayErrors(errors.getError("client_id"))}</div> )} <p> <Trans>The public identifier for the app in Azure in UUID format.</Trans>{" "} <a href="https://learn.microsoft.com/en-us/azure/healthcare-apis/register-application#application-id-client-id" rel="noopener noreferrer" target="_blank" > <Trans>Where to find it?</Trans> </a> </p> </div> <div className={`input text required ${this.hasAllInputDisabled() ? "disabled" : ""}`}> <label> <Trans>Directory (tenant) ID</Trans> </label> <input id="sso-azure-tenant-id-input" type="text" className="fluid form-element" name="tenant_id" ref={this.tenantIdInputRef} value={ssoConfig.tenant_id} onChange={this.handleInputChange} placeholder={this.translate("Directory ID")} disabled={this.hasAllInputDisabled()} /> {errors?.hasError("tenant_id") && ( <div className="error-message">{this.displayErrors(errors.getError("tenant_id"))}</div> )} <p> <Trans>The Azure Active Directory tenant ID, in UUID format.</Trans>{" "} <a href="https://learn.microsoft.com/en-gb/azure/active-directory/fundamentals/active-directory-how-to-find-tenant" rel="noopener noreferrer" target="_blank" > <Trans>Where to find it?</Trans> </a> </p> </div> <div className={`input text required ${this.hasAllInputDisabled() ? "disabled" : ""}`}> <label> <Trans>Secret</Trans> </label> <Password id="sso-azure-secret-input" className="fluid form-element" onChange={this.handleInputChange} autoComplete="off" name="client_secret" placeholder={this.translate("Secret")} disabled={this.hasAllInputDisabled()} value={ssoConfig.client_secret} preview={true} inputRef={this.clientSecretInputRef} /> {errors?.hasError("client_secret") && ( <div className="error-message">{this.displayErrors(errors.getError("client_secret"))}</div> )} <p> <Trans>Allows Azure and Passbolt API to securely share information.</Trans>{" "} <a href="https://learn.microsoft.com/en-us/azure/marketplace/create-or-update-client-ids-and-secrets#add-a-client-id-and-client-secret" rel="noopener noreferrer" target="_blank" > <Trans>Where to find it?</Trans> </a> </p> </div> <div className={`input text date-wrapper required ${this.hasAllInputDisabled() ? "disabled" : ""}`}> <label> <Trans>Secret expiry</Trans> </label> <div className="button-inline"> <input id="sso-azure-secret-expiry-input" type="date" className={`fluid form-element ${ssoConfig.client_secret_expiry ? "" : "empty"}`} name="client_secret_expiry" ref={this.clientSecretExpiryInputRef} value={ssoConfig.client_secret_expiry || ""} onChange={this.handleInputChange} disabled={this.hasAllInputDisabled()} /> <CalendarSVG className="svg-icon" /> </div> {errors?.hasError("client_secret_expiry") && ( <div className="error-message">{this.displayErrors(errors.getError("client_secret_expiry"))}</div> )} </div> <div className="warning message"> <div> <Trans> <b>Warning</b>: This secret will expire after some time (typically a few months). Make sure you save the expiry date and rotate it on time. </Trans> </div> </div> <div> <div className={`accordion operation-details ${this.state.advancedSettingsOpened ? "" : "closed"}`}> <div className="accordion-header" onClick={this.handleAdvancedSettingsCLick}> <button type="button" className="link no-border" id="advanced-settings-panel-button"> {this.state.advancedSettingsOpened ? ( <CaretDownSVG className="caret-down" /> ) : ( <CaretRightSVG className="caret-right" /> )} <Trans>Advanced settings</Trans> </button> </div> </div> </div> {this.state.advancedSettingsOpened && ( <> <div className={`select-wrapper input required ${this.hasAllInputDisabled() ? "disabled" : ""}`}> <label htmlFor="email-claim-input"> <Trans>Email claim</Trans> </label> <Select id="email-claim-input" name="email_claim" items={this.emailClaimList} value={ssoConfig.email_claim} onChange={this.handleInputChange} /> <p> <Trans>Defines which Azure field needs to be used as Passbolt username.</Trans> </p> </div> {ssoConfig.email_claim === "upn" && ( <div className="warning message"> <div> <Trans> <b>Warning</b>: UPN is not active by default on Azure and requires a specific option set on Azure to be working. </Trans> </div> </div> )} {ssoConfig.email_claim === "email" && ( <div className="warning message"> <div> <Trans> <b>Warning</b>: using Azure email field to map with Passbolt username is generally unsafe (see. noauth vulnerability class). </Trans> </div> </div> )} <div className={`select-wrapper input required ${this.hasAllInputDisabled() ? "disabled" : ""}`}> <label htmlFor="prompt-input"> <Trans>Prompt</Trans> </label> <Select id="prompt-input" name="prompt" items={this.promptOptionList} value={ssoConfig.prompt} onChange={this.handleInputChange} /> <p> <Trans>Defines the Azure login behaviour by prompting the user to fully login each time or not.</Trans> </p> </div> <div className="input-wrapper form-element"> <div className="toggle-swith-title"> <Trans>Login hint</Trans> </div> <div className="input toggle-switch"> <input type="checkbox" className="toggle-switch-checkbox checkbox" name="login_hint" onChange={this.handleInputChange} checked={ssoConfig.login_hint} disabled={this.hasAllInputDisabled()} id="login_hint-input" /> <label htmlFor="login_hint-input"> <Trans> If checked, users signing in with Microsoft Azure must use their Passbolt email address. </Trans> </label> </div> </div> </> )} </> ); } } AzureSsoProviderForm.propTypes = { adminSsoContext: PropTypes.object, // The administration sso configuration context actionFeedbackContext: PropTypes.any, // The action feedback context context: PropTypes.any, // The application context clipboardContext: PropTypes.object, // the clipboard context t: PropTypes.func, // The translation function }; export default withAppContext( withActionFeedback(withAdminSso(withClipboard(withTranslation("common")(AzureSsoProviderForm)))), );