Add security logic to readonly and object support

This commit is contained in:
Cooldude2606 committed 2024-09-21 17:41:46 +01:00
1 parent e3eb266704
commit 49d584394d
5 files changed
+77 -63

No files matched your search

+13 -8
View File
@@ -118,15 +118,17 @@ export interface FieldDefinition {
*/
access?: ConfigLocation[];
/**
* If present treat the config value as a credential and only allow reading
* from the locations specified.
* If present treat the config value as a credential.
* Only the locations specified will be able to read the current value.
* Access can be used to specify which locations are able to overwrite the value.
*/
credential?: ConfigLocation[];
/**
* True if graphical interfaces should prevent the user modifying the value
* This is client side and does not provide any validation logic
* If present treat the config value as readonly.
* Only the locations specified will be able to update the config value.
* Access can be used to specify which locations are able to read the value.
*/
readonly?: boolean;
readonly?: ConfigLocation[];
/**
* True if graphical interfaces should hide the config value
*/
@@ -240,11 +242,14 @@ export class Config<
*/
_checkAccess(name: string, def: FieldDefinition, location: ConfigLocation, mode: ConfigAccess, error: boolean) {
// If credential array is present it acts as read permission and the access array as write permission,
// if not present access array acts as a combined read/write permission check.
// If readonly array is present it acts as write permission and the access array as the read permission.
// If neither is present access array acts as a combined read/write permission check.
if (
def.credential && mode & ConfigAccess.read && !(def.credential).includes(location)
|| def.readonly && mode & ConfigAccess.write && !(def.readonly).includes(location)
|| (
(!def.credential || mode & ConfigAccess.write)
&& (!def.readonly || mode & ConfigAccess.read)
&& !(def.access ?? this.defaultAccess).includes(location)
)
) {
@@ -304,7 +309,7 @@ export class Config<
for (let [name, value] of Object.entries(this.fields)) {
let def = this.constructor.fieldDefinitions[name];
if (
!this._checkAccess(name, def, this.location, ConfigAccess.write, false)
this.location !== "control" && !this._checkAccess(name, def, this.location, ConfigAccess.write, false)
|| this.location !== remote && !this._checkAccess(name, def, remote, ConfigAccess.read, false)
) {
continue;
@@ -353,7 +358,7 @@ export class Config<
}
if (
!this._checkAccess(name, def, remote, ConfigAccess.write, false)
this.location !== "control" && !this._checkAccess(name, def, remote, ConfigAccess.write, false)
) {
continue;
}
+1
View File
@@ -24,6 +24,7 @@ export interface PluginLoginForm {
}
export interface InputComponentProps {
disabled?: boolean,
fieldDefinition: FieldDefinition,
value: null | boolean | number | string,
onChange: (value: null | boolean | number | string) => void,
@@ -32,24 +32,24 @@ function getInitialValues(config: lib.Config<any>, props: BaseConfigTreeProps) {
return initialValues;
}
function renderInput(inputComponents: Record<string, InputComponent>, def: lib.FieldDefinition) {
function renderInput(inputComponents: Record<string, InputComponent>, def: lib.FieldDefinition, readonly: boolean) {
if (def.inputComponent && Object.prototype.hasOwnProperty.call(inputComponents, def.inputComponent)) {
// Field.Item will provide the value and onChange props to the component.
type InputPartial = React.ComponentClass<Omit<InputComponentProps, "value" | "onChange">>;
const CustomInput = inputComponents[def.inputComponent] as unknown as InputPartial;
return <CustomInput fieldDefinition={def} />;
return <CustomInput fieldDefinition={def} disabled={readonly} />;
}
if (def.type === "boolean") {
return <Checkbox disabled={def.readonly} />;
return <Checkbox disabled={readonly} />;
}
if (def.type === "string") {
if (def.credential) {
return <Input.Password autoComplete={def.autoComplete} disabled={def.readonly} />;
return <Input.Password autoComplete={def.autoComplete} disabled={readonly} />;
}
return <Input autoComplete={def.autoComplete} disabled={def.readonly} />;
return <Input autoComplete={def.autoComplete} disabled={readonly} />;
}
if (def.type === "number") {
return <InputNumber autoComplete={def.autoComplete} disabled={def.readonly} />;
return <InputNumber autoComplete={def.autoComplete} disabled={readonly} />;
}
return `Unknown type ${def.type}`;
@@ -264,16 +264,17 @@ function computeTreeData(
};
for (let [field, def] of Object.entries(groupDefs)) {
const canWrite = config.canAccess(`${groupName}.${field}`, lib.ConfigAccess.write);
const canRead = config.canAccess(`${groupName}.${field}`, lib.ConfigAccess.read);
if (!canWrite && !canRead) {
continue;
}
if (def.hidden === true) {
continue;
}
let fieldName = `${groupName}.${field}`;
const fieldName = `${groupName}.${field}`;
const canWrite = config.canAccess(fieldName, lib.ConfigAccess.write);
const canRead = config.canAccess(fieldName, lib.ConfigAccess.read);
if (!canWrite && !canRead) {
continue;
}
let value = config.get(fieldName) as any;
let childNode: ChildNode = {
key: fieldName,
@@ -314,7 +315,7 @@ function computeTreeData(
return Promise.resolve();
}}]}
>
<Input className="json-input" />
<Input className="json-input" disabled={!canWrite} />
</Form.Item>,
});
@@ -322,47 +323,53 @@ function computeTreeData(
propsMap.set(propPath, [fieldName, prop]);
}
let newPropPath = `${groupName}.${field}:add`;
childNode.children.push({
key: newPropPath,
title: <Space>
<Form.Item
name={`${newPropPath}.name`}
noStyle={true}
>
<Input />
</Form.Item>:
<Form.Item
name={`${newPropPath}.value`}
noStyle={true}
>
<Input className="json-input" />
</Form.Item>
<Button
size="small"
onClick={() => {
let propName = form.getFieldValue(`${newPropPath}.name`);
let propValue = form.getFieldValue(`${newPropPath}.value`);
if (!value || !Object.prototype.hasOwnProperty.call(value, propName)) {
let newConfig = ConfigClass.fromJSON(config!.toJSON(), "control");
(newConfig as lib.Config<any>).setProp(fieldName, propName, null);
setConfig(newConfig);
}
if (canWrite) {
let newPropPath = `${groupName}.${field}:add`;
childNode.children.push({
key: newPropPath,
title: <Space>
<Form.Item
name={`${newPropPath}.name`}
noStyle={true}
>
<Input />
</Form.Item>:
<Form.Item
name={`${newPropPath}.value`}
noStyle={true}
>
<Input className="json-input" />
</Form.Item>
<Button
size="small"
onClick={() => {
let propName = form.getFieldValue(`${newPropPath}.name`);
let propValue = form.getFieldValue(`${newPropPath}.value`);
if (!value || !Object.prototype.hasOwnProperty.call(value, propName)) {
let newConfig = ConfigClass.fromJSON(config!.toJSON(), "control");
(newConfig as lib.Config<any>).setProp(fieldName, propName, null);
setConfig(newConfig);
}
let propPath = `${groupName}.${field}.${propName}`;
form.setFieldsValue({
[`${newPropPath}.name`]: "",
[`${newPropPath}.value`]: "",
[propPath]: propValue,
});
form.validateFields([propPath]);
onValuesChange({ [propPath]: propValue });
}}
>
Add
</Button>
</Space>,
});
let propPath = `${groupName}.${field}.${propName}`;
form.setFieldsValue({
[`${newPropPath}.name`]: "",
[`${newPropPath}.value`]: "",
[propPath]: propValue,
});
form.validateFields([propPath]);
onValuesChange({ [propPath]: propValue });
}}
>
Add
</Button>
</Space>,
});
}
if (!childNode.children.length) {
continue;
}
} else {
childNode.title = <Form.Item
@@ -376,7 +383,7 @@ function computeTreeData(
tooltip={def.description}
valuePropName={def.type === "boolean" ? "checked" : "value"}
>
{renderInput(control.inputComponents, def)}
{renderInput(control.inputComponents, def, !canWrite)}
</Form.Item>;
initialValues[fieldName] = value;
}
@@ -15,6 +15,6 @@ export default function InputModPack(props: InputComponentProps) {
value: modPack.id,
}))}
allowClear={props.fieldDefinition.optional}
disabled={props.disabled}
/>;
}
@@ -26,5 +26,6 @@ export default function InputRole(props: InputComponentProps) {
value: role.id,
}))}
allowClear={props.fieldDefinition.optional}
disabled={props.disabled}
/>;
}