mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
(bug) OIDC scopes
This commit is contained in:
parent
02df763a66
commit
31b4089bd2
@ -5,6 +5,8 @@
|
||||
- Fix a bug: unable to run scripts on systemts with legacy version of docker-compose
|
||||
- Fix a bug: unable to sign up if admin actived organization's additional fields with required
|
||||
- Fix a bug: undefined error in new member page
|
||||
- Fix a bug: OIDC scopes are separated by spaces, not commas
|
||||
- Fix a bug: unable to create OIDC custom scopes
|
||||
- Fix a security issue: updated rack to 2.2.3.1 to fix [CVE-2022-30123](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-30123) and [CVE-2022-30122](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-30122)
|
||||
|
||||
|
||||
|
@ -74,6 +74,14 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Some OIDC providers expect that the scopes are separated by the space character,
|
||||
* rather than a comma.
|
||||
*/
|
||||
const formatScopes = (values: Array<string>): string => {
|
||||
return values.join(' ');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="openid-connect-form">
|
||||
<hr/>
|
||||
@ -105,15 +113,17 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
{!scopesAvailable && <FormInput id="providable_attributes.scope"
|
||||
register={register}
|
||||
label={t('app.admin.authentication.openid_connect_form.scope')}
|
||||
placeholder="openid,profile,email"
|
||||
placeholder="openid profile email"
|
||||
tooltip={<HtmlTranslate trKey="app.admin.authentication.openid_connect_form.scope_help_html" />} />}
|
||||
{scopesAvailable && <FormMultiSelect id="providable_attributes.scope"
|
||||
expectedResult="string"
|
||||
label={t('app.admin.authentication.openid_connect_form.scope')}
|
||||
tooltip={<HtmlTranslate trKey="app.admin.authentication.openid_connect_form.scope_help_html" />}
|
||||
options={scopesAvailable.map((scope) => ({ value: scope, label: scope }))}
|
||||
formatResult={formatScopes}
|
||||
creatable
|
||||
control={control} />}
|
||||
setValue={setValue}
|
||||
currentValue={currentFormValues.scope?.split(' ')}
|
||||
register={register} />}
|
||||
<FormSelect id="providable_attributes.prompt"
|
||||
label={t('app.admin.authentication.openid_connect_form.prompt')}
|
||||
tooltip={<HtmlTranslate trKey="app.admin.authentication.openid_connect_form.prompt_help_html" />}
|
||||
@ -139,7 +149,7 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
placeholder="https://sso.exemple.com/my-account"
|
||||
label={t('app.admin.authentication.openid_connect_form.profile_edition_url')}
|
||||
tooltip={t('app.admin.authentication.openid_connect_form.profile_edition_url_help')}
|
||||
rules={{ pattern: urlRegex }} />
|
||||
rules={{ required: false, pattern: urlRegex }} />
|
||||
<h4>{t('app.admin.authentication.openid_connect_form.client_options')}</h4>
|
||||
<FormInput id="providable_attributes.client__identifier"
|
||||
label={t('app.admin.authentication.openid_connect_form.client__identifier')}
|
||||
|
@ -1,22 +1,30 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Select from 'react-select';
|
||||
import { Controller, Path } from 'react-hook-form';
|
||||
import { difference } from 'lodash';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { FieldPath } from 'react-hook-form/dist/types/path';
|
||||
import { FieldPathValue, UnpackNestedValue } from 'react-hook-form/dist/types';
|
||||
import { FormControlledComponent } from '../../models/form-component';
|
||||
import { FormComponent } from '../../models/form-component';
|
||||
import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FieldPathValue, Path } from 'react-hook-form';
|
||||
import { UnpackNestedValue, UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
|
||||
interface FormSelectProps<TFieldValues, TContext extends object, TOptionValue> extends FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
|
||||
interface CommonProps<TFieldValues, TOptionValue> extends FormComponent<TFieldValues>, AbstractFormItemProps<TFieldValues> {
|
||||
options: Array<selectOption<TOptionValue>>,
|
||||
valuesDefault?: Array<TOptionValue>,
|
||||
onChange?: (values: Array<TOptionValue>) => void,
|
||||
placeholder?: string,
|
||||
expectedResult?: 'array' | 'string'
|
||||
creatable?: boolean,
|
||||
formatResult?: (values: Array<TOptionValue>) => string,
|
||||
}
|
||||
|
||||
// if creatable is set to true, the setValue must be provided
|
||||
type CreatableProps<TFieldValues, TOptionValue> =
|
||||
{ creatable: true, setValue: UseFormSetValue<TFieldValues>, currentValue?: Array<TOptionValue> } |
|
||||
{ creatable?: false, setValue?: never, currentValue?: never };
|
||||
|
||||
type FormSelectProps<TFieldValues, TOptionValue> = CommonProps<TFieldValues, TOptionValue> & CreatableProps<TFieldValues, TOptionValue>;
|
||||
|
||||
/**
|
||||
* Option format, expected by react-select
|
||||
* @see https://github.com/JedWatson/react-select
|
||||
@ -27,9 +35,13 @@ type selectOption<TOptionValue> = { value: TOptionValue, label: string };
|
||||
* This component is a wrapper around react-select to use with react-hook-form.
|
||||
* It is a multi-select component.
|
||||
*/
|
||||
export const FormMultiSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, tooltip, className, control, placeholder, options, valuesDefault, error, rules, disabled, onChange, formState, warning, expectedResult, creatable }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
|
||||
export const FormMultiSelect = <TFieldValues extends FieldValues, TOptionValue>({ id, label, tooltip, className, register, placeholder, options, valuesDefault, error, rules, disabled, onChange, formState, warning, formatResult, creatable, setValue, currentValue }: FormSelectProps<TFieldValues, TOptionValue>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [isDisabled, setIsDisabled] = React.useState<boolean>(false);
|
||||
const [allOptions, setAllOptions] = React.useState<Array<selectOption<TOptionValue>>>(options);
|
||||
const [createdOptions, setCreatedOptions] = React.useState<Array<selectOption<TOptionValue>>>([]);
|
||||
const [selectedOptions, setSelectedOptions] = React.useState<Array<TOptionValue>>(valuesDefault);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof disabled === 'function') {
|
||||
@ -40,52 +52,77 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
|
||||
}, [disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
setAllOptions(options);
|
||||
}, [options]);
|
||||
setAllOptions(options.concat(createdOptions));
|
||||
}, [options, createdOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(selectedOptions);
|
||||
}
|
||||
setValue(
|
||||
id as Path<TFieldValues>,
|
||||
getResult() as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
}, [selectedOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedOptions === undefined && currentValue && currentValue[0]) {
|
||||
setSelectedOptions(currentValue);
|
||||
const custom = difference(currentValue, allOptions.map(o => o.value));
|
||||
if (custom.length > 0) {
|
||||
setCreatedOptions(custom.map(c => {
|
||||
return { value: c, label: c as unknown as string };
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [currentValue]);
|
||||
|
||||
/**
|
||||
* The following callback will trigger the onChange callback, if it was passed to this component,
|
||||
* when the selected option changes.
|
||||
* It will also update the react-hook-form's value, according to the provided 'result' property (string or array).
|
||||
* The following callback will set the new selected options in the component state.
|
||||
*/
|
||||
const onChangeCb = (newValues: Array<TOptionValue>, rhfOnChange): void => {
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(newValues);
|
||||
}
|
||||
if (expectedResult === 'string') {
|
||||
rhfOnChange(newValues.join(','));
|
||||
} else {
|
||||
rhfOnChange(newValues);
|
||||
}
|
||||
const onChangeCb = (newValues: Array<TOptionValue>): void => {
|
||||
setSelectedOptions(newValues);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will return the currently selected options, according to the provided react-hook-form's value.
|
||||
* This function will return the currently selected options, according to the selectedOptions state.
|
||||
*/
|
||||
const getCurrentValues = (value: Array<TOptionValue>|string): Array<selectOption<TOptionValue>> => {
|
||||
let values: Array<TOptionValue> = [];
|
||||
if (typeof value === 'string') {
|
||||
values = value.split(',') as Array<unknown> as Array<TOptionValue>;
|
||||
const getCurrentValues = (): Array<selectOption<TOptionValue>> => {
|
||||
return allOptions.filter(c => selectedOptions?.includes(c.value));
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the expected result (a string or an array).
|
||||
* This is used in the hidden input.
|
||||
*/
|
||||
const getResult = (): string => {
|
||||
if (!selectedOptions) return undefined;
|
||||
|
||||
if (typeof formatResult === 'function') {
|
||||
return formatResult(selectedOptions);
|
||||
} else {
|
||||
values = value;
|
||||
return selectedOptions.join(',');
|
||||
}
|
||||
return allOptions.filter(c => values?.includes(c.value));
|
||||
};
|
||||
|
||||
/**
|
||||
* When the select is 'creatable', this callback handle the creation and the selection of a new option.
|
||||
*/
|
||||
const handleCreate = (value: Array<TOptionValue>|string, rhfOnChange) => {
|
||||
return (inputValue: string) => {
|
||||
// add the new value to the list of options
|
||||
const newOption = { value: inputValue as unknown as TOptionValue, label: inputValue };
|
||||
setAllOptions([...allOptions, newOption]);
|
||||
const handleCreate = (inputValue: string) => {
|
||||
// add the new value to the list of options
|
||||
const newValue = inputValue as unknown as TOptionValue;
|
||||
const newOption = { value: newValue, label: inputValue };
|
||||
setCreatedOptions([...createdOptions, newOption]);
|
||||
|
||||
// select the new option
|
||||
const values = getCurrentValues(value);
|
||||
values.push(newOption);
|
||||
onChangeCb(values.map(c => c.value), rhfOnChange);
|
||||
};
|
||||
// select the new option
|
||||
setSelectedOptions([...selectedOptions, newValue]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Translate the label for a new item when the select is "creatable"
|
||||
*/
|
||||
const formatCreateLabel = (inputValue: string): string => {
|
||||
return t('app.shared.form_multi_select.create_label', { VALUE: inputValue });
|
||||
};
|
||||
|
||||
// if the user can create new options, we need to use a different component
|
||||
@ -96,31 +133,30 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
|
||||
className={`form-multi-select ${className || ''}`} tooltip={tooltip}
|
||||
disabled={disabled}
|
||||
rules={rules} error={error} warning={warning}>
|
||||
<Controller name={id as FieldPath<TFieldValues>}
|
||||
control={control}
|
||||
defaultValue={valuesDefault as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>}
|
||||
rules={rules}
|
||||
render={({ field: { onChange, value, ref } }) =>
|
||||
<AbstractSelect ref={ref}
|
||||
classNamePrefix="rs"
|
||||
className="rs"
|
||||
value={getCurrentValues(value)}
|
||||
onChange={val => {
|
||||
const values = val?.map(c => c.value);
|
||||
onChangeCb(values, onChange);
|
||||
}}
|
||||
onCreateOption={handleCreate(value, onChange)}
|
||||
placeholder={placeholder}
|
||||
options={allOptions}
|
||||
isDisabled={isDisabled}
|
||||
isMulti />
|
||||
} />
|
||||
<AbstractSelect classNamePrefix="rs"
|
||||
className="rs"
|
||||
value={getCurrentValues()}
|
||||
onChange={val => {
|
||||
const values = val?.map(c => c.value);
|
||||
onChangeCb(values);
|
||||
}}
|
||||
onCreateOption={handleCreate}
|
||||
formatCreateLabel={formatCreateLabel}
|
||||
placeholder={placeholder}
|
||||
options={allOptions}
|
||||
isDisabled={isDisabled}
|
||||
isMulti />
|
||||
<input id={id}
|
||||
type="hidden"
|
||||
{...register(id as FieldPath<TFieldValues>, {
|
||||
...rules,
|
||||
value: getResult() as FieldPathValue<TFieldValues, FieldPath<TFieldValues>>
|
||||
})} />
|
||||
</AbstractFormItem>
|
||||
);
|
||||
};
|
||||
|
||||
FormMultiSelect.defaultProps = {
|
||||
expectedResult: 'array',
|
||||
creatable: false,
|
||||
disabled: false
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ class AuthProviderService
|
||||
provider.providable.response_type = 'code'
|
||||
provider.providable.uid_field = provider.auth_provider_mappings
|
||||
.find { |m| m.local_model == 'user' && m.local_field == 'uid' }
|
||||
.api_field
|
||||
&.api_field
|
||||
|
||||
URI.parse(provider.providable.issuer).tap do |uri|
|
||||
provider.providable.client__scheme = uri.scheme
|
||||
|
@ -527,3 +527,5 @@ en:
|
||||
payzen_card_update_modal:
|
||||
update_card: "Update the card"
|
||||
validate_button: "Validate the new card"
|
||||
form_multi_select:
|
||||
create_label: "Add {VALUE}"
|
Loading…
x
Reference in New Issue
Block a user