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

[Branding] Update resource validation procedure. #1019

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/server/rendering/__mocks__/rendering_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ export const setupMock: jest.Mocked<InternalRenderingServiceSetup> = {
};
export const mockSetup = jest.fn().mockResolvedValue(setupMock);
export const mockStop = jest.fn();
export const mockIsResourceValid = jest.fn();
export const mockIsUrlValid = jest.fn();
export const mockIsTitleValid = jest.fn();
export const mockRenderingService: jest.Mocked<IRenderingService> = {
setup: mockSetup,
stop: mockStop,
isResourceValid: mockIsResourceValid,
isUrlValid: mockIsUrlValid,
isTitleValid: mockIsTitleValid,
};
Expand Down
23 changes: 23 additions & 0 deletions src/core/server/rendering/rendering_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,29 @@ describe('RenderingService', () => {
});
});

describe('isResourceValid()', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add functional tests to accompany the existing custom branding functional tests?

it('checks valid URL', async () => {
const result = await service.isResourceValid(
'https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_default.svg',
'config'
);
expect(result).toEqual(true);
});

it('checks valid SVG', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add examples for a PNG and GIF? Also an example of text and/or JavaScript failing would be ideal.

const result = await service.isResourceValid(
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNjQgNjQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik02MS43Mzc0IDIzLjVDNjAuNDg3OCAyMy41IDU5LjQ3NDggMjQuNTEzIDU5LjQ3NDggMjUuNzYyNkM1OS40NzQ4IDQ0LjM4MTMgNDQuMzgxMyA1OS40NzQ4IDI1Ljc2MjYgNTkuNDc0OEMyNC41MTMgNTkuNDc0OCAyMy41IDYwLjQ4NzggMjMuNSA2MS43Mzc0QzIzLjUgNjIuOTg3IDI0LjUxMyA2NCAyNS43NjI2IDY0QzQ2Ljg4MDUgNjQgNjQgNDYuODgwNSA2NCAyNS43NjI2QzY0IDI0LjUxMyA2Mi45ODcgMjMuNSA2MS43Mzc0IDIzLjVaIiBmaWxsPSIjMDA1RUI4Ii8+CjxwYXRoIGQ9Ik00OC4wODE0IDM4QzUwLjI1NzIgMzQuNDUwNSA1Mi4zNjE1IDI5LjcxNzggNTEuOTQ3NSAyMy4wOTIxQzUxLjA4OTkgOS4zNjcyNSAzOC42NTg5IC0xLjA0NDYzIDI2LjkyMDYgMC4wODM3MzI3QzIyLjMyNTMgMC41MjU0NjUgMTcuNjA2OCA0LjI3MTIgMTguMDI2IDEwLjk4MDVDMTguMjA4MiAxMy44OTYxIDE5LjYzNTIgMTUuNjE2OSAyMS45NTQ0IDE2LjkzOTlDMjQuMTYxOCAxOC4xOTkyIDI2Ljk5NzggMTguOTk2OSAzMC4yMTI4IDE5LjkwMTFDMzQuMDk2MiAyMC45OTM0IDM4LjYwMDkgMjIuMjIwMyA0Mi4wNjMgMjQuNzcxN0M0Ni4yMTI1IDI3LjgyOTUgNDkuMDQ5MSAzMS4zNzQzIDQ4LjA4MTQgMzhaIiBmaWxsPSIjMDAzQjVDIi8+CjxwYXRoIGQ9Ik0zLjkxODYxIDE0QzEuNzQyNzYgMTcuNTQ5NSAtMC4zNjE1MDYgMjIuMjgyMiAwLjA1MjQ5MzEgMjguOTA3OUMwLjkxMDA3MiA0Mi42MzI3IDEzLjM0MTEgNTMuMDQ0NiAyNS4wNzk0IDUxLjkxNjNDMjkuNjc0NyA1MS40NzQ1IDM0LjM5MzIgNDcuNzI4OCAzMy45NzQgNDEuMDE5NUMzMy43OTE4IDM4LjEwMzkgMzIuMzY0NyAzNi4zODMxIDMwLjA0NTYgMzUuMDYwMUMyNy44MzgyIDMzLjgwMDggMjUuMDAyMiAzMy4wMDMxIDIxLjc4NzIgMzIuMDk4OUMxNy45MDM4IDMxLjAwNjYgMTMuMzk5MSAyOS43Nzk3IDkuOTM2OTQgMjcuMjI4M0M1Ljc4NzQ2IDI0LjE3MDQgMi45NTA5MiAyMC42MjU3IDMuOTE4NjEgMTRaIiBmaWxsPSIjMDA1RUI4Ii8+Cjwvc3ZnPgo=',
'config'
);
expect(result).toEqual(true);
});

it('checks invalid resource', async () => {
const result = await service.isResourceValid('some garbage', 'config');
expect(result).toEqual(false);
});
});

describe('isUrlValid()', () => {
it('checks valid SVG URL', async () => {
const result = await service.isUrlValid(
Expand Down
39 changes: 32 additions & 7 deletions src/core/server/rendering/rendering_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ import { OpenSearchDashboardsConfigType } from '../opensearch_dashboards_config'

const DEFAULT_TITLE = 'OpenSearch Dashboards';

/**
* Magic constants for the specific mime types
* @see https://en.wikipedia.org/wiki/List_of_file_signatures
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see SVG in this list, and the GIF signature is different.

*/
const MIME_MAGIC = ['<svg', 'GIF8', '‰PNG'];

/** @internal */
export class RenderingService {
constructor(private readonly coreContext: CoreContext) {}
Expand Down Expand Up @@ -261,28 +267,28 @@ export class RenderingService {
opensearchDashboardsConfig: Readonly<OpenSearchDashboardsConfigType>
): Promise<BrandingValidation> => {
const branding = opensearchDashboardsConfig.branding;
const isLogoDefaultValid = await this.isUrlValid(branding.logo.defaultUrl, 'logo default');
const isLogoDefaultValid = await this.isResourceValid(branding.logo.defaultUrl, 'logo default');

const isLogoDarkmodeValid = darkMode
? await this.isUrlValid(branding.logo.darkModeUrl, 'logo darkMode')
? await this.isResourceValid(branding.logo.darkModeUrl, 'logo darkMode')
: false;

const isMarkDefaultValid = await this.isUrlValid(branding.mark.defaultUrl, 'mark default');
const isMarkDefaultValid = await this.isResourceValid(branding.mark.defaultUrl, 'mark default');

const isMarkDarkmodeValid = darkMode
? await this.isUrlValid(branding.mark.darkModeUrl, 'mark darkMode')
? await this.isResourceValid(branding.mark.darkModeUrl, 'mark darkMode')
: false;

const isLoadingLogoDefaultValid = await this.isUrlValid(
const isLoadingLogoDefaultValid = await this.isResourceValid(
branding.loadingLogo.defaultUrl,
'loadingLogo default'
);

const isLoadingLogoDarkmodeValid = darkMode
? await this.isUrlValid(branding.loadingLogo.darkModeUrl, 'loadingLogo darkMode')
? await this.isResourceValid(branding.loadingLogo.darkModeUrl, 'loadingLogo darkMode')
: false;

const isFaviconValid = await this.isUrlValid(branding.faviconUrl, 'favicon');
const isFaviconValid = await this.isResourceValid(branding.faviconUrl, 'favicon');

const isTitleValid = this.isTitleValid(branding.applicationTitle, 'applicationTitle');

Expand All @@ -306,6 +312,25 @@ export class RenderingService {
return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record<string, any>;
}

/**
* Validation function for resources provided for the branding.
* Checks if it`s a base64 encoded resource is it`s not check it as URL
*
* @param {string} resource
* @param {string} configName
* @returns {boolean} indicate if the provided resource can be used in the system
*/
public isResourceValid = async (resource: string, configName?: string): Promise<boolean> => {
if (resource.trim().startsWith('data:image')) {
const buff = Buffer.from(resource.replace(/^data:image\/.*;base64,/, ''), 'base64');
const extractedBytes = buff.subarray(0, 4).toString();
if (MIME_MAGIC.includes(extractedBytes)) {
return true;
}
}
return this.isUrlValid(resource, configName);
};

/**
* Validation function for URLs. Use Axios to call URL and check validity.
* Also needs to be ended with png, svg, gif, PNG, SVG and GIF.
Expand Down