Skip to content

Commit

Permalink
feat: add security properties for iFrame
Browse files Browse the repository at this point in the history
Closes #901
  • Loading branch information
Niklas Kiefer authored and Skaiir committed Jan 31, 2024
1 parent efef010 commit dfccc01
Show file tree
Hide file tree
Showing 15 changed files with 502 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
ConstraintsGroup,
ValidationGroup,
OptionsGroups,
TableHeaderGroups,
LayoutGroup,
TableHeaderGroups
SecurityAttributesGroup
} from './groups';

import { hasEntryConfigured } from './Util';
Expand Down Expand Up @@ -63,6 +64,7 @@ export class PropertiesProvider {
...groups,
GeneralGroup(field, editField, getService),
...TableHeaderGroups(field, editField),
SecurityAttributesGroup(field, editField),
ConditionGroup(field, editField),
LayoutGroup(field, editField),
AppearanceGroup(field, editField),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { get, set } from 'min-dash';

import { simpleBoolEntryFactory } from '../entries/factories';

import { SECURITY_ATTRIBUTES_DEFINITIONS } from '@bpmn-io/form-js-viewer';


export function SecurityAttributesGroup(field, editField) {

const entries = createEntries({ field, editField });

if (!entries.length) {
return null;
}

return {
id: 'securityAttributes',
label: 'Security attributes',
entries,
tooltip: getTooltip()
};
}

function createEntries(props) {
const {
editField,
field
} = props;

return SECURITY_ATTRIBUTES_DEFINITIONS.map((definition) => {
const {
label,
property
} = definition;

return simpleBoolEntryFactory({
id: property,
label: label,
isDefaultVisible: (field) => field.type === 'iframe',
path: [ 'security', property ],
props,
getValue: () => get(field, [ 'security', property ]),
setValue: (value) => {
const security = get(field, [ 'security' ], {});
editField(field, [ 'security' ], set(security, [ property ], value));
}
});
});
}

// helpers //////////

function getTooltip() {
return <>
<p>Allow the iframe to access more functionality of your browser, details regarding the various options can be found in the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe">MDN iFrame documentation.</a></p>
<p>Be cautious when embedding content from external sources. Ensure you trust the source and are aware of the potential security risks.</p>
</>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export { OptionsGroups } from './OptionsGroups';
export { CustomPropertiesGroup } from './CustomPropertiesGroup';
export { AppearanceGroup } from './AppearanceGroup';
export { LayoutGroup } from './LayoutGroup';
export { SecurityAttributesGroup } from './SecurityAttributesGroup';
export { ConditionGroup } from './ConditionGroup';
export { TableHeaderGroups } from './TableHeaderGroups';
Original file line number Diff line number Diff line change
Expand Up @@ -3617,6 +3617,7 @@ describe('properties panel', function() {
// then
expectGroups(container, [
'General',
'Security attributes',
'Layout',
'Custom properties'
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
cleanup,
fireEvent,
render
} from '@testing-library/preact/pure';

import { SECURITY_ATTRIBUTES_DEFINITIONS } from '@bpmn-io/form-js-viewer';

import { SecurityAttributesGroup } from '../../../../../src/features/properties-panel/groups';

import { TestPropertiesPanel, MockPropertiesPanelContext } from '../helper';


describe('SecurityAttributesGroup', function() {

afterEach(() => cleanup());


it('should NOT render for checkbox', function() {

// given
const field = { type: 'checkbox' };

renderSecurityAttributesGroup({ field });

// then
expect(findGroup('appearance', document.body)).to.not.exist;
});

SECURITY_ATTRIBUTES_DEFINITIONS.forEach(({ property }) => {


describe(property, function() {

it('should render for iframe', function() {

// given
const field = { type: 'iframe' };

// when
const { container } = renderSecurityAttributesGroup({ field });

// then
const input = findInput(property, container);

expect(input).to.exist;
});


it('should read', function() {

// given
const field = {
type: 'iframe',
security: {
[property]: true
}
};

// when
const { container } = renderSecurityAttributesGroup({ field });

const input = findInput(property, container);

// then
expect(input).to.exist;
expect(input.checked).to.equal(true);
});


it('should write', async function() {

// given
const field = {
type: 'iframe',
security: {
[property]: true
}
};

const editFieldSpy = sinon.spy();

const { container } = renderSecurityAttributesGroup({ field, editField: editFieldSpy });

const input = findInput(property, container);

// when
fireEvent.click(input);

// then
expect(editFieldSpy).to.have.been.calledOnce;
expect(field.security[property]).to.equal(false);
});

});

});

});


// helper ///////////////

function renderSecurityAttributesGroup(options) {
const {
editField,
field,
services
} = options;

const groups = [ SecurityAttributesGroup(field, editField) ];

return render(
<MockPropertiesPanelContext services={ services }>
<TestPropertiesPanel
field={ field }
groups={ groups } />
</MockPropertiesPanelContext>
);
}

function findInput(id, container) {
return container.querySelector(`input[name="${id}"]`);
}

function findGroup(id, container) {
return container.querySelector(`.bio-properties-panel-group [data-group-id="group-${id}"]`);
}
40 changes: 25 additions & 15 deletions packages/form-js-viewer/src/render/components/form-fields/IFrame.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { useContext, useMemo } from 'preact/hooks';
import { useEffect, useMemo, useState } from 'preact/hooks';

import { FormContext } from '../../context';

import { useSingleLineTemplateEvaluation } from '../../hooks';
import { useSingleLineTemplateEvaluation, useSecurityAttributesMap } from '../../hooks';
import { sanitizeIFrameSource } from '../Sanitizer';

import { Label } from '../Label';

import {
formFieldClasses,
prefixId
} from '../Util';
import { formFieldClasses } from '../Util';


const type = 'iframe';

Expand All @@ -21,14 +17,15 @@ export function IFrame(props) {
const {
field,
disabled,
readonly
readonly,
domId
} = props;

const {
height = DEFAULT_HEIGHT,
id,
label,
url
url,
security = {}
} = field;

const evaluatedUrl = useSingleLineTemplateEvaluation(url, { debug: true });
Expand All @@ -37,10 +34,16 @@ export function IFrame(props) {

const evaluatedLabel = useSingleLineTemplateEvaluation(label, { debug: true });

const { formId } = useContext(FormContext);
const [ sandbox, allow ] = useSecurityAttributesMap(security);
const [ iframeRefresh, setIframeRefresh ] = useState(0);

// forces re-render of iframe when sandbox or allow attributes change, as browsers do not do it automatically
useEffect(() => {
setIframeRefresh(count => count + 1);
}, [ sandbox, allow ]);

return <div class={ formFieldClasses(type, { disabled, readonly }) }>
<Label id={ prefixId(id, formId) } label={ evaluatedLabel } />
<Label id={ domId } label={ evaluatedLabel } />
{
!evaluatedUrl && <IFramePlaceholder text="No content to show." />
}
Expand All @@ -51,8 +54,12 @@ export function IFrame(props) {
title={ evaluatedLabel }
height={ height }
class="fjs-iframe"
id={ prefixId(id, formId) }
sandbox="allow-scripts"
id={ domId }
sandbox={ sandbox }
key={ 'iframe-' + iframeRefresh }

/* @Note: JSX HTML attributes do not include <allow> */
{ ...{ allow: allow } }
/>
}
{
Expand All @@ -75,6 +82,9 @@ IFrame.config = {
label: 'iFrame',
group: 'container',
create: (options = {}) => ({
security: {
allowScripts: true
},
...options
})
};
1 change: 1 addition & 0 deletions packages/form-js-viewer/src/render/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { useCondition } from './useCondition';
export { useOptionsAsync, LOAD_STATES } from './useOptionsAsync';
export { useSecurityAttributesMap } from './useSecurityAttributesMap';
export { useGetLabelCorrelation } from './useGetLabelCorrelation';
export { useScrollIntoView } from './useScrollIntoView';
export { useExpressionEvaluation } from './useExpressionEvaluation';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { get } from 'min-dash';
import { SECURITY_ATTRIBUTES_DEFINITIONS, SANDBOX_ATTRIBUTE } from '../../util/constants';
import { useMemo } from 'preact/hooks';
import { useDeepCompareState } from './useDeepCompareState';

/**
* A custom hook to build up security attributes from form configuration.
*
* @param {Object} security - The security configuration.
* @returns {Array} - Returns a tuple with sandbox and allow attributes.
*/
export function useSecurityAttributesMap(security) {

const securityMemoized = useDeepCompareState(security);

const sandbox = useMemo(() =>
SECURITY_ATTRIBUTES_DEFINITIONS
.filter(({ attribute }) => attribute === SANDBOX_ATTRIBUTE)
.filter(({ property }) => get(securityMemoized, [ property ], false))
.map(({ directive }) => directive)
.join(' ')
, [ securityMemoized ]);

const allow = useMemo(() =>
SECURITY_ATTRIBUTES_DEFINITIONS
.filter(({ attribute }) => attribute !== SANDBOX_ATTRIBUTE)
.filter(({ property }) => get(securityMemoized, [ property ], false))
.map(({ directive }) => directive)
.join('; ')
, [ securityMemoized ]);

return [ sandbox, allow ];
}
Loading

0 comments on commit dfccc01

Please sign in to comment.