diff --git a/CHANGELOG.md b/CHANGELOG.md index a74f6d0b56c..7f52d46766f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - console: fix cross-schema array relationship suggestions - console: add performance fixes for handle large db schemas - console: fix missing cross-schema computed fields in permission builder +- console: add time limits setting to security settings - cli: add support for `network` metadata object - cli: `hasura migrate apply --all-databases` will return a non zero exit code if operation failed on atleast one database (#7499) - cli: `migrate create --from-server` creates the migration and marks it as applied on the server diff --git a/console/src/components/Common/Table/Table.tsx b/console/src/components/Common/Table/Table.tsx index a90a43b2f36..990cab46b83 100644 --- a/console/src/components/Common/Table/Table.tsx +++ b/console/src/components/Common/Table/Table.tsx @@ -197,7 +197,7 @@ export function TableRow({ : styles.justify_center } ${singleColumn ? styles.single_column : styles.justify_center}`} onClick={onClick(i)} - key={i} + key={`${index}${i}`} > {renderCol({ data, diff --git a/console/src/components/Services/ApiExplorer/Security/ApiLimits.tsx b/console/src/components/Services/ApiExplorer/Security/ApiLimits.tsx index 998cf22b203..0b05fffb412 100644 --- a/console/src/components/Services/ApiExplorer/Security/ApiLimits.tsx +++ b/console/src/components/Services/ApiExplorer/Security/ApiLimits.tsx @@ -15,23 +15,19 @@ const ApiLimitsComponent: React.FC = ({ allRoles, dispatch, }) => { - // useEffect(() => { - // dispatch(exportMetadata()); - // }, [dispatch]); - const headers = [ 'Role', 'Depth Limit', 'Node Limit', 'Rate Limit (RPM)', - // 'Operation Timeout (Seconds)', + 'Timeout (Seconds)', ]; const keys = [ 'role', 'depth_limit', 'node_limit', 'rate_limit', - // 'operation_timeout', + 'time_limit', ]; const roles = allRoles; return ( diff --git a/console/src/components/Services/ApiExplorer/Security/LimitsFormWrapper.tsx b/console/src/components/Services/ApiExplorer/Security/LimitsFormWrapper.tsx index 6cb9a688f23..aca2c294f5c 100644 --- a/console/src/components/Services/ApiExplorer/Security/LimitsFormWrapper.tsx +++ b/console/src/components/Services/ApiExplorer/Security/LimitsFormWrapper.tsx @@ -28,6 +28,10 @@ export const labels: Record< info: 'Set a request rate limit for this role. You can also combine additional unique parameters for more granularity.', }, + time_limit: { + title: 'Timeout', + info: 'Global timeout for GraphQL operations.', + }, }; interface LimitsFormWrapperProps extends TableFormProps { @@ -47,6 +51,7 @@ const LimitsFormWrapper: React.FC = ({ const api_limits = useSelector((state: ApiLimitsFormSate) => state); const rateLimit = useSelector((state: ApiLimitsFormSate) => state.rate_limit); const nodeLimit = useSelector((state: ApiLimitsFormSate) => state.node_limit); + const timeLimit = useSelector((state: ApiLimitsFormSate) => state.time_limit); const depthLimit = useSelector( (state: ApiLimitsFormSate) => state.depth_limit ); @@ -81,6 +86,7 @@ const LimitsFormWrapper: React.FC = ({ const disabled_for_role = isEmpty(depthLimit?.global) && isEmpty(nodeLimit?.global) && + isEmpty(timeLimit?.global) && isEmpty(rateLimit?.global); return currentRole !== 'global' && disabled_for_role; }; @@ -99,6 +105,8 @@ const LimitsFormWrapper: React.FC = ({ return dispatch(apiLimitActions.updateDepthLimitState(state)); case 'node_limit': return dispatch(apiLimitActions.updateNodeLimitState(state)); + case 'time_limit': + return dispatch(apiLimitActions.updateTimeLimitState(state)); default: return dispatch(apiLimitActions.updateRateLimitState(state)); } @@ -118,6 +126,10 @@ const LimitsFormWrapper: React.FC = ({ return dispatch( apiLimitActions.updateNodeLimitRole({ role, limit: value }) ); + case 'time_limit': + return dispatch( + apiLimitActions.updateTimeLimitRole({ role, limit: value }) + ); default: return dispatch( apiLimitActions.updateMaxReqPerMin({ role, limit: value }) @@ -129,6 +141,8 @@ const LimitsFormWrapper: React.FC = ({ return dispatch(apiLimitActions.updateGlobalDepthLimit(value)); case 'node_limit': return dispatch(apiLimitActions.updateGlobalNodeLimit(value)); + case 'time_limit': + return dispatch(apiLimitActions.updateGlobalTimeLimit(value)); default: return dispatch(apiLimitActions.updateGlobalMaxReqPerMin(value)); } @@ -162,6 +176,7 @@ const LimitsFormWrapper: React.FC = ({ return ( = ({ {roles.map(role => ( { const roleLimit = per_role?.[role]; diff --git a/console/src/components/Services/ApiExplorer/Security/__tests__/__snapshots__/utils.test.ts.snap b/console/src/components/Services/ApiExplorer/Security/__tests__/__snapshots__/utils.test.ts.snap index ad14c84d17b..c15894feec2 100644 --- a/console/src/components/Services/ApiExplorer/Security/__tests__/__snapshots__/utils.test.ts.snap +++ b/console/src/components/Services/ApiExplorer/Security/__tests__/__snapshots__/utils.test.ts.snap @@ -23,6 +23,13 @@ Array [ }, "state": "disabled", }, + Object { + "global": -1, + "per_role": Object { + "user": undefined, + }, + "state": "disabled", + }, ] `; @@ -49,6 +56,13 @@ Array [ }, "state": "disabled", }, + Object { + "global": -1, + "per_role": Object { + "new_role": undefined, + }, + "state": "disabled", + }, ] `; @@ -75,6 +89,13 @@ Array [ }, "state": "disabled", }, + Object { + "global": -1, + "per_role": Object { + "test_role": undefined, + }, + "state": "disabled", + }, ] `; @@ -101,6 +122,13 @@ Array [ }, "state": "disabled", }, + Object { + "global": -1, + "per_role": Object { + "editor": undefined, + }, + "state": "disabled", + }, ] `; @@ -121,5 +149,10 @@ Array [ "per_role": Object {}, "state": "disabled", }, + Object { + "global": -1, + "per_role": Object {}, + "state": "disabled", + }, ] `; diff --git a/console/src/components/Services/ApiExplorer/Security/actions.ts b/console/src/components/Services/ApiExplorer/Security/actions.ts index 92a73c5b7e3..20e495aeb52 100644 --- a/console/src/components/Services/ApiExplorer/Security/actions.ts +++ b/console/src/components/Services/ApiExplorer/Security/actions.ts @@ -14,6 +14,7 @@ export type updateSecurityFeaturesActionType = { disabled: boolean; depth_limit?: APILimitInputType; node_limit?: APILimitInputType; + time_limit?: APILimitInputType; rate_limit?: APILimitInputType<{ unique_params: Nullable<'IP' | string[]>; max_reqs_per_min: number; diff --git a/console/src/components/Services/ApiExplorer/Security/state.ts b/console/src/components/Services/ApiExplorer/Security/state.ts index d437a10b005..0d076ebf0eb 100644 --- a/console/src/components/Services/ApiExplorer/Security/state.ts +++ b/console/src/components/Services/ApiExplorer/Security/state.ts @@ -25,6 +25,11 @@ const initialState = { { unique_params: Nullable<'IP' | string[]>; max_reqs_per_min: number } >, }, + time_limit: { + global: -1, + state: RoleState.disabled, + per_role: {} as Record, + }, }; type LimitPayload = { @@ -51,9 +56,18 @@ const formStateSLice = createSlice({ updateNodeLimitRole(state, action: PayloadAction) { state.node_limit.per_role[action.payload.role] = action.payload.limit; }, + updateTimeLimitRole(state, action: PayloadAction) { + state.time_limit.per_role[action.payload.role] = action.payload.limit; + }, updateNodeLimitState(state, action: PayloadAction) { state.node_limit.state = action.payload; }, + updateTimeLimitState(state, action: PayloadAction) { + state.time_limit.state = action.payload; + }, + updateGlobalTimeLimit(state, action: PayloadAction) { + state.time_limit.global = action.payload; + }, updateUniqueParams( state, action: PayloadAction> @@ -95,6 +109,7 @@ const formStateSLice = createSlice({ state.depth_limit = action.payload.depth_limit; state.node_limit = action.payload.node_limit; state.rate_limit = action.payload.rate_limit; + state.time_limit = action.payload.time_limit; }, setDisable(state, action: PayloadAction) { state.disabled = action.payload; diff --git a/console/src/components/Services/ApiExplorer/Security/utils.tsx b/console/src/components/Services/ApiExplorer/Security/utils.tsx index e8e8b38ec93..0869ab64fe8 100644 --- a/console/src/components/Services/ApiExplorer/Security/utils.tsx +++ b/console/src/components/Services/ApiExplorer/Security/utils.tsx @@ -4,8 +4,9 @@ import { isEmpty } from '../../../Common/utils/jsUtils'; import { ApiLimitsFormSate } from './state'; const getApiLimits = (metadata: HasuraMetadataV3) => { - const { node_limit, depth_limit, rate_limit } = metadata.api_limits ?? {}; - return { depth_limit, node_limit, rate_limit }; + const { node_limit, depth_limit, rate_limit, time_limit } = + metadata.api_limits ?? {}; + return { depth_limit, node_limit, rate_limit, time_limit }; }; export enum RoleState { @@ -44,6 +45,12 @@ const prepareApiLimits = ( state: RoleState.disabled, }; + res.time_limit = { + global: apiLimits?.time_limit?.global ?? -1, + state: RoleState.disabled, + per_role: apiLimits?.time_limit?.per_role ?? {}, + }; + return res; }; diff --git a/console/src/metadata/types.ts b/console/src/metadata/types.ts index 43aa33fbbc3..1eadba33849 100644 --- a/console/src/metadata/types.ts +++ b/console/src/metadata/types.ts @@ -1001,6 +1001,7 @@ export interface HasuraMetadataV3 { disabled?: boolean; depth_limit?: APILimit; node_limit?: APILimit; + time_limit?: APILimit; rate_limit?: APILimit<{ unique_params: Nullable<'IP' | string[]>; max_reqs_per_min: number; diff --git a/console/src/metadata/utils.ts b/console/src/metadata/utils.ts index 1bc4f9087a3..2c12e7f0c11 100644 --- a/console/src/metadata/utils.ts +++ b/console/src/metadata/utils.ts @@ -193,6 +193,7 @@ export const updateAPILimitsQuery = ({ disabled: boolean; depth_limit?: APILimitInputType; node_limit?: APILimitInputType; + time_limit?: APILimitInputType; rate_limit?: APILimitInputType<{ unique_params: Nullable<'IP' | string[]>; max_reqs_per_min: number; @@ -204,7 +205,12 @@ export const updateAPILimitsQuery = ({ disabled: newAPILimits.disabled, }; - const api_limits = ['depth_limit', 'node_limit', 'rate_limit'] as const; + const api_limits = [ + 'depth_limit', + 'node_limit', + 'rate_limit', + 'time_limit', + ] as const; api_limits.forEach(key => { const role = newAPILimits[key]?.per_role @@ -281,7 +287,12 @@ export const removeAPILimitsQuery = ({ }; } - const api_limits = ['depth_limit', 'node_limit', 'rate_limit'] as const; + const api_limits = [ + 'depth_limit', + 'node_limit', + 'rate_limit', + 'time_limit', + ] as const; api_limits.forEach(key => { delete existingAPILimits?.[key]?.per_role?.[role];