1
0
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:
Sylvain 2022-05-30 12:56:22 +02:00
parent 02df763a66
commit 31b4089bd2
5 changed files with 113 additions and 63 deletions

View File

@ -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)

View File

@ -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')}

View File

@ -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
};

View File

@ -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

View File

@ -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}"