Skip to content

Commit

Permalink
Merge pull request #2357 from botpress/ya-fix-perms
Browse files Browse the repository at this point in the history
fix(core): fix permissions view
  • Loading branch information
allardy committed Sep 19, 2019
2 parents 077dbc9 + 28e3df7 commit 23eea72
Show file tree
Hide file tree
Showing 20 changed files with 186 additions and 105 deletions.
16 changes: 9 additions & 7 deletions modules/qna/src/views/full/FormModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button, Checkbox, Classes, Dialog, FormGroup, H6, Intent, TextArea } from '@blueprintjs/core'
// @ts-ignore
import ElementsList from 'botpress/elements-list'
import { AccessControl } from 'botpress/utils'
import classnames from 'classnames'
import _ from 'lodash'
import some from 'lodash/some'
Expand Down Expand Up @@ -346,13 +347,14 @@ export default class FormModal extends Component<Props> {
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button id="btn-cancel" text="Cancel" onClick={this.closeAndClear} />
<Button
id="btn-submit"
tabIndex={3}
text={isEdit ? 'Edit' : 'Save'}
intent={Intent.PRIMARY}
onClick={this.handleSubmit}
/>
<AccessControl resource="module.qna" operation="write">
<Button
id="btn-submit"
text={isEdit ? 'Edit' : 'Save'}
intent={Intent.PRIMARY}
onClick={this.handleSubmit}
/>
</AccessControl>
</div>
</div>
</Dialog>
Expand Down
5 changes: 4 additions & 1 deletion modules/qna/src/views/full/ImportModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Button, Callout, Classes, Dialog, FileInput, FormGroup, Intent, Radio, RadioGroup } from '@blueprintjs/core'
import 'bluebird-global'
import { AccessControl } from 'botpress/utils'
import { toastFailure, toastSuccess } from 'botpress/utils'
import _ from 'lodash'
import React, { FC, Fragment, useEffect, useState } from 'react'
Expand Down Expand Up @@ -233,7 +234,9 @@ Either the file is empty, or it doesn't match any known format.`)

return (
<Fragment>
<Button icon="download" id="btn-importJson" text="Import JSON" onClick={() => setDialogOpen(true)} />
<AccessControl resource="module.qna" operation="write">
<Button icon="download" id="btn-importJson" text="Import JSON" onClick={() => setDialogOpen(true)} />
</AccessControl>

<Dialog
isOpen={isDialogOpen}
Expand Down
23 changes: 14 additions & 9 deletions modules/qna/src/views/full/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Button, Icon, Intent, Position, Switch, Tooltip } from '@blueprintjs/co
import { Container } from 'botpress/ui'
import { ElementPreview } from 'botpress/utils'
import { Downloader } from 'botpress/utils'
import { AccessControl } from 'botpress/utils'
import classnames from 'classnames'
import React, { Component } from 'react'
import { ButtonGroup, ButtonToolbar, FormControl, FormGroup, Pagination, Panel, Well } from 'react-bootstrap'
Expand Down Expand Up @@ -211,13 +212,15 @@ export default class QnaAdmin extends Component<Props> {
placeholder="Search for a category"
/>

<Button
id="btn-create-qna"
text="Add new"
icon="add"
intent={Intent.PRIMARY}
onClick={() => this.setState({ QnAModalType: 'create', currentItemId: null, showQnAModal: true })}
/>
<AccessControl resource="module.qna" operation="write">
<Button
id="btn-create-qna"
text="Add new"
icon="add"
intent={Intent.PRIMARY}
onClick={() => this.setState({ QnAModalType: 'create', currentItemId: null, showQnAModal: true })}
/>
</AccessControl>
</React.Fragment>
)

Expand Down Expand Up @@ -324,8 +327,10 @@ export default class QnaAdmin extends Component<Props> {
) : null}
</div>
<div className={style.itemAction}>
<Button icon="trash" className={style.itemActionDelete} onClick={this.deleteItem(id)} minimal={true} />
<Switch checked={item.enabled} onChange={this.toggleEnableItem.bind(this, item, id)} large={true} />
<AccessControl resource="module.qna" operation="write">
<Button icon="trash" className={style.itemActionDelete} onClick={this.deleteItem(id)} minimal={true} />
<Switch checked={item.enabled} onChange={this.toggleEnableItem.bind(this, item, id)} large={true} />
</AccessControl>
</div>
</Well>
)
Expand Down
13 changes: 11 additions & 2 deletions src/bp/core/routers/bots/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { URL } from 'url'
import { disableForModule } from '../conditionalMiddleware'
import { CustomRouter } from '../customRouter'
import { NotFoundError } from '../errors'
import { checkTokenHeader, needPermissions } from '../util'
import { checkMethodPermissions, checkTokenHeader, needPermissions } from '../util'

const debugMedia = DEBUG('audit:action:media-upload')
const DEFAULT_MAX_SIZE = '10mb'
Expand All @@ -49,6 +49,7 @@ export class BotsRouter extends CustomRouter {
private ghostService: GhostService
private checkTokenHeader: RequestHandler
private needPermissions: (operation: string, resource: string) => RequestHandler
private checkMethodPermissions: (resource: string) => RequestHandler
private machineId: string | undefined
private botpressConfig: BotpressConfig | undefined
private workspaceService: WorkspaceService
Expand Down Expand Up @@ -79,6 +80,7 @@ export class BotsRouter extends CustomRouter {
this.ghostService = args.ghostService
this.workspaceService = args.workspaceService
this.needPermissions = needPermissions(this.workspaceService)
this.checkMethodPermissions = checkMethodPermissions(this.workspaceService)
this.checkTokenHeader = checkTokenHeader(this.authService, TOKEN_AUDIENCE)
this.mediaPathRegex = new RegExp(/^\/api\/v(\d)\/bots\/[A-Z0-9_-]+\/media\//, 'i')
}
Expand Down Expand Up @@ -130,7 +132,12 @@ export class BotsRouter extends CustomRouter {
const router = Router({ mergeParams: true })
if (_.get(options, 'checkAuthentication', true)) {
router.use(this.checkTokenHeader)
router.use(this.needPermissions('write', identity))

if (options && options.checkMethodPermissions) {
router.use(this.checkMethodPermissions(identity))
} else {
router.use(this.needPermissions('write', identity))
}
}

if (!_.get(options, 'enableJsonBodyParser', true)) {
Expand Down Expand Up @@ -190,6 +197,7 @@ export class BotsRouter extends CustomRouter {
}

const config = await this.configProvider.getBotpressConfig()
const workspaceId = await this.workspaceService.getBotWorkspaceId(botId)

const data = this.studioParams(botId)
const liteEnv = `
Expand Down Expand Up @@ -217,6 +225,7 @@ export class BotsRouter extends CustomRouter {
window.APP_NAME = "${data.botpress.name}";
window.SHOW_POWERED_BY = ${!!config.showPoweredBy};
window.BOT_LOCKED = ${!!bot.locked};
window.WORKSPACE_ID = "${workspaceId}";
window.SOCKET_TRANSPORTS = ["${getSocketTransports(config).join('","')}"];
${app === 'studio' ? studioEnv : ''}
${app === 'lite' ? liteEnv : ''}
Expand Down
14 changes: 14 additions & 0 deletions src/bp/core/routers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,20 @@ export const needPermissions = (workspaceService: WorkspaceService) => (operatio
return next(err)
}

/**
* This method checks if the user has read access if method is get, and write access otherwise
*/
export const checkMethodPermissions = (workspaceService: WorkspaceService) => (resource: string) => async (
req: RequestWithUser,
_res: Response,
next: NextFunction
) => {
const method = req.method.toLowerCase()
const operation = method === 'get' || method === 'options' ? 'read' : 'write'
const err = await checkPermissions(workspaceService)(operation, resource)(req)
return next(err)
}

/**
* Just like needPermissions but returns a boolean and can be used inside an express middleware
*/
Expand Down
2 changes: 1 addition & 1 deletion src/bp/pro
Submodule pro updated from 7bc929 to 364f35
7 changes: 7 additions & 0 deletions src/bp/sdk/botpress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,13 @@ declare module 'botpress/sdk' {
*/
checkAuthentication: RouterCondition

/**
* When checkAuthentication is enabled, set this to true to enforce permissions based on the method.
* GET/OPTIONS requests requires READ permissions, while all other requires WRITE permissions
* @default true
*/
checkMethodPermissions?: RouterCondition

/**
* Parse the body as JSON when possible
* @default true
Expand Down
64 changes: 28 additions & 36 deletions src/bp/ui-studio/src/web/components/ContentForm/UploadWidget.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'

import { Button, FormGroup, InputGroup, FormControl, HelpBlock } from 'react-bootstrap'
import PermissionsChecker from '~/components/Layout/PermissionsChecker'
import { AccessControl } from '~/components/Shared/Utils'
import Loading from '~/components/Util/Loading'
import axios from 'axios'

Expand Down Expand Up @@ -65,10 +64,9 @@ class UploadWidget extends Component {
const { expanded, uploading, error } = this.state

return (
<PermissionsChecker
user={this.props.user}
op="write"
res="bot.media"
<AccessControl
operation="write"
resource="bot.media"
fallback={<em>Youd don&apos;t have permission to upload files for this bot. Talk to your team owner.</em>}
>
<FormGroup>
Expand All @@ -91,38 +89,32 @@ class UploadWidget extends Component {
<HelpBlock>No file picked yet.</HelpBlock>
)}
</FormGroup>
{expanded &&
!uploading && (
<FormGroup>
<InputGroup>
<FormControl
type="file"
accept={filter || '*'}
placeholder={this.props.placeholder || 'Pick file to upload'}
onChange={this.startUpload}
/>
<InputGroup.Button>
<Button bsStyle="link" onClick={this.collapse}>
Cancel
</Button>
</InputGroup.Button>
</InputGroup>
</FormGroup>
)}
{expanded &&
error && (
<FormGroup validationState="error">
<HelpBlock>{error}</HelpBlock>
</FormGroup>
)}
{expanded && !uploading && (
<FormGroup>
<InputGroup>
<FormControl
type="file"
accept={filter || '*'}
placeholder={this.props.placeholder || 'Pick file to upload'}
onChange={this.startUpload}
/>
<InputGroup.Button>
<Button bsStyle="link" onClick={this.collapse}>
Cancel
</Button>
</InputGroup.Button>
</InputGroup>
</FormGroup>
)}
{expanded && error && (
<FormGroup validationState="error">
<HelpBlock>{error}</HelpBlock>
</FormGroup>
)}
{expanded && uploading && <Loading />}
</PermissionsChecker>
</AccessControl>
)
}
}

const mapStateToProps = state => ({
user: state.user
})

export default connect(mapStateToProps)(UploadWidget)
export default UploadWidget
21 changes: 0 additions & 21 deletions src/bp/ui-studio/src/web/components/Layout/PermissionsChecker.js

This file was deleted.

13 changes: 5 additions & 8 deletions src/bp/ui-studio/src/web/components/Layout/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import { connect } from 'react-redux'
import { NavLink, withRouter } from 'react-router-dom'
import classnames from 'classnames'
import { Tooltip, OverlayTrigger } from 'react-bootstrap'
import { Collapse } from 'react-bootstrap'
import { GoBeaker } from 'react-icons/go'
import _ from 'lodash'

import PermissionsChecker from './PermissionsChecker'
import { AccessControl } from '../Shared/Utils'

const style = require('./Sidebar.scss')

Expand Down Expand Up @@ -66,7 +64,7 @@ class Sidebar extends React.Component {
)

return (
<PermissionsChecker key={`menu_module_${module.name}`} user={this.props.user} res={rule.res} op={rule.op}>
<AccessControl key={`menu_module_${module.name}`} resource={rule.res} operation={rule.op}>
<li id={`bp-menu_${module.name}`}>
<NavLink
to={path}
Expand All @@ -83,12 +81,12 @@ class Sidebar extends React.Component {
)}
</NavLink>
</li>
</PermissionsChecker>
</AccessControl>
)
}

renderBasicItem = ({ name, path, rule, icon, renderSuffix }) => (
<PermissionsChecker user={this.props.user} res={rule.res} op={rule.op} key={name}>
<AccessControl resource={rule.res} operation={rule.op} key={name}>
<li id={`bp-menu_${name}`} key={path}>
<NavLink to={path} title={name} activeClassName={style.active} onClick={this.handleMenuItemClicked}>
<i className="icon material-icons" style={{ marginRight: '5px' }}>
Expand All @@ -98,7 +96,7 @@ class Sidebar extends React.Component {
{renderSuffix && renderSuffix()}
</NavLink>
</li>
</PermissionsChecker>
</AccessControl>
)

render() {
Expand All @@ -123,7 +121,6 @@ class Sidebar extends React.Component {
}

const mapStateToProps = state => ({
user: state.user,
viewMode: state.ui.viewMode,
modules: state.modules
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import { updateDocumentationModal } from '~/actions'
import LangSwitcher from './LangSwitcher'
import BotSwitcher from './BotSwitcher'
import ActionItem from './ActionItem'
import PermissionsChecker from '../PermissionsChecker'
import NotificationHub from '~/components/Notifications/Hub'
import { GoMortarBoard } from 'react-icons/go'
import NluPerformanceStatus from './NluPerformanceStatus'

import axios from 'axios'
import { AccessControl } from '~/components/Shared/Utils'
import ConfigStatus from './ConfigStatus'

const COMPLETED_DURATION = 2000
Expand Down Expand Up @@ -164,7 +164,7 @@ class StatusBar extends React.Component {
updateSyncStatus={syncedStatus => this.setState({ nluSynced: syncedStatus })}
synced={this.state.nluSynced}
/>
<PermissionsChecker user={this.props.user} res="bot.logs" op="read">
<AccessControl resource="bot.logs" operation="read">
<ActionItem
id="statusbar_logs"
title="Logs Panel"
Expand All @@ -175,7 +175,7 @@ class StatusBar extends React.Component {
>
<Icon icon="console" />
</ActionItem>
</PermissionsChecker>
</AccessControl>
<ActionItem
onClick={this.props.onToggleGuidedTour}
title="Toggle Guided Tour"
Expand All @@ -202,7 +202,6 @@ class StatusBar extends React.Component {
}

const mapStateToProps = state => ({
user: state.user,
botInfo: state.bot,
docHints: state.ui.docHints,
contentLang: state.language.contentLang
Expand Down
Loading

0 comments on commit 23eea72

Please sign in to comment.