Merge pull request #6105 from influxdata/fix/auto-execution

feat: preventing ddl and dml statements from autosubmiting
dependabot/npm_and_yarn/msgpackr-1.11.2
Vlastimil Hajek 2024-12-04 09:23:12 +01:00 committed by GitHub
commit 126af481a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 169 additions and 59 deletions

View File

@ -3,7 +3,8 @@
### Bug Fixes ### Bug Fixes
1. [#6103](https://github.com/influxdata/chronograf/pull/6103): Set active database for InfluxQL meta queries. 1. [#6103](https://github.com/influxdata/chronograf/pull/6103): Set active database for InfluxQL meta queries.
1. [#6111](https://github.com/influxdata/chronograf/pull/6111): Fix loading Hosts page for large number of hosts. 2. [#6105](https://github.com/influxdata/chronograf/pull/6105): Prevent dangerous InfluxQL statements from auto-execution.
3. [#6111](https://github.com/influxdata/chronograf/pull/6111): Loading Hosts page for large number of hosts.
### Other ### Other

View File

@ -182,7 +182,7 @@
"SwitchCase": 1 "SwitchCase": 1
} }
], ],
"linebreak-style": [2, "unix"], "linebreak-style": 0,
"lines-around-comment": 0, "lines-around-comment": 0,
"max-depth": 0, "max-depth": 0,
"max-len": 0, "max-len": 0,

View File

@ -8,24 +8,32 @@ import ReactCodeMirror from 'src/dashboards/components/ReactCodeMirror'
import TemplateDrawer from 'src/shared/components/TemplateDrawer' import TemplateDrawer from 'src/shared/components/TemplateDrawer'
import QueryStatus from 'src/shared/components/QueryStatus' import QueryStatus from 'src/shared/components/QueryStatus'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {Dropdown, DropdownMode, ComponentStatus} from 'src/reusable_ui' import {
import {Button, ComponentColor, ComponentSize} from 'src/reusable_ui' Button,
ComponentColor,
ComponentSize,
ComponentStatus,
Dropdown,
DropdownMode,
} from 'src/reusable_ui'
// Utils // Utils
import {getDeep} from 'src/utils/wrappers' import {getDeep} from 'src/utils/wrappers'
import {makeCancelable} from 'src/utils/promises' import {makeCancelable} from 'src/utils/promises'
// Constants // Constants
import {MATCH_INCOMPLETE_TEMPLATES, applyMasks} from 'src/tempVars/constants' import {applyMasks, MATCH_INCOMPLETE_TEMPLATES} from 'src/tempVars/constants'
import {METAQUERY_TEMPLATE_OPTIONS} from 'src/data_explorer/constants' import {
DropdownChildTypes,
METAQUERY_TEMPLATE_OPTIONS,
MetaQueryTemplateOption,
} from 'src/data_explorer/constants'
// Types // Types
import {Template, QueryConfig} from 'src/types' import {QueryConfig, Template} from 'src/types'
import {WrappedCancelablePromise} from 'src/types/promises' import {WrappedCancelablePromise} from 'src/types/promises'
import { import {isExcludedStatement} from 'src/utils/queryFilter'
MetaQueryTemplateOption, import {ErrorSkipped} from 'src/types/queries'
DropdownChildTypes,
} from 'src/data_explorer/constants'
interface TempVar { interface TempVar {
tempVar: string tempVar: string
@ -41,11 +49,12 @@ interface State {
filteredTemplates: Template[] filteredTemplates: Template[]
isSubmitted: boolean isSubmitted: boolean
configID: string configID: string
isExcluded: boolean
} }
interface Props { interface Props {
query: string query: string
onUpdate: (text: string) => Promise<void> onUpdate: (text: string, isAutoSubmitted: boolean) => Promise<void>
config: QueryConfig config: QueryConfig
templates: Template[] templates: Template[]
onMetaQuerySelected: () => void onMetaQuerySelected: () => void
@ -64,21 +73,31 @@ const TEMPLATE_VAR = /[:]\w+[:]/g
class InfluxQLEditor extends Component<Props, State> { class InfluxQLEditor extends Component<Props, State> {
public static getDerivedStateFromProps(nextProps: Props, prevState: State) { public static getDerivedStateFromProps(nextProps: Props, prevState: State) {
const {isSubmitted, editedQueryText} = prevState const {isSubmitted, editedQueryText} = prevState
const {query, config, templates} = nextProps
const isQueryConfigChanged = nextProps.config.id !== prevState.configID const isQueryConfigChanged = config.id !== prevState.configID
const isQueryTextChanged = editedQueryText.trim() !== nextProps.query.trim() const isQueryTextChanged = editedQueryText.trim() !== query.trim()
// if query has been switched, set submitted state for excluded query based on the previous submitted way
if ((isSubmitted && isQueryTextChanged) || isQueryConfigChanged) { let submitted: boolean
if (isQueryConfigChanged) {
submitted = isExcludedStatement(query)
? config.status?.error !== ErrorSkipped
: true
} else {
submitted = isSubmitted
}
if ((submitted && isQueryTextChanged) || isQueryConfigChanged) {
return { return {
...BLURRED_EDITOR_STATE, ...BLURRED_EDITOR_STATE,
selectedTemplate: { selectedTemplate: {
tempVar: getDeep<string>(nextProps.templates, FIRST_TEMP_VAR, ''), tempVar: getDeep<string>(templates, FIRST_TEMP_VAR, ''),
}, },
filteredTemplates: nextProps.templates, filteredTemplates: templates,
templatingQueryText: nextProps.query, templatingQueryText: query,
editedQueryText: nextProps.query, editedQueryText: query,
configID: nextProps.config.id, configID: config.id,
focused: isQueryConfigChanged, focused: isQueryConfigChanged,
isSubmitted: submitted,
isExcluded: isExcludedStatement(query),
} }
} }
@ -102,8 +121,9 @@ class InfluxQLEditor extends Component<Props, State> {
filteredTemplates: props.templates, filteredTemplates: props.templates,
templatingQueryText: props.query, templatingQueryText: props.query,
editedQueryText: props.query, editedQueryText: props.query,
isSubmitted: true,
configID: props.config.id, configID: props.config.id,
isSubmitted: true,
isExcluded: isExcludedStatement(props.query),
} }
} }
@ -126,6 +146,7 @@ class InfluxQLEditor extends Component<Props, State> {
isShowingTemplateValues, isShowingTemplateValues,
focused, focused,
isSubmitted, isSubmitted,
isExcluded,
} = this.state } = this.state
return ( return (
@ -159,9 +180,17 @@ class InfluxQLEditor extends Component<Props, State> {
<div className="varmoji-container"> <div className="varmoji-container">
<div className="varmoji-front"> <div className="varmoji-front">
<QueryStatus <QueryStatus
status={config.status} status={
config.status?.error === ErrorSkipped
? config.status.submittedStatus
: config.status
}
isShowingTemplateValues={isShowingTemplateValues} isShowingTemplateValues={isShowingTemplateValues}
isSubmitted={isSubmitted} isSubmitted={
(isSubmitted && !isExcluded) ||
(isExcluded &&
templatingQueryText === config.status?.submittedQuery)
}
> >
{this.queryStatusButtons} {this.queryStatusButtons}
</QueryStatus> </QueryStatus>
@ -200,7 +229,7 @@ class InfluxQLEditor extends Component<Props, State> {
private handleBlurEditor = (): void => { private handleBlurEditor = (): void => {
this.setState({focused: false, isShowingTemplateValues: false}) this.setState({focused: false, isShowingTemplateValues: false})
this.handleUpdate() this.handleUpdate(true)
} }
private handleCloseDrawer = (): void => { private handleCloseDrawer = (): void => {
@ -245,7 +274,7 @@ class InfluxQLEditor extends Component<Props, State> {
const isTemplating = matched && !_.isEmpty(templates) const isTemplating = matched && !_.isEmpty(templates)
if (isTemplating) { if (isTemplating) {
// maintain cursor poition // maintain cursor position
const matchedVar = {tempVar: `${matched[0]}:`} const matchedVar = {tempVar: `${matched[0]}:`}
const filteredTemplates = this.filterTemplates(matched[0]) const filteredTemplates = this.filterTemplates(matched[0])
const selectedTemplate = this.selectMatchingTemplate( const selectedTemplate = this.selectMatchingTemplate(
@ -262,8 +291,10 @@ class InfluxQLEditor extends Component<Props, State> {
isSubmitted, isSubmitted,
}) })
} else { } else {
const isExcluded = isExcludedStatement(value)
this.setState({ this.setState({
isTemplating, isTemplating,
isExcluded,
templatingQueryText: value, templatingQueryText: value,
editedQueryText: value, editedQueryText: value,
isSubmitted, isSubmitted,
@ -271,22 +302,26 @@ class InfluxQLEditor extends Component<Props, State> {
} }
} }
private handleUpdate = async (): Promise<void> => { private handleUpdate = async (isAutoSubmitted?: boolean): Promise<void> => {
const {onUpdate} = this.props const {onUpdate, config} = this.props
const {editedQueryText, isSubmitted, isExcluded} = this.state
if (!this.isDisabled && !this.state.isSubmitted) { if (
const {editedQueryText} = this.state !this.isDisabled &&
(!isSubmitted || (isExcluded && config.status?.error === ErrorSkipped))
) {
this.cancelPendingUpdates() this.cancelPendingUpdates()
const update = onUpdate(editedQueryText) const update = onUpdate(editedQueryText, isAutoSubmitted)
const cancelableUpdate = makeCancelable(update) const cancelableUpdate = makeCancelable(update)
this.pendingUpdates = [...this.pendingUpdates, cancelableUpdate] this.pendingUpdates = [...this.pendingUpdates, cancelableUpdate]
try { try {
await cancelableUpdate.promise await cancelableUpdate.promise
// prevent changing submitted status when edited while awaiting update // prevent changing submitted status when edited while awaiting update
if (this.state.editedQueryText === editedQueryText) { if (
this.state.editedQueryText === editedQueryText &&
(!isExcluded || !isAutoSubmitted)
) {
this.setState({isSubmitted: true}) this.setState({isSubmitted: true})
} }
} catch (error) { } catch (error) {
@ -443,7 +478,7 @@ class InfluxQLEditor extends Component<Props, State> {
size={ComponentSize.ExtraSmall} size={ComponentSize.ExtraSmall}
color={ComponentColor.Primary} color={ComponentColor.Primary}
status={this.isDisabled && ComponentStatus.Disabled} status={this.isDisabled && ComponentStatus.Disabled}
onClick={this.handleUpdate} onClick={() => this.handleUpdate()}
text="Submit Query" text="Submit Query"
/> />
</div> </div>

View File

@ -189,6 +189,7 @@ export const getConfig = async (
// return back the raw query // return back the raw query
queryConfig.rawText = query queryConfig.rawText = query
} }
return { return {
...queryConfig, ...queryConfig,
originalQuery: query, originalQuery: query,

View File

@ -4,17 +4,9 @@ import {TEMP_VAR_INTERVAL, DEFAULT_DURATION_MS} from 'src/shared/constants'
import replaceTemplates, {replaceInterval} from 'src/tempVars/utils/replace' import replaceTemplates, {replaceInterval} from 'src/tempVars/utils/replace'
import {proxy} from 'src/utils/queryUrlGenerator' import {proxy} from 'src/utils/queryUrlGenerator'
import {Source, Template} from 'src/types' import {Query, Source, Template} from 'src/types'
import {TimeSeriesResponse} from 'src/types/series' import {TimeSeriesResponse} from 'src/types/series'
import {ErrorSkipped} from 'src/types/queries'
// REVIEW: why is this different than the `Query` in src/types?
interface Query {
text: string
id: string
database?: string
db?: string
rp?: string
}
interface QueryResult { interface QueryResult {
value: TimeSeriesResponse | null value: TimeSeriesResponse | null
@ -33,6 +25,18 @@ export function executeQueries(
let counter = queries.length let counter = queries.length
for (let i = 0; i < queries.length; i++) { for (let i = 0; i < queries.length; i++) {
const q = queries[i]
if (
q.queryConfig.isExcluded &&
!q.queryConfig.status.isManuallySubmitted
) {
results[i] = {value: null, error: ErrorSkipped}
counter -= 1
if (counter === 0) {
resolve(results)
}
continue
}
executeQuery(source, queries[i], templates, uuid) executeQuery(source, queries[i], templates, uuid)
.then(result => (results[i] = {value: result, error: null})) .then(result => (results[i] = {value: result, error: null}))
.catch(result => (results[i] = {value: null, error: result})) .catch(result => (results[i] = {value: null, error: result}))
@ -58,9 +62,9 @@ export const executeQuery = async (
const {data} = await proxy({ const {data} = await proxy({
source: source.links.proxy, source: source.links.proxy,
rp: query.rp, rp: query.queryConfig.retentionPolicy,
query: text, query: text,
db: query.db || query.database, db: query.queryConfig.database,
uuid, uuid,
}) })

View File

@ -50,7 +50,7 @@ interface PassedProps {
templates: Template[] templates: Template[]
onAddQuery: () => void onAddQuery: () => void
onDeleteQuery: (index: number) => void onDeleteQuery: (index: number) => void
onEditRawText: (text: string) => Promise<void> onEditRawText: (text: string, isAutoSubmitted: boolean) => Promise<void>
onMetaQuerySelected: () => void onMetaQuerySelected: () => void
} }

View File

@ -54,6 +54,7 @@ import {
import {SourceOption} from 'src/types/sources' import {SourceOption} from 'src/types/sources'
import {Links, ScriptStatus} from 'src/types/flux' import {Links, ScriptStatus} from 'src/types/flux'
import queryBuilderFetcher from './fluxQueryBuilder/apis/queryBuilderFetcher' import queryBuilderFetcher from './fluxQueryBuilder/apis/queryBuilderFetcher'
import {isExcludedStatement} from 'src/utils/queryFilter'
interface ConnectedProps { interface ConnectedProps {
script: string script: string
@ -420,10 +421,20 @@ class TimeMachine extends PureComponent<Props, State> {
return getDeep(queryDrafts, '0.source', '') === '' return getDeep(queryDrafts, '0.source', '') === ''
} }
private handleEditRawText = async (text: string): Promise<void> => { private handleEditRawText = async (
const {templates, onUpdateQueryDrafts, queryDrafts, notify} = this.props text: string,
isAutoSubmitted: boolean
): Promise<void> => {
const {
templates,
onUpdateQueryDrafts,
queryDrafts,
queryStatuses,
notify,
} = this.props
const activeID = this.activeQuery.id const activeID = this.activeQuery.id
const url: string = _.get(this.source, 'links.queries', '') const url: string = _.get(this.source, 'links.queries', '')
const isExcluded = isExcludedStatement(text)
let newQueryConfig let newQueryConfig
@ -446,11 +457,16 @@ class TimeMachine extends PureComponent<Props, State> {
queryConfig: { queryConfig: {
...newQueryConfig, ...newQueryConfig,
rawText: text, rawText: text,
status: {loading: true}, isExcluded,
}, },
} }
}) })
this.handleEditQueryStatus(activeID, {
...queryStatuses[activeID],
isManuallySubmitted: !isAutoSubmitted,
submittedStatus: queryStatuses[activeID]?.submittedStatus,
submittedQuery: queryStatuses[activeID]?.submittedQuery,
})
onUpdateQueryDrafts(updatedQueryDrafts) onUpdateQueryDrafts(updatedQueryDrafts)
} }

View File

@ -128,6 +128,20 @@ class TimeSeries extends PureComponent<Props, State> {
const currQueries = _.map(this.props.queries, q => q.text) const currQueries = _.map(this.props.queries, q => q.text)
const queriesDifferent = !_.isEqual(prevQueries, currQueries) const queriesDifferent = !_.isEqual(prevQueries, currQueries)
let manualSubmit = false
if (!queriesDifferent) {
for (let i = 0; i < this.props.queries.length; i++) {
const query = this.props.queries[i]
const prevQuery = prevProps.queries[i]
if (
query.queryConfig?.status?.isManuallySubmitted &&
!prevQuery.queryConfig?.status?.isManuallySubmitted
) {
manualSubmit = true
break
}
}
}
const prevTemplates = _.get(prevProps, 'templates') const prevTemplates = _.get(prevProps, 'templates')
const newTemplates = _.get(this.props, 'templates') const newTemplates = _.get(this.props, 'templates')
// templates includes dashTime and upperDashTime which capture zoomedTimeRange // templates includes dashTime and upperDashTime which capture zoomedTimeRange
@ -140,6 +154,7 @@ class TimeSeries extends PureComponent<Props, State> {
const timeRangeChanged = oldLower !== newLower || oldUpper !== newUpper const timeRangeChanged = oldLower !== newLower || oldUpper !== newUpper
const shouldExecuteQueries = const shouldExecuteQueries =
manualSubmit ||
queriesDifferent || queriesDifferent ||
timeRangeChanged || timeRangeChanged ||
templatesDifferent || templatesDifferent ||
@ -308,7 +323,13 @@ class TimeSeries extends PureComponent<Props, State> {
const {source, templates, editQueryStatus, queries} = this.props const {source, templates, editQueryStatus, queries} = this.props
for (const query of queries) { for (const query of queries) {
editQueryStatus(query.id, {loading: true}) const prevStatus = query.queryConfig.status
editQueryStatus(query.id, {
loading: true,
isManuallySubmitted: prevStatus?.isManuallySubmitted,
submittedStatus: prevStatus?.submittedStatus,
submittedQuery: prevStatus?.submittedQuery,
})
} }
const results = await this.executeInfluxQLQueries( const results = await this.executeInfluxQLQueries(
@ -335,8 +356,18 @@ class TimeSeries extends PureComponent<Props, State> {
queryStatus = {success: 'Success!'} queryStatus = {success: 'Success!'}
} }
} }
const shouldPreserve =
editQueryStatus(query.id, queryStatus) query.queryConfig.isExcluded &&
!query.queryConfig.status?.isManuallySubmitted
editQueryStatus(query.id, {
...queryStatus,
submittedStatus: shouldPreserve
? query.queryConfig.status.submittedStatus
: queryStatus,
submittedQuery: shouldPreserve
? query.queryConfig.status.submittedQuery
: query.text,
})
} }
const validQueryResults = results const validQueryResults = results

View File

@ -27,6 +27,7 @@ import {
setLocalStorage, setLocalStorage,
TMLocalStorageKey, TMLocalStorageKey,
} from 'src/shared/utils/timeMachine' } from 'src/shared/utils/timeMachine'
import {isExcludedStatement} from 'src/utils/queryFilter'
// Constants // Constants
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants' import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
@ -147,7 +148,7 @@ export class TimeMachineContainer {
state = {...state, queryDrafts} state = {...state, queryDrafts}
} }
// prevents "DROP" or "DELETE" queries from being persisted. // prevents DDL and DML statements from being persisted.
const savable = getDeep<CellQuery[]>(state, 'queryDrafts', []).filter( const savable = getDeep<CellQuery[]>(state, 'queryDrafts', []).filter(
({query, type}) => { ({query, type}) => {
if (type !== 'influxql') { if (type !== 'influxql') {
@ -161,8 +162,8 @@ export class TimeMachineContainer {
const queries = query.split(';') const queries = query.split(';')
let isSavable = true let isSavable = true
for (let i = 0; i <= queries.length; i++) { for (let i = 0; i <= queries.length; i++) {
const qs = getDeep<string>(queries, `${i}`, '').toLocaleLowerCase() const qs = getDeep<string>(queries, `${i}`, '')
if (qs.startsWith('drop') || qs.startsWith('delete')) { if (isExcludedStatement(qs)) {
isSavable = false isSavable = false
} }
} }

View File

@ -1,6 +1,8 @@
// Types // Types
import {Source} from 'src/types' import {Source} from 'src/types'
export const ErrorSkipped = 'skipped'
export interface Query { export interface Query {
text: string text: string
id: string id: string
@ -27,6 +29,7 @@ export interface QueryConfig {
upper?: string upper?: string
isQuerySupportedByExplorer?: boolean // doesn't come from server -- is set in CellEditorOverlay isQuerySupportedByExplorer?: boolean // doesn't come from server -- is set in CellEditorOverlay
originalQuery?: string originalQuery?: string
isExcluded?: boolean
} }
export interface QueryStatus { export interface QueryStatus {
@ -86,9 +89,12 @@ export interface Namespace {
export interface Status { export interface Status {
loading?: boolean loading?: boolean
isManuallySubmitted?: boolean
error?: string error?: string
warn?: string warn?: string
success?: string success?: string
submittedStatus?: Status
submittedQuery?: string
} }
export interface TimeRange { export interface TimeRange {

View File

@ -0,0 +1,15 @@
const excludedStatements: string[] = [
'drop',
'delete',
'alter',
'create',
'grant',
'revoke',
'use',
]
export const isExcludedStatement = (query: string): boolean => {
return excludedStatements.some(statement =>
query?.toLowerCase().startsWith(statement)
)
}