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

Added type check to webhook signature #10

Merged
merged 9 commits into from
May 28, 2021
Merged
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ yarn add strapi-plugin-mux-video-uploader

### Webhooks

**Please note**: We've currently disabled webhook signature verification as there is not a way to access the raw request body from the Koa.js middleware (which Strapi is using for parsing requests). This is needed to ensure that we are verifying the signature and that the request JSON payload has properties in the same order that was used for generating the signature.

When setting up your Webhook configuration in the [Mux Dashboard](https://dashboard.mux.com/settings/webhooks), the "URL to notify" field should be in the format of—

```
Expand All @@ -47,9 +49,9 @@ On this view, set the appropriate values to their fields and click the Save butt

### Permissions

Currently, anyone that has administrative access to your Strapi instance will be able to utilize the plugin for uploading and managing content.
Currently, anyone with "Super Admin" access to your Strapi instance will be able to utilize the plugin for uploading and managing content.

The only real permission that needs to be set to function is the public access to the `muxwebhookhandler` method. This is needed so that Mux can send Webhook events to your Strapi instance for updating `MuxAsset` content types.
Aside from admin permissions, a **public** user role needs to be configured to allow access to the `muxwebhookhandler` method. This is needed so that Mux can send Webhook events to your Strapi instance for updating `MuxAsset` content types.

To enable this permission, do the following steps—

Expand Down
9 changes: 9 additions & 0 deletions README_INTERNAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Releasing

1. Merge feature branches, bug fixes, and whatever changes into `master` after CI passes and PRs are approved
1. Create a new branch off `master` when you're ready to release a new version
1. On this branch run `npm version [...]` (see `npm-version` [docs](https://docs.npmjs.com/cli/v7/commands/npm-version) for more info) which will bump the version in `package.json` and make a tag (for example `npm version patch -m "Bump for 3.1.2"`). Follow [SemVer rules](https://semver.org/) for patch/minor/major.
1. Push the version commit and the tag `git push && git push --tags origin`
1. Open Pull Request, "Rebase and merge" after approved
1. Create a new release in the Github UI, give the release a name and add release notes (creating the release will kick off npm publish)
1. Publish the new version to npmjs.com using the `npm publish` command
17 changes: 10 additions & 7 deletions admin/src/containers/App/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { NotFound } from 'strapi-helper-plugin';
import { CheckPagePermissions, NotFound } from 'strapi-helper-plugin';

import pluginPermissions from './../../permissions';
import pluginId from '../../pluginId';
import HomePage from '../HomePage';

const App = () => {
return (
<div>
<Switch>
<Route path={`/plugins/${pluginId}`} component={HomePage} exact />
<Route component={NotFound} />
</Switch>
</div>
<>
<CheckPagePermissions permissions={pluginPermissions.main}>
<Switch>
<Route path={`/plugins/${pluginId}`} component={HomePage} exact />
<Route component={NotFound} />
</Switch>
</CheckPagePermissions>
</>
);
};

Expand Down
121 changes: 64 additions & 57 deletions admin/src/containers/Settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import { Button, Flex, InputText } from '@buffetjs/core';
import { Label } from 'strapi-helper-plugin';
import { CheckPagePermissions, Label } from 'strapi-helper-plugin';
import styled from 'styled-components';

import Well from './../../components/well';
import { setMuxSettings } from '../../services/strapi';
import pluginPermissions from './../../permissions';

const ContainerStyled = styled.div`
&> * {
Expand Down Expand Up @@ -90,64 +91,70 @@ const Settings = () => {
}, []);

return (
<ContainerStyled>
<Flex alignItems='center' justifyContent='space-between'>
<div>
<H1Styled>Settings</H1Styled>
<SubHeadingStyled>Mux Video Uploader</SubHeadingStyled>
</div>
<ButtonWrapperStyled>
<Button color="cancel" label="Cancel" onClick={onCancelClick} disabled={cancelDisabled} />
<Button color="success" label="Save" onClick={onSaveClick} />
</ButtonWrapperStyled>
</Flex>
<Flex>
<Well>
<ShortRowStyled>
<>
<CheckPagePermissions
permissions={pluginPermissions.settings}
>
<ContainerStyled>
<Flex alignItems='center' justifyContent='space-between'>
<div>
<Label message='Access Token' />
<InputText
name="access_token"
onChange={({ target: { value } }: InputTextOnChange) => {
setAccesToken(value);
setCancelDisabled(false);
}}
type="password"
value={accessToken}
/>
<H1Styled>Settings</H1Styled>
<SubHeadingStyled>Mux Video Uploader</SubHeadingStyled>
</div>
</ShortRowStyled>
<LongRowStyled>
<div>
<Label message='Secret Key' />
<InputText
name="secret_key"
onChange={({ target: { value } }: InputTextOnChange) => {
setSecretKey(value);
setCancelDisabled(false);
}}
type="password"
value={secretKey}
/>
</div>
</LongRowStyled>
<ShortRowStyled>
<div>
<Label message='Webhook Signing Secret' />
<InputText
name="webhook_signing_secret"
onChange={({ target: { value } }: InputTextOnChange) => {
setWebhookSigningSecret(value);
setCancelDisabled(false);
}}
type="password"
value={webhookSigningSecret}
/>
</div>
</ShortRowStyled>
</Well>
</Flex>
</ContainerStyled>
<ButtonWrapperStyled>
<Button color="cancel" label="Cancel" onClick={onCancelClick} disabled={cancelDisabled} />
<Button color="success" label="Save" onClick={onSaveClick} />
</ButtonWrapperStyled>
</Flex>
<Flex>
<Well>
<ShortRowStyled>
<div>
<Label message='Access Token' />
<InputText
name="access_token"
onChange={({ target: { value } }: InputTextOnChange) => {
setAccesToken(value);
setCancelDisabled(false);
}}
type="password"
value={accessToken}
/>
</div>
</ShortRowStyled>
<LongRowStyled>
<div>
<Label message='Secret Key' />
<InputText
name="secret_key"
onChange={({ target: { value } }: InputTextOnChange) => {
setSecretKey(value);
setCancelDisabled(false);
}}
type="password"
value={secretKey}
/>
</div>
</LongRowStyled>
<ShortRowStyled>
<div>
<Label message='Webhook Signing Secret' />
<InputText
name="webhook_signing_secret"
onChange={({ target: { value } }: InputTextOnChange) => {
setWebhookSigningSecret(value);
setCancelDisabled(false);
}}
type="password"
value={webhookSigningSecret}
/>
</div>
</ShortRowStyled>
</Well>
</Flex>
</ContainerStyled>
</CheckPagePermissions>
</>
);
};

Expand Down
46 changes: 21 additions & 25 deletions admin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,14 @@ import Initializer from './containers/Initializer';
import lifecycles from './lifecycles';
import trads from './translations';
import Settings from './containers/Settings';
import getTrad from './utils/getTrad';
import pluginPermissions from './permissions';

export default (strapi:any) => {
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
const icon = pluginPkg.strapi.icon;
const name = pluginPkg.strapi.name;

const menuSection = {
id: pluginId,
title: {
id: `${pluginId}.foo`,
defaultMessage: 'Mux Video Uploader',
},
links: [
{
title: 'General',
to: `${strapi.settingsBaseURL}/${pluginId}/general`,
name: 'General',
}
],
};

const plugin = {
blockerComponent: null,
blockerComponentProps: {},
Expand All @@ -35,16 +22,31 @@ export default (strapi:any) => {
initializer: Initializer,
injectedComponents: [],
isReady: false,
// isRequired: pluginPkg.strapi.required || false,
// @ts-ignore
isRequired: pluginPkg.strapi.required || false,
layout: null,
lifecycles,
mainComponent: App,
name,
preventComponentRendering: false,
trads,
settings: {
mainComponent: Settings,
menuSection,
menuSection: {
id: pluginId,
title: getTrad('SettingsNav.section-label'),
links: [
{
title: {
id: getTrad('SettingsNav.link.settings'),
defaultMessage: 'Settings',
},
name: 'settings',
to: `${strapi.settingsBaseURL}/${pluginId}`,
Component: Settings,
permissions: pluginPermissions.settings
},
],
}
},
menu: {
pluginsSectionLinks: [
Expand All @@ -56,13 +58,7 @@ export default (strapi:any) => {
defaultMessage: name,
},
name,
permissions: [
// Uncomment to set the permissions of the plugin here
// {
// action: '', // the action name should be plugins::plugin-name.actionType
// subject: null,
// },
],
permissions: pluginPermissions.main
},
],
},
Expand Down
12 changes: 12 additions & 0 deletions admin/src/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pluginId from './pluginId';

const pluginPermissions = {
// This permission regards the main component (App) and is used to tell
// If the plugin link should be displayed in the menu
// And also if the plugin is accessible. This use case is found when a user types the url of the
// plugin directly in the browser
settings: [{ action: `plugins::${pluginId}.settings.write`, subject: null }],
main: [{ action: `plugins::${pluginId}.read`, subject: null }]
};

export default pluginPermissions;
5 changes: 4 additions & 1 deletion admin/src/translations/en.json
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
{}
{
"SettingsNav.section-label": "Mux Video Uploader",
"SettingsNav.link.settings": "Settings"
}
21 changes: 21 additions & 0 deletions config/functions/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pluginId from './../../admin/src/pluginId';

export = async () => {
const actions = [
{
section: "settings",
category: "Mux Video Uploader",
displayName: "Access the Mux Video Uploader Settings page",
uid: "settings.write",
pluginName: pluginId
},
{
section: 'plugins',
displayName: 'Read',
uid: 'read',
pluginName: pluginId
},
];

await strapi.admin.services.permission.actionProvider.registerMany(actions);
};
2 changes: 1 addition & 1 deletion controllers/mux-asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { sanitizeEntity, parseMultipartData } from 'strapi-utils';
import { Context } from 'koa';

import pluginId from '../admin/src/pluginId';
import pluginId from './../admin/src/pluginId';

const model = `plugins::${pluginId}.mux-asset`;

Expand Down
37 changes: 27 additions & 10 deletions controllers/mux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Mux from '@mux/mux-node';
import { Context } from 'koa';

import { getConfig } from '../services/mux';
import pluginId from '../admin/src/pluginId';
import pluginId from './../admin/src/pluginId';

const { Webhooks } = Mux;

Expand Down Expand Up @@ -71,24 +71,41 @@ const submitRemoteUpload = async (ctx:Context) => {

const muxWebhookHandler = async (ctx:Context) => {
const body = ctx.request.body;
const sig = ctx.request.headers['mux-signature'];
const sigHttpHeader = ctx.request.headers['mux-signature'];

const config = await getConfig('general');

if(sig === undefined || sig === '') {
if(sigHttpHeader === undefined || sigHttpHeader === '' || (Array.isArray(sigHttpHeader) && sigHttpHeader.length < 0)) {
ctx.throw(401, 'Webhook signature is missing');
}

let isSigValid;

try {
isSigValid = Webhooks.verifyHeader(JSON.stringify(body), sig, config.webhook_signing_secret);
} catch(err) {
ctx.throw(403, err);
if(Array.isArray(sigHttpHeader) && sigHttpHeader.length > 1) {
ctx.throw(401, 'we have an unexpected amount of signatures');
}

return;
let sig;

if(Array.isArray(sigHttpHeader)){
sig = sigHttpHeader[0];
}
else{
sig = sigHttpHeader;
}

// TODO: Currently commented out because we should be using the raw request body for verfiying
// Webhook signatures, NOT JSON.stringify. Strapi does not currently allow for access to the
// Koa.js request (the middleware used for parsing requests).

// let isSigValid;

// try {
// isSigValid = Webhooks.verifyHeader(JSON.stringify(body), sig, config.webhook_signing_secret);
// } catch(err) {
// ctx.throw(403, err);

// return;
// }

const { type, data } = body;

let payload;
Expand Down
Loading