Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(history): handle sanitization when fetching versions #20212

Merged
merged 19 commits into from Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -64,10 +64,8 @@ const CustomRelationInput = (props: RelationsFieldProps) => {
>(props.name);

/**
* TODO:
*
* Ideally the server would return the correct shape, however, for admin user relations
* it sanitizes everyything out when it finds an object for the relation value.
* it sanitizes everything out when it finds an object for the relation value.
*/
const formattedFieldValue = Array.isArray(field.value)
? {
Expand Down
Expand Up @@ -5,7 +5,6 @@ export const FIELDS_TO_IGNORE = [
'publishedAt',
'createdBy',
'updatedBy',
'locale',
'strapi_stage',
'strapi_assignee',
];
Expand Up @@ -77,10 +77,7 @@ const createHistoryVersionController = ({ strapi }: { strapi: Core.Strapi }) =>
async (version: HistoryVersions.HistoryVersionDataResponse & { locale: string }) => {
return {
...version,
data: await permissionChecker.sanitizeOutput({
...version.data,
locale: version.locale.code,
}),
data: await permissionChecker.sanitizeOutput(version.data),
createdBy: version.createdBy
? pick(['id', 'firstname', 'lastname', 'username', 'email'], version.createdBy)
: undefined,
Expand Down
Expand Up @@ -307,7 +307,6 @@ const createHistoryService = ({ strapi }: { strapi: Core.Strapi }) => {
userAbility: params.state.userAbility,
model: attributeSchema.target,
});

const sanitizedEntry = await permissionChecker.sanitizeOutput(relatedEntry);

if (sanitizedEntry) {
Expand Down Expand Up @@ -352,12 +351,19 @@ const createHistoryService = ({ strapi }: { strapi: Core.Strapi }) => {
return currentRelationData;
}

const permissionChecker = getContentManagerService('permission-checker').create({
userAbility: params.state.userAbility,
model: 'plugin::upload.file',
});

const relatedEntry = await strapi.db
.query('plugin::upload.file')
.findOne({ where: { id: entry.id } });

if (relatedEntry) {
currentRelationData.results.push(relatedEntry);
const sanitizedEntry = await permissionChecker.sanitizeOutput(relatedEntry);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why you picked this in the end and not mainfield only population

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here for upload I don't think a mainField is available 🤔 but maybe it is. For the other instance with relations I did test it out but I think I abandoned it quite quickly when values weren't showing up as expected. I'll try it again real quick.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as you want I'm just curious, don't want to block you. And I doubt sanitization is needed on media assets

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I might just merge this now then and circle back to the idea in another PR with some of the refactor ideas.


if (sanitizedEntry) {
currentRelationData.results.push(sanitizedEntry);
} else {
// The related content has been deleted
currentRelationData.meta.missingCount += 1;
Expand Down Expand Up @@ -398,41 +404,29 @@ const createHistoryService = ({ strapi }: { strapi: Core.Strapi }) => {
) {
/**
* Don't build the relations response object for relations to admin users,
* because pickAllowedAdminUserFields sanitizati
* because pickAllowedAdminUserFields will sanitize the data in the controller.
*/
if (attributeSchema.target === 'admin::user') {
const sanitizedAdminUser = await Promise.all(
const adminUsers = await Promise.all(
attributeValues.map(async (userToPopulate) => {
if (userToPopulate == null) {
return null;
}

const user = await strapi
return strapi
.query('admin::user')
.findOne({ where: { id: userToPopulate.id } });

const permissionChecker = getContentManagerService('permission-checker').create(
{
userAbility: params.state.userAbility,
model: 'admin::user',
}
);
const sanitizedUser = await permissionChecker.sanitizeOutput(user);

return sanitizedUser;
})
);

return {
...(await currentDataWithRelations),
/**
* TODO:
*
* Ideally we would return the same "{results: [], meta: {}}" shape, however,
* when sanitizing the data as a whole in the controller before sending to the client,
* the data for admin relation user is completely sanitized if we return an object here as opposed to an array.
*/
[attributeKey]: sanitizedAdminUser,
[attributeKey]: adminUsers,
};
}

Expand Down
57 changes: 28 additions & 29 deletions tests/e2e/tests/content-manager/history.spec.ts
@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { test, expect, Page } from '@playwright/test';
import { login } from '../../utils/login';
import { resetDatabaseAndImportDataFromPath } from '../../utils/dts-import';
import { describeOnCondition } from '../../utils/shared';
Expand All @@ -7,6 +7,14 @@ import { waitForRestart } from '../../utils/restart';

const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';

const goToHistoryPage = async (page: Page) => {
const moreActionsButton = await page.getByRole('button', { name: /more actions/i });
await expect(moreActionsButton).toBeEnabled();
await moreActionsButton.click();
const historyButton = await page.getByRole('menuitem', { name: /content history/i });
await historyButton.click();
};

describeOnCondition(edition === 'EE')('History', () => {
test.describe('Collection Type', () => {
test.beforeEach(async ({ page }) => {
Expand Down Expand Up @@ -43,8 +51,8 @@ describeOnCondition(edition === 'EE')('History', () => {
await titleInput.fill(frenchTitle);
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: /content history/i }).click();
await goToHistoryPage(page);

await page.waitForURL(HISTORY_URL);
await expect(titleInput).toHaveValue(frenchTitle);

Expand All @@ -59,8 +67,7 @@ describeOnCondition(edition === 'EE')('History', () => {
await titleInput.fill(englishTitle);
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: /content history/i }).click();
await goToHistoryPage(page);
await page.waitForURL(HISTORY_URL);
const versionCards = await page.getByRole('listitem', { name: 'Version card' });
await expect(versionCards).toHaveCount(1);
Expand All @@ -85,8 +92,7 @@ describeOnCondition(edition === 'EE')('History', () => {
await titleInput.fill('Being from Kansas City');
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: /content history/i }).click();
await goToHistoryPage(page);
await page.waitForURL(HISTORY_URL);
await expect(versionCards).toHaveCount(2);
// Assert the most recent version is the current version
Expand All @@ -105,8 +111,8 @@ describeOnCondition(edition === 'EE')('History', () => {
*/
await page.getByRole('button', { name: 'Publish' }).click();
// Go to the history page
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: /content history/i }).click();
// Go to the history page
await goToHistoryPage(page);
await page.waitForURL(HISTORY_URL);
// Publish also creates a new draft so we expect the count to increase by 2
await expect(versionCards).toHaveCount(4);
Expand All @@ -129,8 +135,7 @@ describeOnCondition(edition === 'EE')('History', () => {
await titleInput.fill('Being from Kansas City, Missouri');
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: /content history/i }).click();
await goToHistoryPage(page);
await page.waitForURL(HISTORY_URL);
await expect(versionCards).toHaveCount(5);
// Assert the current version is the modified version
Expand All @@ -148,7 +153,6 @@ describeOnCondition(edition === 'EE')('History', () => {
/**
* Create an initial entry to also create an initial version
*/
await page.goto('/admin');
await page.getByRole('link', { name: 'Content Manager' }).click();
await page.getByRole('link', { name: /Create new entry/, exact: true }).click();
await page.waitForURL(CREATE_URL);
Expand Down Expand Up @@ -190,8 +194,7 @@ describeOnCondition(edition === 'EE')('History', () => {
/**
* Go to the history page
*/
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: 'Content History' }).click();
await goToHistoryPage(page);
await page.waitForURL(HISTORY_URL);
const versionCards = await page.getByRole('listitem', { name: 'Version card' });
await expect(versionCards).toHaveCount(2);
Expand Down Expand Up @@ -238,8 +241,7 @@ describeOnCondition(edition === 'EE')('History', () => {
await titleInput.fill(frenchTitle);
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: /content history/i }).click();
await goToHistoryPage(page);
await page.waitForURL(HISTORY_URL);
await expect(titleInput).toHaveValue(frenchTitle);

Expand All @@ -253,8 +255,7 @@ describeOnCondition(edition === 'EE')('History', () => {
await titleInput.fill(englishTitle);
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: /content history/i }).click();
await goToHistoryPage(page);
await page.waitForURL(HISTORY_URL);
const versionCards = await page.getByRole('listitem', { name: 'Version card' });
await expect(versionCards).toHaveCount(1);
Expand All @@ -279,14 +280,14 @@ describeOnCondition(edition === 'EE')('History', () => {
*/
await page.getByRole('textbox', { name: 'title' }).fill('Welcome to AFC Richmond');
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: /content history/i }).click();
// Go to the history page
await goToHistoryPage(page);
await page.waitForURL(HISTORY_URL);
await expect(versionCards).toHaveCount(2);
// Assert the most recent version is the current version
await expect(titleInput).toHaveValue('Welcome to AFC Richmond');
// Assert the previous version in the list is the expected version
previousVersion.click();
await previousVersion.click();
await expect(titleInput).toHaveValue('AFC Richmond');

// Go back to the entry
Expand All @@ -296,16 +297,16 @@ describeOnCondition(edition === 'EE')('History', () => {
* Publish
*/
await page.getByRole('button', { name: 'Publish' }).click();
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: /content history/i }).click();
// Go to the history page
await goToHistoryPage(page);
await page.waitForURL('**/content-manager/single-types/api::homepage.homepage/history**');
// Publish also creates a new draft so we expect the count to increase by 2
await expect(versionCards).toHaveCount(4);
// The current version is the most recent draft
await expect(currentVersion.getByText('Draft')).toBeVisible();
await expect(titleInput).toHaveValue('Welcome to AFC Richmond');
// The second in the list is the published version
previousVersion.click();
await previousVersion.click();
await expect(previousVersion.getByText('Published')).toBeVisible();
await expect(titleInput).toHaveValue('Welcome to AFC Richmond');

Expand All @@ -318,8 +319,7 @@ describeOnCondition(edition === 'EE')('History', () => {
await titleInput.fill('Welcome to AFC Richmond!');
await page.getByRole('button', { name: 'Save' }).click();
// Go to the history page
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: /content history/i }).click();
await goToHistoryPage(page);
await page.waitForURL(HISTORY_URL);
await expect(versionCards).toHaveCount(5);
// Assert the current version is the most recent published version
Expand Down Expand Up @@ -373,14 +373,13 @@ describeOnCondition(edition === 'EE')('History', () => {
/**
* Go to the history page
*/
await page.getByRole('button', { name: /more actions/i }).click();
await page.getByRole('menuitem', { name: 'Content History' }).click();
await goToHistoryPage(page);
await page.waitForURL(HISTORY_URL);
const versionCards = await page.getByRole('listitem', { name: 'Version card' });
await expect(versionCards).toHaveCount(2);

const previousVersion = versionCards.nth(1);
previousVersion.click();
await previousVersion.click();

// Assert the unknown field is present
await expect(page.getByText('Unknown fields')).toBeVisible();
Expand Down