Add require_scopes field to DevKey Management
Can now set whether a developer key require scopes or not. Legacy behavior of scopes is to allow dev keys to have access to all endpoints, this allows for the continuing behavior. Fixes: PLAT-3345 Test Plan: - Go to a create/upate a key - Notice the require scopes checkbox - Click the require scopes checkbox so that it is toggled off - Scopes ui should be removed and saving the key persists the state - attempt to access an endpoint using the changed endpoint that had scopes, it should be able to act like a legacy key - go back to the key management modal and flip the require scopes back to on. - attempt to access an endpoint that does not have a scope for the dev key, should fail - for a legacy key, the scopes ui should be off when first editing. Change-Id: I0a53ff8a44b80081b518d780a8288f4cc4c36027 Reviewed-on: https://gerrit.instructure.com/150216 QA-Review: August Thornton <august@instructure.com> Reviewed-by: Weston Dransfield <wdransfield@instructure.com> Product-Review: Jesse Poulos <jpoulos@instructure.com> Tested-by: Jenkins
This commit is contained in:
parent
5571a9e878
commit
489b5d6ecf
|
@ -148,6 +148,7 @@ class DeveloperKeysController < ApplicationController
|
|||
:redirect_uris,
|
||||
:vendor_code,
|
||||
:visible,
|
||||
:require_scopes,
|
||||
scopes: []
|
||||
)
|
||||
end
|
||||
|
|
|
@ -22,11 +22,32 @@ import I18n from 'i18n!react_developer_keys'
|
|||
import ScreenReaderContent from '@instructure/ui-core/lib/components/ScreenReaderContent'
|
||||
import TextArea from '@instructure/ui-core/lib/components/TextArea'
|
||||
import TextInput from '@instructure/ui-core/lib/components/TextInput'
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import DeveloperKeyScopes from './Scopes'
|
||||
|
||||
export default class DeveloperKeyFormFields extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const { developerKey } = props
|
||||
this.state = {
|
||||
requireScopes: developerKey && developerKey.require_scopes
|
||||
}
|
||||
}
|
||||
|
||||
get keyForm () {
|
||||
return this.keyFormRef
|
||||
}
|
||||
|
||||
get requireScopes () {
|
||||
return this.state.requireScopes
|
||||
}
|
||||
|
||||
setKeyFormRef = node => { this.keyFormRef = node }
|
||||
|
||||
fieldValue(field, defaultValue) {
|
||||
const {developerKey} = this.props
|
||||
if (Object.keys(developerKey).length > 0) {
|
||||
|
@ -35,74 +56,74 @@ export default class DeveloperKeyFormFields extends React.Component {
|
|||
return developerKey[field]
|
||||
}
|
||||
|
||||
developerKeySettings() {
|
||||
return (
|
||||
<FormFieldGroup
|
||||
rowSpacing="small"
|
||||
vAlign="middle"
|
||||
description={<ScreenReaderContent>{I18n.t('Developer Key Settings')}</ScreenReaderContent>}
|
||||
>
|
||||
<TextInput
|
||||
label={I18n.t('Key Name:')}
|
||||
name="developer_key[name]"
|
||||
defaultValue={this.fieldValue('name', 'Unnamed Tool')}
|
||||
/>
|
||||
<TextInput
|
||||
label={I18n.t('Owner Email:')}
|
||||
name="developer_key[email]"
|
||||
defaultValue={this.fieldValue('email')}
|
||||
/>
|
||||
<TextInput
|
||||
label={I18n.t('Redirect URI (Legacy):')}
|
||||
name="developer_key[redirect_uri]"
|
||||
defaultValue={this.fieldValue('redirect_uri')}
|
||||
/>
|
||||
<TextArea
|
||||
label={I18n.t('Redirect URIs:')}
|
||||
name="developer_key[redirect_uris]"
|
||||
defaultValue={this.fieldValue('redirect_uris')}
|
||||
resize="both"
|
||||
/>
|
||||
<TextInput
|
||||
label={I18n.t('Vendor Code (LTI 2):')}
|
||||
name="developer_key[vendor_code]"
|
||||
defaultValue={this.fieldValue('vendor_code')}
|
||||
/>
|
||||
<TextInput
|
||||
label={I18n.t('Icon URL:')}
|
||||
name="developer_key[icon_url]"
|
||||
defaultValue={this.fieldValue('icon_url')}
|
||||
/>
|
||||
<TextArea
|
||||
label={I18n.t('Notes:')}
|
||||
name="developer_key[notes]"
|
||||
defaultValue={this.fieldValue('notes')}
|
||||
resize="both"
|
||||
/>
|
||||
</FormFieldGroup>
|
||||
)
|
||||
}
|
||||
|
||||
scopeSettings() {
|
||||
return (
|
||||
<DeveloperKeyScopes
|
||||
developerKey={this.props.developerKey}
|
||||
availableScopes={this.props.availableScopes}
|
||||
availableScopesPending={this.props.availableScopesPending}
|
||||
store={this.props.store}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
)
|
||||
handleRequireScopesChange = () => {
|
||||
this.setState({ requireScopes: !this.state.requireScopes })
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid hAlign="center">
|
||||
<GridRow>
|
||||
<GridCol width={3}>{this.developerKeySettings()}</GridCol>
|
||||
<GridCol width={8}>{this.scopeSettings()}</GridCol>
|
||||
</GridRow>
|
||||
</Grid>
|
||||
<form ref={this.setKeyFormRef}>
|
||||
<Grid hAlign="center">
|
||||
<GridRow>
|
||||
<GridCol width={3}>
|
||||
<FormFieldGroup
|
||||
rowSpacing="small"
|
||||
vAlign="middle"
|
||||
description={<ScreenReaderContent>{I18n.t('Developer Key Settings')}</ScreenReaderContent>}
|
||||
>
|
||||
<TextInput
|
||||
label={I18n.t('Key Name:')}
|
||||
name="developer_key[name]"
|
||||
defaultValue={this.fieldValue('name', 'Unnamed Tool')}
|
||||
/>
|
||||
<TextInput
|
||||
label={I18n.t('Owner Email:')}
|
||||
name="developer_key[email]"
|
||||
defaultValue={this.fieldValue('email')}
|
||||
/>
|
||||
<TextInput
|
||||
label={I18n.t('Redirect URI (Legacy):')}
|
||||
name="developer_key[redirect_uri]"
|
||||
defaultValue={this.fieldValue('redirect_uri')}
|
||||
/>
|
||||
<TextArea
|
||||
label={I18n.t('Redirect URIs:')}
|
||||
name="developer_key[redirect_uris]"
|
||||
defaultValue={this.fieldValue('redirect_uris')}
|
||||
resize="both"
|
||||
/>
|
||||
<TextInput
|
||||
label={I18n.t('Vendor Code (LTI 2):')}
|
||||
name="developer_key[vendor_code]"
|
||||
defaultValue={this.fieldValue('vendor_code')}
|
||||
/>
|
||||
<TextInput
|
||||
label={I18n.t('Icon URL:')}
|
||||
name="developer_key[icon_url]"
|
||||
defaultValue={this.fieldValue('icon_url')}
|
||||
/>
|
||||
<TextArea
|
||||
label={I18n.t('Notes:')}
|
||||
name="developer_key[notes]"
|
||||
defaultValue={this.fieldValue('notes')}
|
||||
resize="both"
|
||||
/>
|
||||
</FormFieldGroup>
|
||||
</GridCol>
|
||||
<GridCol width={8}>
|
||||
<DeveloperKeyScopes
|
||||
availableScopes={this.props.availableScopes}
|
||||
availableScopesPending={this.props.availableScopesPending}
|
||||
developerKey={this.props.developerKey}
|
||||
requireScopes={this.state.requireScopes}
|
||||
onRequireScopesChange={this.handleRequireScopesChange}
|
||||
dispatch={this.props.dispatch}
|
||||
listDeveloperKeyScopesSet={this.props.listDeveloperKeyScopesSet}
|
||||
/>
|
||||
</GridCol>
|
||||
</GridRow>
|
||||
</Grid>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -112,19 +133,16 @@ DeveloperKeyFormFields.defaultProps = {
|
|||
}
|
||||
|
||||
DeveloperKeyFormFields.propTypes = {
|
||||
store: PropTypes.shape({
|
||||
dispatch: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
actions: PropTypes.shape({
|
||||
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
|
||||
developerKey: PropTypes.shape({
|
||||
notes: PropTypes.string,
|
||||
icon_url: PropTypes.string,
|
||||
vendor_code: PropTypes.string,
|
||||
redirect_uris: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
name: PropTypes.string
|
||||
name: PropTypes.string,
|
||||
require_scopes: PropTypes.bool
|
||||
}),
|
||||
availableScopes: PropTypes.objectOf(PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
|
|
|
@ -42,22 +42,37 @@ export default class DeveloperKeyModal extends React.Component {
|
|||
return this.developerKey() ? I18n.t('Create developer key') : I18n.t('Edit developer key')
|
||||
}
|
||||
|
||||
get submissionForm () {
|
||||
return this.newForm ? this.newForm.keyForm : <form />
|
||||
}
|
||||
|
||||
get requireScopes () {
|
||||
return this.newForm && this.newForm.requireScopes
|
||||
}
|
||||
|
||||
submitForm = () => {
|
||||
const method = this.developerKey() ? 'put' : 'post'
|
||||
const formData = new FormData(this.form)
|
||||
const scopesArrayKey = 'developer_key[scopes][]'
|
||||
const formData = new FormData(this.submissionForm)
|
||||
|
||||
this.props.selectedScopes.forEach((scope) => {
|
||||
formData.append(scopesArrayKey, scope)
|
||||
})
|
||||
|
||||
if (this.props.selectedScopes.length > 0) {
|
||||
this.props.store.dispatch(
|
||||
this.props.actions.createOrEditDeveloperKey(formData, this.developerKeyUrl(), method)
|
||||
)
|
||||
} else {
|
||||
if (!this.requireScopes) {
|
||||
formData.delete('developer_key[scopes][]')
|
||||
formData.append('developer_key[require_scopes]', false)
|
||||
} else if (this.props.selectedScopes.length === 0 &&
|
||||
this.props.createOrEditDeveloperKeyState.developerKey.scopes.length === 0
|
||||
) {
|
||||
$.flashError(I18n.t('At least one scope must be selected.'))
|
||||
return
|
||||
} else {
|
||||
const scopesArrayKey = 'developer_key[scopes][]'
|
||||
|
||||
this.props.selectedScopes.forEach((scope) => {
|
||||
formData.append(scopesArrayKey, scope)
|
||||
})
|
||||
formData.append('developer_key[require_scopes]', true)
|
||||
}
|
||||
this.props.store.dispatch(
|
||||
this.props.actions.createOrEditDeveloperKey(formData, this.developerKeyUrl(), method)
|
||||
)
|
||||
}
|
||||
|
||||
modalBody() {
|
||||
|
@ -76,26 +91,24 @@ export default class DeveloperKeyModal extends React.Component {
|
|||
}
|
||||
|
||||
developerKeyForm() {
|
||||
return (
|
||||
<form
|
||||
ref={el => {
|
||||
this.form = el
|
||||
}}
|
||||
>
|
||||
<DeveloperKeyFormFields
|
||||
developerKey={this.developerKey()}
|
||||
availableScopes={this.props.availableScopes}
|
||||
availableScopesPending={this.props.availableScopesPending}
|
||||
store={this.props.store}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
const {
|
||||
availableScopes,
|
||||
availableScopesPending,
|
||||
createOrEditDeveloperKeyState: { developerKey }
|
||||
} = this.props;
|
||||
|
||||
return <DeveloperKeyFormFields
|
||||
ref={this.setNewFormRef}
|
||||
developerKey={developerKey}
|
||||
availableScopes={availableScopes}
|
||||
availableScopesPending={availableScopesPending}
|
||||
dispatch={this.props.store.dispatch}
|
||||
listDeveloperKeyScopesSet={this.props.actions.listDeveloperKeyScopesSet}
|
||||
/>
|
||||
}
|
||||
|
||||
modalContainerRef = div => {
|
||||
this.modalContainer = div
|
||||
}
|
||||
setNewFormRef = node => { this.newForm = node }
|
||||
modalContainerRef = node => { this.modalContainer = node }
|
||||
|
||||
modalIsOpen() {
|
||||
return this.props.createOrEditDeveloperKeyState.developerKeyModalOpen
|
||||
|
@ -124,7 +137,7 @@ export default class DeveloperKeyModal extends React.Component {
|
|||
mountNode={this.props.mountNode}
|
||||
>
|
||||
<ModalHeader>
|
||||
<Heading level="h4">{I18n.t('Key Settings')}</Heading>
|
||||
<Heading level="h3" as="h2">{I18n.t('Key Settings')}</Heading>
|
||||
</ModalHeader>
|
||||
<ModalBody>{this.modalBody()}</ModalBody>
|
||||
<ModalFooter>
|
||||
|
@ -152,7 +165,8 @@ DeveloperKeyModal.propTypes = {
|
|||
actions: PropTypes.shape({
|
||||
createOrEditDeveloperKey: PropTypes.func.isRequired,
|
||||
developerKeysModalClose: PropTypes.func.isRequired,
|
||||
setEditingDeveloperKey: PropTypes.func.isRequired
|
||||
setEditingDeveloperKey: PropTypes.func.isRequired,
|
||||
listDeveloperKeyScopesSet: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
createOrEditDeveloperKeyState: PropTypes.shape({
|
||||
developerKeyCreateOrEditSuccessful: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -19,12 +19,19 @@
|
|||
import I18n from 'i18n!react_developer_keys'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import Billboard from '@instructure/ui-core/lib/components/Billboard'
|
||||
import Checkbox from '@instructure/ui-core/lib/components/Checkbox'
|
||||
import Flex, { FlexItem } from '@instructure/ui-layout/lib/components/Flex'
|
||||
import Grid, {GridCol, GridRow} from '@instructure/ui-core/lib/components/Grid'
|
||||
import IconWarning from 'instructure-icons/lib/Line/IconWarningLine'
|
||||
import IconSearchLine from 'instructure-icons/lib/Line/IconSearchLine'
|
||||
import ScreenReaderContent from '@instructure/ui-core/lib/components/ScreenReaderContent'
|
||||
import Spinner from '@instructure/ui-core/lib/components/Spinner'
|
||||
import Text from '@instructure/ui-core/lib/components/Text'
|
||||
import TextInput from '@instructure/ui-core/lib/components/TextInput'
|
||||
import View from '@instructure/ui-layout/lib/components/View'
|
||||
|
||||
import DeveloperKeyScopesList from './ScopesList'
|
||||
|
||||
export default class DeveloperKeyScopes extends React.Component {
|
||||
|
@ -36,7 +43,14 @@ export default class DeveloperKeyScopes extends React.Component {
|
|||
})
|
||||
}
|
||||
|
||||
enforceScopesSrText () {
|
||||
return this.props.requireScopes
|
||||
? I18n.t('Clicking the checkbox will cause scopes table to disappear below')
|
||||
: I18n.t('Clicking the checkbox will cause scopes table to appear below')
|
||||
}
|
||||
|
||||
body() {
|
||||
const { developerKey } = this.props
|
||||
if (this.props.availableScopesPending) {
|
||||
return (
|
||||
<GridRow hAlign="space-around">
|
||||
|
@ -48,16 +62,31 @@ export default class DeveloperKeyScopes extends React.Component {
|
|||
</GridRow>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GridRow>
|
||||
<GridCol>
|
||||
<DeveloperKeyScopesList
|
||||
availableScopes={this.props.availableScopes}
|
||||
selectedScopes={this.props.developerKey.scopes}
|
||||
filter={this.state.filter}
|
||||
actions={this.props.actions}
|
||||
dispatch={this.props.store.dispatch}
|
||||
/>
|
||||
<View>
|
||||
{this.props.requireScopes
|
||||
? <DeveloperKeyScopesList
|
||||
availableScopes={this.props.availableScopes}
|
||||
selectedScopes={developerKey ? developerKey.scopes : []}
|
||||
filter={this.state.filter}
|
||||
listDeveloperKeyScopesSet={this.props.listDeveloperKeyScopesSet}
|
||||
dispatch={this.props.dispatch}
|
||||
/>
|
||||
: <Billboard
|
||||
hero={<IconWarning />}
|
||||
size="large"
|
||||
headingAs="h2"
|
||||
headingLevel="h2"
|
||||
margin="xx-large"
|
||||
readOnly
|
||||
heading={I18n.t('When scope enforcement is disabled, tokens have access to all endpoints available to the authorizing user.')}
|
||||
/>
|
||||
|
||||
}
|
||||
</View>
|
||||
</GridCol>
|
||||
</GridRow>
|
||||
)
|
||||
|
@ -73,13 +102,30 @@ export default class DeveloperKeyScopes extends React.Component {
|
|||
</Text>
|
||||
</GridCol>
|
||||
<GridCol width="auto">
|
||||
<TextInput
|
||||
label={<ScreenReaderContent />}
|
||||
placeholder={I18n.t('Search endpoints')}
|
||||
type="search"
|
||||
icon={() => <IconSearchLine />}
|
||||
onChange={this.handleFilterChange}
|
||||
/>
|
||||
<Flex>
|
||||
<FlexItem padding="0 large 0 0" data-automation="enforce_scopes">
|
||||
<Checkbox
|
||||
variant="toggle"
|
||||
label={
|
||||
<span>
|
||||
<Text>{I18n.t('Enforce Scopes')}</Text>
|
||||
<ScreenReaderContent>{this.enforceScopesSrText()}</ScreenReaderContent>
|
||||
</span>
|
||||
}
|
||||
checked={this.props.requireScopes}
|
||||
onChange={this.props.onRequireScopesChange}
|
||||
/>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<TextInput
|
||||
label={<ScreenReaderContent />}
|
||||
placeholder={I18n.t('Search endpoints')}
|
||||
type="search"
|
||||
icon={() => <IconSearchLine />}
|
||||
onChange={this.handleFilterChange}
|
||||
/>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</GridCol>
|
||||
</GridRow>
|
||||
{this.body()}
|
||||
|
@ -96,12 +142,8 @@ DeveloperKeyScopes.propTypes = {
|
|||
})
|
||||
)).isRequired,
|
||||
availableScopesPending: PropTypes.bool.isRequired,
|
||||
store: PropTypes.shape({
|
||||
dispatch: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
actions: PropTypes.shape({
|
||||
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
|
||||
developerKey: PropTypes.shape({
|
||||
notes: PropTypes.string,
|
||||
icon_url: PropTypes.string,
|
||||
|
@ -110,7 +152,9 @@ DeveloperKeyScopes.propTypes = {
|
|||
email: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
scopes: PropTypes.arrayOf(PropTypes.string)
|
||||
})
|
||||
}),
|
||||
requireScopes: PropTypes.bool.isRequired,
|
||||
onRequireScopesChange: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
DeveloperKeyScopes.defaultProps = {
|
||||
|
|
|
@ -65,7 +65,7 @@ export default class DeveloperKeyScopesList extends React.Component {
|
|||
selectedScopes,
|
||||
readOnlySelected: this.onlySelectGet(selectedScopes)
|
||||
})
|
||||
this.props.dispatch(this.props.actions.listDeveloperKeyScopesSet(selectedScopes))
|
||||
this.props.dispatch(this.props.listDeveloperKeyScopesSet(selectedScopes))
|
||||
}
|
||||
|
||||
uniqueSelectedScopes(selectedScopes) {
|
||||
|
@ -94,7 +94,8 @@ export default class DeveloperKeyScopesList extends React.Component {
|
|||
selectedScopes: newScopes,
|
||||
readOnlySelected: event.currentTarget.checked
|
||||
})
|
||||
this.props.dispatch(this.props.actions.listDeveloperKeyScopesSet(newScopes))
|
||||
|
||||
this.props.dispatch(this.props.listDeveloperKeyScopesSet(newScopes))
|
||||
}
|
||||
|
||||
noFilter() {
|
||||
|
@ -176,9 +177,7 @@ export default class DeveloperKeyScopesList extends React.Component {
|
|||
|
||||
DeveloperKeyScopesList.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
listDeveloperKeyScopesSet: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
|
||||
availableScopes: PropTypes.objectOf(
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
|
|
|
@ -19,7 +19,7 @@ import PropTypes from 'prop-types'
|
|||
import React from 'react'
|
||||
import Pill from '@instructure/ui-core/lib/components/Pill'
|
||||
|
||||
export default class DeveloperKeyScopesList extends React.Component {
|
||||
export default class ScopesMethod extends React.Component {
|
||||
methodColorMap() {
|
||||
return({
|
||||
get: 'primary',
|
||||
|
@ -48,11 +48,11 @@ export default class DeveloperKeyScopesList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
DeveloperKeyScopesList.propTypes = {
|
||||
ScopesMethod.propTypes = {
|
||||
method: PropTypes.string.isRequired,
|
||||
margin: PropTypes.string
|
||||
}
|
||||
|
||||
DeveloperKeyScopesList.defaultProps = {
|
||||
ScopesMethod.defaultProps = {
|
||||
margin: undefined
|
||||
}
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
|
||||
import React from 'react'
|
||||
import { mount } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import DeveloperKeyScopes from '../Scopes'
|
||||
|
||||
function props(pending = false) {
|
||||
function props(pending = false, requireScopes = true, onRequireScopesChange = () => {}) {
|
||||
return({
|
||||
developerKey: {},
|
||||
availableScopes: {
|
||||
|
@ -47,7 +48,10 @@ function props(pending = false) {
|
|||
]
|
||||
},
|
||||
availableScopesPending: pending,
|
||||
store: { dispatch: () => {} }
|
||||
dispatch: () => {},
|
||||
listDeveloperKeyScopesSet: () => {},
|
||||
requireScopes,
|
||||
onRequireScopesChange
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,3 +78,28 @@ it('handles filter input change by setting the filter state', () => {
|
|||
wrapper.instance().handleFilterChange(eventDup)
|
||||
expect(wrapper.state().filter).toBe('banana')
|
||||
})
|
||||
|
||||
it('renders Billboard if requireScopes is false', () => {
|
||||
const wrapper = mount(<DeveloperKeyScopes {...props(undefined, false)} />)
|
||||
wrapper.find('Billboard')
|
||||
expect(wrapper.find('Billboard')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('does not render Billboard if requireScopes is true', () => {
|
||||
const wrapper = mount(<DeveloperKeyScopes {...props(undefined, true)} />)
|
||||
wrapper.find('Billboard')
|
||||
expect(wrapper.find('Billboard')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('renders DeveloperKeyScopesList if requireScopes is true', () => {
|
||||
const wrapper = mount(<DeveloperKeyScopes {...props(undefined, true)} />)
|
||||
wrapper.find('DeveloperKeyScopesList')
|
||||
expect(wrapper.find('DeveloperKeyScopesList')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('controls requireScopes change when clicking requireScopes button', () => {
|
||||
const requireScopesStub = sinon.stub()
|
||||
const wrapper = mount(<DeveloperKeyScopes {...props(undefined, true, requireScopesStub)} />)
|
||||
wrapper.find('Checkbox').filterWhere(n => n.prop('variant') === 'toggle').props().onChange()
|
||||
expect(requireScopesStub.called).toBe(true)
|
||||
})
|
||||
|
|
|
@ -115,6 +115,8 @@ const scopes = {
|
|||
}
|
||||
|
||||
const props = {
|
||||
dispatch: () => {},
|
||||
listDeveloperKeyScopesSet: () => {},
|
||||
availableScopes: {
|
||||
"oauth":[
|
||||
{
|
||||
|
@ -139,8 +141,7 @@ const props = {
|
|||
]
|
||||
},
|
||||
filter: '',
|
||||
actions: {listDeveloperKeyScopesSet: () => {}},
|
||||
dispatch: () => {}
|
||||
actions: {listDeveloperKeyScopesSet: () => {}}
|
||||
}
|
||||
|
||||
it("renders each group", () => {
|
||||
|
@ -174,7 +175,7 @@ it("only renders 10 groups on the initaial render", () => {
|
|||
expect(wrapper.instance().state.availableScopes).toHaveLength(10)
|
||||
})
|
||||
|
||||
describe("handlerReadOnlySelected", () => {
|
||||
describe("handleReadOnlySelected", () => {
|
||||
it("selects all scopes with GET as the verb", () => {
|
||||
const wrapper = mount(<DeveloperKeyScopesList {...props} />)
|
||||
const fakeEvent = {
|
||||
|
@ -285,4 +286,4 @@ describe("setSelectedScopes", () => {
|
|||
expect(state.readOnlySelected).toEqual(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -27,7 +27,13 @@ import makeVisibleDeveloperKeyReducer from '../reducers/makeVisibleReducer'
|
|||
import makeInvisibleDeveloperKeyReducer from '../reducers/makeInvisibleReducer'
|
||||
import listDeveloperKeyScopesReducer from '../reducers/listScopesReducer'
|
||||
|
||||
const createStoreWithMiddleware = applyMiddleware(ReduxThunk)(createStore)
|
||||
const middleware = [
|
||||
ReduxThunk,
|
||||
|
||||
// this is so redux-logger is not included in the production webpack bundle
|
||||
(process.env.NODE_ENV !== 'production') && require('redux-logger')() // eslint-disable-line global-require
|
||||
].filter(Boolean)
|
||||
const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore)
|
||||
|
||||
const developerKeysReducer = combineReducers({
|
||||
listDeveloperKeys: listDeveloperKeysReducer,
|
||||
|
|
|
@ -39,7 +39,6 @@ class DeveloperKey < ActiveRecord::Base
|
|||
before_save :protect_default_key
|
||||
after_save :clear_cache
|
||||
after_create :create_default_account_binding
|
||||
before_validation :set_require_scopes
|
||||
before_validation :validate_scopes!
|
||||
|
||||
validates_as_url :redirect_uri, allowed_schemes: nil
|
||||
|
@ -236,11 +235,6 @@ class DeveloperKey < ActiveRecord::Base
|
|||
owner_account.developer_key_account_bindings.create!(developer_key: self)
|
||||
end
|
||||
|
||||
def set_require_scopes
|
||||
return unless api_token_scoping_on?
|
||||
self.require_scopes = self.scopes.present?
|
||||
end
|
||||
|
||||
def validate_scopes!
|
||||
return true unless api_token_scoping_on?
|
||||
return true if self.scopes.empty?
|
||||
|
|
|
@ -20,7 +20,7 @@ module Api::V1::DeveloperKey
|
|||
include Api::V1::Json
|
||||
|
||||
DEVELOPER_KEY_JSON_ATTRS = %w(
|
||||
name created_at email user_id user_name icon_url notes workflow_state scopes
|
||||
name created_at email user_id user_name icon_url notes workflow_state scopes require_scopes
|
||||
).freeze
|
||||
INHERITED_DEVELOPER_KEY_JSON_ATTRS = %w[name created_at icon_url workflow_state].freeze
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@instructure/ui-buttons": "^5.9.0",
|
||||
"@instructure/ui-core": "^4.8.0",
|
||||
"@instructure/ui-icons": "^5.9.0",
|
||||
"@instructure/ui-layout": "^5.9.0",
|
||||
"@instructure/ui-menu": "^5.9.0",
|
||||
"@instructure/ui-overlays": "^5.9.0",
|
||||
"@instructure/ui-themeable": "^5.9.0",
|
||||
|
@ -158,10 +159,10 @@
|
|||
"style-loader": "^0.20",
|
||||
"stylelint": "^9",
|
||||
"through2": "^2",
|
||||
"uglifyjs-webpack-plugin": "^1.2",
|
||||
"webpack": "^3",
|
||||
"webpack-cleanup-plugin": "^0.5",
|
||||
"webpack-manifest-plugin": "^1",
|
||||
"uglifyjs-webpack-plugin": "^1.2",
|
||||
"xsslint": "0.1.4",
|
||||
"yaml-loader": "^0.5"
|
||||
},
|
||||
|
|
|
@ -106,6 +106,6 @@ test('populates the key notes', () => {
|
|||
})
|
||||
|
||||
test('renders the scopes component', () => {
|
||||
const [, , , , , input] = formFieldInputs(developerKey)
|
||||
const [, , , , , , input] = formFieldInputs(developerKey)
|
||||
equal(input.placeholder, "Search endpoints")
|
||||
})
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react'
|
||||
import {mount, shallow} from 'enzyme'
|
||||
import DeveloperKeyModal from 'jsx/developer_keys/NewKeyModal'
|
||||
import $ from 'jquery'
|
||||
import $ from '../../../../app/coffeescripts/jquery.rails_flash_notifications'
|
||||
|
||||
QUnit.module('NewKeyModal', {
|
||||
teardown() {
|
||||
|
@ -100,7 +100,7 @@ test('it opens the modal if isOpen prop is true', () => {
|
|||
/>
|
||||
)
|
||||
equal(wrapper.find('Modal').prop('open'), true)
|
||||
ok(wrapper.find('Modal Heading [level="h4"]').exists())
|
||||
ok(wrapper.find('Modal Heading [level="h3"]').exists())
|
||||
})
|
||||
|
||||
test('it closes the modal if isOpen prop is false', () => {
|
||||
|
@ -316,6 +316,47 @@ test('it sends the contents of the form saving', () => {
|
|||
const createOrEditSpy = sinon.spy()
|
||||
const dispatchSpy = sinon.spy()
|
||||
|
||||
const fakeActions = {
|
||||
createOrEditDeveloperKey: createOrEditSpy
|
||||
}
|
||||
const fakeStore = {
|
||||
dispatch: dispatchSpy
|
||||
}
|
||||
const developerKey2 = Object.assign({}, developerKey, { require_scopes: true, scopes: ['test'] })
|
||||
const editDeveloperKeyState2 = Object.assign({}, editDeveloperKeyState, { developerKey: developerKey2 })
|
||||
|
||||
const wrapper = mount(
|
||||
<DeveloperKeyModal
|
||||
availableScopes={{}}
|
||||
availableScopesPending={false}
|
||||
closeModal={() => {}}
|
||||
createOrEditDeveloperKeyState={editDeveloperKeyState2}
|
||||
listDeveloperKeyScopesState={listDeveloperKeyScopesState}
|
||||
actions={fakeActions}
|
||||
store={fakeStore}
|
||||
mountNode={modalMountNode}
|
||||
selectedScopes={selectedScopes}
|
||||
/>
|
||||
)
|
||||
|
||||
wrapper.node.submitForm()
|
||||
|
||||
const [[sentFormData]] = createOrEditSpy.args
|
||||
|
||||
equal(sentFormData.get('developer_key[name]'), developerKey.name)
|
||||
equal(sentFormData.get('developer_key[email]'), developerKey.email)
|
||||
equal(sentFormData.get('developer_key[redirect_uri]'), developerKey.redirect_uri)
|
||||
equal(sentFormData.get('developer_key[redirect_uris]'), developerKey.redirect_uris)
|
||||
equal(sentFormData.get('developer_key[vendor_code]'), developerKey.vendor_code)
|
||||
equal(sentFormData.get('developer_key[icon_url]'), developerKey.icon_url)
|
||||
equal(sentFormData.get('developer_key[notes]'), developerKey.notes)
|
||||
equal(sentFormData.get('developer_key[require_scopes]'), 'true')
|
||||
})
|
||||
|
||||
test('sends form content without scopes and require_scopes set to false when not require_scopes', () => {
|
||||
const createOrEditSpy = sinon.spy()
|
||||
const dispatchSpy = sinon.spy()
|
||||
|
||||
const fakeActions = {
|
||||
createOrEditDeveloperKey: createOrEditSpy
|
||||
}
|
||||
|
@ -348,6 +389,7 @@ test('it sends the contents of the form saving', () => {
|
|||
equal(sentFormData.get('developer_key[vendor_code]'), developerKey.vendor_code)
|
||||
equal(sentFormData.get('developer_key[icon_url]'), developerKey.icon_url)
|
||||
equal(sentFormData.get('developer_key[notes]'), developerKey.notes)
|
||||
equal(sentFormData.get('developer_key[require_scopes]'), 'false')
|
||||
})
|
||||
|
||||
test('it adds each selected scope to the form data', () => {
|
||||
|
@ -355,12 +397,14 @@ test('it adds each selected scope to the form data', () => {
|
|||
const dispatchSpy = sinon.spy()
|
||||
const fakeActions = { createOrEditDeveloperKey: createOrEditSpy }
|
||||
const fakeStore = { dispatch: dispatchSpy }
|
||||
const developerKey2 = Object.assign({}, developerKey, { require_scopes: true, scopes: ['test'] })
|
||||
const editDeveloperKeyState2 = Object.assign({}, editDeveloperKeyState, { developerKey: developerKey2 })
|
||||
const wrapper = mount(
|
||||
<DeveloperKeyModal
|
||||
availableScopes={{}}
|
||||
availableScopesPending={false}
|
||||
closeModal={() => {}}
|
||||
createOrEditDeveloperKeyState={editDeveloperKeyState}
|
||||
createOrEditDeveloperKeyState={editDeveloperKeyState2}
|
||||
listDeveloperKeyScopesState={listDeveloperKeyScopesState}
|
||||
actions={fakeActions}
|
||||
store={fakeStore}
|
||||
|
@ -379,12 +423,14 @@ test('flashes an error if no scopes are selected', () => {
|
|||
const dispatchSpy = sinon.spy()
|
||||
const fakeActions = { createOrEditDeveloperKey: createOrEditSpy }
|
||||
const fakeStore = { dispatch: dispatchSpy }
|
||||
const developerKey2 = Object.assign({}, developerKey, { require_scopes: true, scopes: [] })
|
||||
const editDeveloperKeyState2 = Object.assign({}, editDeveloperKeyState, { developerKey: developerKey2 })
|
||||
const wrapper = mount(
|
||||
<DeveloperKeyModal
|
||||
availableScopes={{}}
|
||||
availableScopesPending={false}
|
||||
closeModal={() => {}}
|
||||
createOrEditDeveloperKeyState={editDeveloperKeyState}
|
||||
createOrEditDeveloperKeyState={editDeveloperKeyState2}
|
||||
listDeveloperKeyScopesState={listDeveloperKeyScopesState}
|
||||
actions={fakeActions}
|
||||
store={fakeStore}
|
||||
|
@ -395,4 +441,4 @@ test('flashes an error if no scopes are selected', () => {
|
|||
wrapper.node.submitForm()
|
||||
ok(flashStub.calledWith('At least one scope must be selected.'))
|
||||
flashStub.restore()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -102,14 +102,14 @@ describe DeveloperKey do
|
|||
end.not_to raise_exception
|
||||
end
|
||||
|
||||
it 'sets "require_scopes" to true if scopes are present' do
|
||||
key = DeveloperKey.create!(scopes: valid_scopes)
|
||||
expect(key.require_scopes).to eq true
|
||||
it 'does not set "require_scopes" to true if scopes are present and require_scopes is false' do
|
||||
key = DeveloperKey.create!(scopes: valid_scopes, require_scopes: false)
|
||||
expect(key.require_scopes).to eq false
|
||||
end
|
||||
|
||||
it 'sets "require_scopes" to false if scopes are blank' do
|
||||
key = DeveloperKey.create!
|
||||
expect(key.require_scopes).to eq false
|
||||
it 'does not set "require_scopes" to false if scopes are blank and require_scopes is true' do
|
||||
key = DeveloperKey.create!(require_scopes: true)
|
||||
expect(key.require_scopes).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -64,6 +64,10 @@ describe 'Developer Keys' do
|
|||
fj("#keys tbody tr.key button:has(svg[name='IconEdit'])").click
|
||||
end
|
||||
|
||||
def click_enforce_scopes
|
||||
f("[data-automation='enforce_scopes'] div").click
|
||||
end
|
||||
|
||||
def click_scope_group_checkbox
|
||||
fxpath('//*[@class="scopes-group"]/span[1]/span[2]').click
|
||||
end
|
||||
|
@ -73,7 +77,7 @@ describe 'Developer Keys' do
|
|||
end
|
||||
|
||||
def select_all_readonly_checkbox
|
||||
fxpath("//*[@class='scopes-list']/span/div/span/span/span/span[2]/div")
|
||||
fxpath("//*[@class='scopes-list']/span/div/span[1]/span/span/span[2]/div")
|
||||
end
|
||||
|
||||
def all_endpoints_readonly_checkbox_selected?
|
||||
|
@ -88,6 +92,7 @@ describe 'Developer Keys' do
|
|||
f("input[name='developer_key[email]']").send_keys("admin@example.com")
|
||||
f("textarea[name='developer_key[redirect_uris]']").send_keys("http://example.com")
|
||||
f("input[name='developer_key[icon_url]']").send_keys("/images/delete.png")
|
||||
click_enforce_scopes
|
||||
click_scope_group_checkbox
|
||||
find_button("Save Key").click
|
||||
|
||||
|
@ -109,6 +114,7 @@ describe 'Developer Keys' do
|
|||
replace_content(f("input[name='developer_key[email]']"), "admins@example.com")
|
||||
replace_content(f("textarea[name='developer_key[redirect_uris]']"), "http://b/")
|
||||
replace_content(f("input[name='developer_key[icon_url]']"), "/images/add.png")
|
||||
click_enforce_scopes
|
||||
click_scope_group_checkbox
|
||||
find_button("Save Key").click
|
||||
|
||||
|
@ -131,6 +137,7 @@ describe 'Developer Keys' do
|
|||
replace_content(f("input[name='developer_key[email]']"), "admins@example.com")
|
||||
replace_content(f("input[name='developer_key[redirect_uri]']"), "https://b/")
|
||||
replace_content(f("input[name='developer_key[icon_url]']"), "/images/add.png")
|
||||
click_enforce_scopes
|
||||
click_scope_group_checkbox
|
||||
find_button("Save Key").click
|
||||
|
||||
|
@ -149,6 +156,7 @@ describe 'Developer Keys' do
|
|||
get "/accounts/#{Account.default.id}/developer_keys"
|
||||
click_edit_icon
|
||||
f("input[name='developer_key[icon_url]']").clear
|
||||
click_enforce_scopes
|
||||
click_scope_group_checkbox
|
||||
find_button("Save Key").click
|
||||
|
||||
|
@ -318,11 +326,12 @@ describe 'Developer Keys' do
|
|||
Account.default.enable_feature!(:api_token_scoping)
|
||||
end
|
||||
|
||||
def expand_scope_group_by_filter(scope)
|
||||
def expand_scope_group_by_filter(scope_group)
|
||||
get "/accounts/#{Account.default.id}/developer_keys"
|
||||
find_button("Developer Key").click
|
||||
filter_scopes_by_name(scope)
|
||||
fj(".toggle-scope-group span:contains('#{scope}')").click
|
||||
click_enforce_scopes
|
||||
filter_scopes_by_name(scope_group)
|
||||
fj(".toggle-scope-group span:contains('#{scope_group}')").click
|
||||
end
|
||||
|
||||
def filter_scopes_by_name(scope)
|
||||
|
@ -380,14 +389,13 @@ describe 'Developer Keys' do
|
|||
end
|
||||
|
||||
it "removes scopes from backend developer key via UI" do
|
||||
skip 'will be fixed in PLAT-3391'
|
||||
expand_scope_group_by_filter('assignment_groups_api')
|
||||
click_scope_group_checkbox
|
||||
find_button("Save Key").click
|
||||
click_edit_icon
|
||||
filter_scopes_by_name 'assignment_groups_api'
|
||||
click_scope_group_checkbox
|
||||
filter_scopes_by_name 'account_domain_lookups'
|
||||
click_scope_group_checkbox
|
||||
dk = DeveloperKey.last
|
||||
find_button("Save Key").click
|
||||
wait_for_ajaximations
|
||||
|
@ -395,8 +403,10 @@ describe 'Developer Keys' do
|
|||
end
|
||||
|
||||
it "keeps all endpoints read only checkbox checked after save" do
|
||||
skip 'will be fixed in PLAT-3391'
|
||||
get "/accounts/#{Account.default.id}/developer_keys"
|
||||
find_button("Developer Key").click
|
||||
click_enforce_scopes
|
||||
select_all_readonly_checkbox.click
|
||||
find_button("Save Key").click
|
||||
click_edit_icon
|
||||
|
|
Loading…
Reference in New Issue