console: function permissions UI (#413)

GitOrigin-RevId: ccdfab19751b0d238a4ebcec59ba73a798103ca9
This commit is contained in:
Sameer Kolhar 2021-02-12 22:31:41 +05:30 committed by hasura-bot
parent 8a68cc6650
commit 8655e6fd3a
20 changed files with 847 additions and 233 deletions

View File

@ -139,6 +139,7 @@ have select permissions to the target table of the function.
- console: show only compatible postgres functions in computed fields section (close #5155) (#5978)
- console: added export data option on browse rows page (close #1438 #5158)
- console: add session argument field for computed fields (close #5154) (#5610)
- console: add support for function permissions (#413)
- cli: add missing global flags for seed command (#5565)
- cli: allow seeds as alias for seed command (#5693)
- cli: fix action timeouts not being picked up in metadata operations (#6220)

View File

@ -80,7 +80,12 @@ interface APIPayload {
[key: string]: any;
}
export type QueryEndpoint = 'query' | 'metadata';
export const queryEndpoints = {
query: 'query',
metadata: 'metadata',
} as const;
export type QueryEndpoint = keyof typeof queryEndpoints;
export const makeDataAPIOptions = (
dataApiUrl: string,
@ -211,18 +216,26 @@ export const trackCreateFunctionTable = () => {
};
};
export const createSampleTable = () => {
return {
type: 'run_sql',
source: 'default',
args: {
sql: `CREATE TABLE text_result(
result text
);`,
cascade: false,
},
};
};
export const createSampleTable = () => ({
type: 'run_sql',
source: 'default',
args: {
sql: 'CREATE TABLE text_result(result text);',
cascade: false,
},
});
export const dropTableIfExists = (
table: { name: string; schema: string },
source = 'default'
) => ({
type: 'run_sql',
source,
args: {
sql: `DROP TABLE IF EXISTS "${table.schema}"."${table.name}";`,
cascade: false,
},
});
export const getTrackSampleTableQuery = () => {
return {

View File

@ -85,7 +85,7 @@ export const testSessVariable = () => {
cy.wait(3000);
trackFunctionRequest(getTrackFnPayload(fN), ResultType.SUCCESS);
cy.wait(1500);
cy.wait(5000);
cy.visit(`data/default/schema/public/functions/${fN}/modify`);
cy.get(getElementFromAlias(`${fN}-session-argument-btn`), {
@ -148,15 +148,17 @@ export const deleteCustomFunction = () => {
export const trackVolatileFunction = () => {
const fN = 'customVolatileFunc'.toLowerCase();
dataRequest(dropTableIfExists({ name: 'text_result', schema: 'public'}), ResultType.SUCCESS);
cy.wait(5000);
dataRequest(createSampleTable(), ResultType.SUCCESS);
cy.wait(1500);
cy.wait(5000);
dataRequest(getTrackSampleTableQuery(), ResultType.SUCCESS, 'metadata');
dataRequest(createVolatileFunction(fN), ResultType.SUCCESS);
cy.wait(1500);
cy.wait(5000);
cy.visit(`data/default/schema/public`);
cy.get(getElementFromAlias(`add-track-function-${fN}`)).click();
cy.get(getElementFromAlias('track-as-mutation')).click();
cy.wait(500);
cy.wait(2000);
cy.url().should(
'eq',
`${baseUrl}/data/default/schema/public/functions/${fN}/modify`
@ -166,17 +168,19 @@ export const trackVolatileFunction = () => {
export const trackVolatileFunctionAsQuery = () => {
const fN = 'customVolatileFunc'.toLowerCase();
dataRequest(dropTableIfExists({ name: 'text_result', schema: 'public'}), ResultType.SUCCESS);
cy.wait(5000);
dataRequest(createSampleTable(), ResultType.SUCCESS);
cy.wait(1500);
cy.wait(5000);
dataRequest(getTrackSampleTableQuery(), ResultType.SUCCESS, 'metadata');
dataRequest(createVolatileFunction(fN), ResultType.SUCCESS);
cy.wait(1500);
cy.wait(5000);
cy.visit(`data/default/schema/public`);
cy.get(getElementFromAlias(`add-track-function-${fN}`)).click();
cy.get(getElementFromAlias('track-as-query')).click();
cy.wait(100);
cy.wait(2000);
cy.get(getElementFromAlias('track-as-query-confirm')).click();
cy.wait(500);
cy.wait(2000);
cy.url().should(
'eq',
`${baseUrl}/data/default/schema/public/functions/${fN}/modify`

View File

@ -39,6 +39,7 @@ const TableRow = ({
>
{p.access}
{p.editIcon}
{p.tooltip}
</td>
);
});

View File

@ -17,6 +17,7 @@ export interface MainState {
serverConfig: {
data: {
version: string;
is_function_permissions_inferred: boolean;
is_admin_secret_set: boolean;
is_auth_hook_set: boolean;
is_jwt_set: boolean;
@ -50,6 +51,7 @@ const defaultState: MainState = {
serverConfig: {
data: {
version: '',
is_function_permissions_inferred: true,
is_admin_secret_set: false,
is_auth_hook_set: false,
is_jwt_set: false,

View File

@ -22,7 +22,7 @@ import {
functionWrapperConnector,
permissionsSummaryConnector,
ModifyCustomFunction,
PermissionCustomFunction,
FunctionPermissions,
ConnectedDatabaseManagePage,
} from '.';
@ -60,7 +60,7 @@ const makeDataRouter = (
>
<IndexRedirect to="modify" />
<Route path="modify" component={ModifyCustomFunction} />
<Route path="permissions" component={PermissionCustomFunction} />
<Route path="permissions" component={FunctionPermissions} />
</Route>
<Route
path=":schema/tables/:table"

View File

@ -11,11 +11,10 @@ import { showErrorNotification } from '../Common/Notification';
import {
fetchDataInit,
fetchFunctionInit,
// updateSchemaInfo,
UPDATE_CURRENT_DATA_SOURCE,
UPDATE_CURRENT_SCHEMA,
} from './DataActions';
import { setDriver } from '../../../dataSources/dataSources';
import { setDriver } from '../../../dataSources';
type Params = {
source?: string;

View File

@ -1,164 +0,0 @@
import React from 'react';
import Helmet from 'react-helmet';
import { Link } from 'react-router';
import { push } from 'react-router-redux';
import { connect } from 'react-redux';
import CommonTabLayout from '../../../../Common/Layout/CommonTabLayout/CommonTabLayout';
import tabInfo from '../Modify/tabInfo';
import globals from '../../../../../Globals';
import { fetchCustomFunction } from '../customFunctionReducer';
import {
updateSchemaInfo,
UPDATE_CURRENT_SCHEMA,
fetchFunctionInit,
setTable,
} from '../../DataActions';
import { NotFoundError } from '../../../../Error/PageNotFound';
import {
getSchemaBaseRoute,
getFunctionBaseRoute,
getTablePermissionsRoute,
} from '../../../../Common/utils/routesUtils';
import { pageTitle } from '../Modify/ModifyCustomFunction';
import styles from '../Modify/ModifyCustomFunction.scss';
class Permission extends React.Component {
constructor(props) {
super(props);
this.state = {
funcFetchCompleted: false,
};
this.urlWithSource = `/data/${props.currentSource}`;
this.urlWithSchema = `/data/${props.currentSource}/schema/${props.currentSchema}`;
this.prefixUrl = globals.urlPrefix + this.urlWithSource;
}
componentDidMount() {
const { functionName, schema } = this.props.params;
if (!functionName) {
this.props.dispatch(push(this.prefixUrl));
}
Promise.all([
this.props
.dispatch(
fetchCustomFunction(functionName, schema, this.props.currentSource)
)
.then(() => {
this.setState({ funcFetchCompleted: true });
}),
]);
}
render() {
const {
functionSchema: schema,
functionName,
setOffTable,
setOffTableSchema,
} = this.props.functions;
if (this.state.funcFetchCompleted && !functionName) {
// throw a 404 exception
throw new NotFoundError();
}
const { dispatch, currentSource } = this.props;
const functionBaseUrl = getFunctionBaseRoute(
schema,
currentSource,
functionName
);
const permissionTableUrl = getTablePermissionsRoute(
setOffTableSchema,
currentSource,
setOffTable,
true
);
const breadCrumbs = [
{
title: 'Data',
url: this.urlWithSource,
},
{
title: 'Schema',
url: this.urlWithSchema,
},
{
title: schema,
url: getSchemaBaseRoute(schema),
},
];
const onClickPerm = () => {
if (schema !== setOffTableSchema) {
Promise.all([
dispatch({
type: UPDATE_CURRENT_SCHEMA,
currentSchema: setOffTableSchema,
}),
dispatch(updateSchemaInfo()),
dispatch(fetchFunctionInit()),
dispatch(setTable(setOffTable)),
]);
}
};
if (functionName) {
breadCrumbs.push({
title: functionName,
url: functionBaseUrl,
});
breadCrumbs.push({
title: 'Permission',
url: '',
});
}
return (
<div className={'col-xs-8' + ' ' + styles.modifyWrapper}>
<Helmet
title={`Permission ${pageTitle} - ${functionName} - ${pageTitle}s | Hasura`}
/>
<CommonTabLayout
appPrefix={this.urlWithSource}
currentTab="permissions"
heading={functionName}
tabsInfo={tabInfo}
breadCrumbs={breadCrumbs}
baseUrl={functionBaseUrl}
showLoader={false}
testPrefix={'functions'}
/>
<br />
<p>
Permissions defined for the SETOF table, <b>{setOffTable}</b>, are
applicable to the data returned by this function.
<br />
<br />
See <b>{setOffTable}</b> permissions{' '}
<Link
to={permissionTableUrl}
data-test="custom-function-permission-link"
onClick={onClickPerm}
>
here
</Link>
.
</p>
</div>
);
}
}
const mapStateToProps = state => ({
currentSchema: state.tables.currentSchema,
});
const permissionConnector = connect(mapStateToProps);
const ConnectedPermission = permissionConnector(Permission);
export default ConnectedPermission;

View File

@ -0,0 +1,232 @@
import React, { useState, useEffect } from 'react';
import Helmet from 'react-helmet';
import { connect, ConnectedProps } from 'react-redux';
import { Link, RouteComponentProps } from 'react-router';
import { push } from 'react-router-redux';
import globals from '../../../../../Globals';
import { ReduxState } from '../../../../../types';
import CommonTabLayout from '../../../../Common/Layout/CommonTabLayout/CommonTabLayout';
import { mapDispatchToPropsEmpty } from '../../../../Common/utils/reactUtils';
import {
getFunctionBaseRoute,
getSchemaBaseRoute,
getTablePermissionsRoute,
} from '../../../../Common/utils/routesUtils';
import { NotFoundError } from '../../../../Error/PageNotFound';
import {
fetchFunctionInit,
setTable,
updateSchemaInfo,
UPDATE_CURRENT_SCHEMA,
} from '../../DataActions';
import { fetchCustomFunction } from '../customFunctionReducer';
import tabInfo from '../Modify/tabInfo';
import PermissionsEditor from './PermissionsEditor';
import { getFunctionSelector } from '../../../../../metadata/selector';
import styles from '../Modify/ModifyCustomFunction.scss';
const PermissionServerFlagNote = ({ isEditable = false }) =>
!isEditable ? (
<>
<br />
<p>
Function will be exposed automatically if there are SELECT permissions
for the role. To expose query functions to roles explicitly, set{' '}
<code>HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS=false</code> on the
server (
<a
href="https://hasura.io/docs/1.0/graphql/core/api-reference/schema-metadata-api/custom-functions.html#api-custom-functions"
target="_blank"
rel="noopener noreferrer"
>
Read More
</a>
)
</p>
</>
) : (
<>
<br />
<p>
The function will be exposed to the role only if the SELECT Permission
are enabled for the role.
</p>
</>
);
const checkPermissionEditState = (
functionPermsInferred: boolean,
funcExposedAsMutation: boolean
) => {
if (!functionPermsInferred) {
// case when INFERRED_PERMISSIONS=false on server
return true;
}
return functionPermsInferred && funcExposedAsMutation;
};
interface PermissionsProps extends ReduxProps {}
const Permissions: React.FC<PermissionsProps> = ({
currentDataSource,
currentSchema,
currentFunction,
currentFunctionInfo,
dispatch,
functions,
serverConfig,
}) => {
const isFunctionPermissionsInferred =
serverConfig.is_function_permissions_inferred ?? true;
const isFunctionExposedAsMutation =
currentFunctionInfo(currentFunction, currentSchema)?.configuration
?.exposed_as === 'mutation' ?? false;
const isPermissionsEditable = checkPermissionEditState(
isFunctionPermissionsInferred,
isFunctionExposedAsMutation
);
const [funcFetchCompleted, updateFunctionFetchState] = useState(false);
const urlWithSource = `/data/${currentDataSource}`;
const urlWithSchema = `/data/${currentDataSource}/schema/${currentSchema}`;
const prefixURL = `${globals.urlPrefix}${urlWithSource}`;
useEffect(() => {
if (!currentFunction) {
dispatch(push(prefixURL));
}
dispatch(
fetchCustomFunction(currentFunction, currentSchema, currentDataSource)
).then(() => {
updateFunctionFetchState(true);
});
}, []);
if (funcFetchCompleted && !currentFunction) {
throw new NotFoundError();
}
const {
functionSchema: schema,
functionName,
setOffTable,
setOffTableSchema,
} = functions;
const functionBaseURL = getFunctionBaseRoute(
schema,
currentDataSource,
functionName
);
const permissionTableURL = getTablePermissionsRoute(
setOffTableSchema,
currentDataSource,
setOffTable,
true
);
const breadCrumbs = [
{
title: 'Data',
url: urlWithSchema,
},
{
title: 'Schema',
url: urlWithSchema,
},
{
title: schema,
url: getSchemaBaseRoute(schema),
},
];
const onClickPerm = () => {
if (schema !== setOffTableSchema) {
Promise.all([
dispatch({
type: UPDATE_CURRENT_SCHEMA,
currentSchema: setOffTableSchema,
}),
dispatch(updateSchemaInfo()),
dispatch(fetchFunctionInit()),
dispatch(setTable(setOffTable)),
]);
}
};
if (functionName) {
breadCrumbs.push({
title: functionName,
url: functionBaseURL,
});
breadCrumbs.push({
title: 'Permission',
url: '',
});
}
return (
<div className={`col-xs-8 ${styles.modifyWrapper}`}>
<Helmet title={`Permission Custom Function - ${functionName} | Hasura`} />
<CommonTabLayout
appPrefix={urlWithSource}
currentTab="permissions"
heading={functionName}
tabsInfo={tabInfo}
breadCrumbs={breadCrumbs}
baseUrl={functionBaseURL}
showLoader={false}
testPrefix="functions"
/>
<br />
<p>
Permissions will be inherited from the SELECT permissions of the
referenced table (
<Link
to={permissionTableURL}
data-test="custom-function-permission-link"
onClick={onClickPerm}
>
<b>{setOffTable}</b>
</Link>
) by default.
</p>
<PermissionServerFlagNote isEditable={isPermissionsEditable} />
<br />
<PermissionsEditor
currentFunctionName={currentFunction}
currentSchema={currentSchema}
isPermissionsEditable={isPermissionsEditable}
/>
</div>
);
};
type OwnProps = RouteComponentProps<
{
functionName: string;
schema: string;
},
unknown
>;
const mapStateToProps = (state: ReduxState, ownProps: OwnProps) => {
return {
currentSchema: ownProps.params.schema,
currentFunction: ownProps.params.functionName,
currentFunctionInfo: getFunctionSelector(state),
currentDataSource: state.tables.currentDataSource,
functions: state.functions,
serverConfig: state.main?.serverConfig?.data ?? {},
};
};
const functionsPermissionsConnector = connect(
mapStateToProps,
mapDispatchToPropsEmpty
);
type ReduxProps = ConnectedProps<typeof functionsPermissionsConnector>;
const FunctionPermissions = functionsPermissionsConnector(Permissions);
export default FunctionPermissions;

View File

@ -0,0 +1,56 @@
import React from 'react';
import Button from '../../../../../Common/Button';
import { ButtonProps } from '../../../../../Common/Button/Button';
import styles from '../../../../../Common/Permissions/PermissionStyles.scss';
type PermissionsActionButtonProps = {
onClick: () => void;
color: ButtonProps['color'];
text: string;
};
const PermissionsActionButton: React.FC<PermissionsActionButtonProps> = ({
onClick,
color,
text,
}) => (
<Button color={color} className={styles.add_mar_right} onClick={onClick}>
{text}
</Button>
);
type PermissionEditorProps = {
role: string;
isEditing: boolean;
closeFn: () => void;
saveFn: () => void;
removeFn: () => void;
isPermSet: boolean;
};
const PermissionEditor: React.FC<PermissionEditorProps> = ({
role,
isEditing,
closeFn,
saveFn,
removeFn,
isPermSet,
}) =>
isEditing ? (
<div className={styles.activeEdit}>
<div className={styles.add_mar_bottom}>
This function is {!isPermSet ? 'not' : null} allowed for role:{' '}
<b>{role}</b>
<br />
Click {!isPermSet ? '"Save"' : '"Remove"'} if you wish to{' '}
{!isPermSet ? 'allow' : 'disallow'} it.
</div>
{!isPermSet ? (
<PermissionsActionButton onClick={saveFn} color="yellow" text="Save" />
) : (
<PermissionsActionButton onClick={removeFn} color="red" text="Remove" />
)}
<PermissionsActionButton onClick={closeFn} color="white" text="Cancel" />
</div>
) : null;
export default PermissionEditor;

View File

@ -0,0 +1,304 @@
import React, { useReducer } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import {
getFunctions,
getCurrentTableInformation,
rolesSelector,
} from '../../../../../../metadata/selector';
import { permissionsSymbols } from '../../../../../Common/Permissions/PermissionSymbols';
import PermTableBody from '../../../../../Common/Permissions/TableBody';
import PermTableHeader from '../../../../../Common/Permissions/TableHeader';
import { mapDispatchToPropsEmpty } from '../../../../../Common/utils/reactUtils';
import {
dropFunctionPermission,
setFunctionPermission,
} from '../../customFunctionReducer';
import PermissionEditor from './PermissionEditor';
import { ReduxState } from '../../../../../../types';
import {
FunctionPermission,
SelectPermissionEntry,
} from '../../../../../../metadata/types';
import styles from '../../../../../Common/Permissions/PermissionStyles.scss';
import Tooltip from '../../../../../Common/Tooltip/Tooltip';
const getFunctionPermissions = (
allFunctions: InjectedProps['allFunctions'],
currentFunctionSchema: string,
currentFunctionName: string
) =>
allFunctions.find(
fn =>
fn.function_name === currentFunctionName &&
fn.function_schema === currentFunctionSchema
)?.permissions;
const findFunctionPermissions = (
allPermissions: FunctionPermission[] | undefined | null,
userRole: string
) => {
if (!allPermissions) {
return false;
}
return allPermissions.find(permRole => permRole.role === userRole);
};
const getRoleQueryPermissionSymbol = (
allPermissions: FunctionPermission[] | undefined | null,
permissionRole: string,
selectRoles: SelectPermissionEntry[] | null
) => {
console.log(allPermissions, permissionRole, selectRoles);
if (permissionRole === 'admin') {
return permissionsSymbols.fullAccess;
}
// selectRoles is populated only when the fn is a query otherwise, we pass an empty array
if (selectRoles) {
if (selectRoles.find(sel => sel.role === permissionRole)) {
return permissionsSymbols.fullAccess;
}
return permissionsSymbols.noAccess;
}
const existingPerm = findFunctionPermissions(allPermissions, permissionRole);
if (existingPerm) {
return permissionsSymbols.fullAccess;
}
return permissionsSymbols.noAccess;
};
const initialState = {
isEditing: false,
role: '',
};
type ReducerState = typeof initialState;
type ReducerAction = { type: string; role: string };
const PERM_UPDATE_OPEN_STATE = 'PERM_UPDATE_OPEN_STATE';
const PERM_UPDATE_CLOSE_STATE = 'PERM_UPDATE_CLOSE_STATE';
const functionsPermissionsReducer = (
state: ReducerState,
action: ReducerAction
) => {
switch (action && action.type) {
case PERM_UPDATE_OPEN_STATE:
return {
isEditing: true,
role: action?.role,
};
case PERM_UPDATE_CLOSE_STATE:
return {
...state,
isEditing: false,
};
default:
return state;
}
};
const PermissionsLegend = () => (
<div className={styles.permissionsLegend}>
<span className={styles.permissionsLegendValue}>
{permissionsSymbols.fullAccess} : allowed
</span>
<span className={styles.permissionsLegendValue}>
{permissionsSymbols.noAccess} : not allowed
</span>
</div>
);
const EditIcon = () => (
<span className={styles.editPermsIcon}>
<i className="fa fa-pencil" aria-hidden="true" />
</span>
);
const PermissionsTableBody: React.FC<PermissionTableProps> = ({
allPermissions,
allRoles,
permCloseEdit,
permOpenEdit,
permissionsEditState,
readOnlyMode,
isEditable,
selectRoles,
}) => {
const queryTypes = ['Permission'];
const { isEditing, role: permEditRole } = permissionsEditState;
const getQueryTypes = (role: string) =>
queryTypes.map(queryType => {
const dispatchOpenEdit = (r: string) => () => {
if (r) {
permOpenEdit(r);
}
};
const isCurrEdit = isEditing && permEditRole === role;
let editIcon = null;
let className = '';
let onClick = () => {};
const tooltip =
!isEditable && role !== 'admin' ? (
<Tooltip message="Forbidden from edits since function permissions are inferred" />
) : null;
if (role !== 'admin' && !readOnlyMode && isEditable) {
editIcon = <EditIcon />;
if (isCurrEdit) {
onClick = permCloseEdit;
className += ` ${styles.currEdit}`;
} else {
className += styles.clickableCell;
onClick = dispatchOpenEdit(role);
}
}
return {
permType: queryType,
className,
editIcon,
onClick,
dataTest: `${role}-${queryType}`,
access: getRoleQueryPermissionSymbol(allPermissions, role, selectRoles),
tooltip,
};
});
const roleList = ['admin', ...allRoles];
const rolePermissions = roleList.map(r => ({
roleName: r,
permTypes: getQueryTypes(r),
}));
return (
<PermTableBody
rolePermissions={rolePermissions}
dispatchRoleNameChange={() => {}}
/>
);
};
type PermissionTableProps = {
permCloseEdit: () => void;
permOpenEdit: (role: string) => void;
permissionsEditState: ReducerState;
allRoles: string[];
readOnlyMode: boolean;
allPermissions: FunctionPermission[] | undefined | null;
isEditable: boolean;
selectRoles: SelectPermissionEntry[] | null;
};
const PermissionsTable: React.FC<PermissionTableProps> = props => (
<>
<PermissionsLegend />
<table className={`table table-bordered ${styles.permissionsTable}`}>
<PermTableHeader headings={['Role', 'Permission']} />
<PermissionsTableBody {...props} />
</table>
</>
);
interface PermissionsProps extends InjectedProps {
currentSchema: string;
currentFunctionName: string;
isPermissionsEditable: boolean;
}
const Permissions: React.FC<PermissionsProps> = ({
allFunctions,
dispatch,
currentSchema,
currentFunctionName,
allRoles,
readOnlyMode = false,
getTableSelectPermissions,
isPermissionsEditable,
functions,
}) => {
const [permissionsEditState, permissionsDispatch] = useReducer(
functionsPermissionsReducer,
initialState
);
const { isEditing, role: permEditRole } = permissionsEditState;
const permCloseEdit = () => {
permissionsDispatch({
type: PERM_UPDATE_CLOSE_STATE,
role: '',
});
};
const permOpenEdit = (role: string) => {
permissionsDispatch({
type: PERM_UPDATE_OPEN_STATE,
role,
});
};
const allPermissions = getFunctionPermissions(
allFunctions,
currentSchema,
currentFunctionName
);
const { setOffTable, setOffTableSchema } = functions;
const selectRoles = !isPermissionsEditable
? getTableSelectPermissions(setOffTable, setOffTableSchema)
: null;
const isPermSet =
getRoleQueryPermissionSymbol(allPermissions, permEditRole, selectRoles) ===
permissionsSymbols.fullAccess;
const saveFunc = () => {
dispatch(setFunctionPermission(permEditRole, permCloseEdit));
};
const removeFunc = () => {
dispatch(dropFunctionPermission(permEditRole, permCloseEdit));
};
return (
<>
<PermissionsTable
permCloseEdit={permCloseEdit}
permOpenEdit={permOpenEdit}
permissionsEditState={permissionsEditState}
allRoles={allRoles}
readOnlyMode={readOnlyMode}
allPermissions={allPermissions}
isEditable={isPermissionsEditable}
selectRoles={selectRoles}
/>
<div className={`${styles.add_mar_bottom}`}>
{!readOnlyMode && (
<PermissionEditor
saveFn={saveFunc}
removeFn={removeFunc}
closeFn={permCloseEdit}
role={permEditRole}
isEditing={isEditing}
isPermSet={isPermSet}
/>
)}
</div>
</>
);
};
const mapStateToProps = (state: ReduxState) => ({
allRoles: rolesSelector(state),
allFunctions: getFunctions(state),
functions: state.functions,
readOnlyMode: state.main.readOnlyMode,
getTableSelectPermissions: getCurrentTableInformation(state),
});
const permissionsUIConnector = connect(
mapStateToProps,
mapDispatchToPropsEmpty
);
type InjectedProps = ConnectedProps<typeof permissionsUIConnector>;
export default permissionsUIConnector(Permissions);

View File

@ -1,22 +1,19 @@
/* Import default State */
import { functionData } from './customFunctionState';
import Endpoints, { globalCookiePolicy } from '../../../../Endpoints';
import requestAction from '../../../../utils/requestAction';
import dataHeaders from '../Common/Headers';
import _push from '../push';
import { getSchemaBaseRoute } from '../../../Common/utils/routesUtils';
import { dataSource } from '../../../../dataSources';
import Endpoints, { globalCookiePolicy } from '../../../../Endpoints';
import { exportMetadata } from '../../../../metadata/actions';
import { getRunSqlQuery } from '../../../Common/utils/v1QueryUtils';
import {
getUntrackFunctionQuery,
createFunctionPermissionQuery,
dropFunctionPermissionQuery,
getTrackFunctionQuery,
getUntrackFunctionQuery,
} from '../../../../metadata/queryUtils';
import requestAction from '../../../../utils/requestAction';
import { getSchemaBaseRoute } from '../../../Common/utils/routesUtils';
import { getRunSqlQuery } from '../../../Common/utils/v1QueryUtils';
import { makeRequest } from '../../RemoteSchema/Actions';
import dataHeaders from '../Common/Headers';
import _push from '../push';
import { functionData } from './customFunctionState';
/* Constants */
@ -41,6 +38,15 @@ const SESSVAR_CUSTOM_FUNCTION_ADD_FAIL =
const SESSVAR_CUSTOM_FUNCTION_ADD_SUCCESS =
'@customFunction/SESSVAR_CUSTOM_FUNCTION_ADD_SUCCESS';
const PERMISSION_CUSTOM_FUNCTION_SET_SUCCESS =
'@customFunction/PERMISSION_CUSTOM_FUNCTION_SET_SUCCESS';
const PERMISSION_CUSTOM_FUNCTION_SET_FAIL =
'@customFunction/PERMISSION_CUSTOM_FUNCTION_SET_FAIL';
const PERMISSION_CUSTOM_FUNCTION_DROP_SUCCESS =
'@customFunction/PERMISSION_CUSTOM_FUNCTION_DROP_SUCCESS';
const PERMISSION_CUSTOM_FUNCTION_DROP_FAIL =
'@customFunction/PERMISSION_CUSTOM_FUNCTION_DROP_FAIL';
/* Action creators */
const fetchCustomFunction = (functionName, schema, source) => {
return (dispatch, getState) => {
@ -103,11 +109,12 @@ const deleteFunction = () => (dispatch, getState) => {
const successMsg = 'Function deleted';
const errorMsg = 'Deleting function failed';
const customOnSuccess = () =>
const customOnSuccess = () => {
dispatch({ type: DELETING_CUSTOM_FUNCTION });
dispatch(_push(getSchemaBaseRoute(currentSchema, source)));
};
const customOnError = () => dispatch({ type: DELETE_CUSTOM_FUNCTION_FAIL });
dispatch({ type: DELETING_CUSTOM_FUNCTION });
return dispatch(
makeRequest(
sqlUpQueries,
@ -146,6 +153,7 @@ const unTrackCustomFunction = () => {
const errorMsg = 'Delete custom function failed';
const customOnSuccess = () => {
dispatch({ type: UNTRACKING_CUSTOM_FUNCTION });
dispatch(_push(getSchemaBaseRoute(currentSchema, currentDataSource)));
dispatch({ type: RESET });
dispatch(exportMetadata());
@ -156,7 +164,6 @@ const unTrackCustomFunction = () => {
]);
};
dispatch({ type: UNTRACKING_CUSTOM_FUNCTION });
return dispatch(
makeRequest(
[payload],
@ -230,13 +237,13 @@ const updateSessVar = session_argument => {
const errorMsg = 'Updating Session argument variable failed';
const customOnSuccess = () => {
dispatch({ type: SESSVAR_CUSTOM_FUNCTION_REQUEST });
dispatch(exportMetadata());
};
const customOnError = error => {
dispatch({ type: SESSVAR_CUSTOM_FUNCTION_ADD_FAIL, data: error });
};
dispatch({ type: SESSVAR_CUSTOM_FUNCTION_REQUEST });
return dispatch(
makeRequest(
upQuery.args,
@ -252,16 +259,105 @@ const updateSessVar = session_argument => {
};
};
/* */
const setFunctionPermission = (userRole, onSuccessCb) => (
dispatch,
getState
) => {
const { currentDataSource, currentSchema } = getState().tables;
const { functionName } = getState().functions;
const currentFunction = { schema: currentSchema, name: functionName };
const upQuery = createFunctionPermissionQuery(
currentDataSource,
currentFunction,
userRole
);
const downQuery = dropFunctionPermissionQuery(
currentDataSource,
currentFunction,
userRole
);
const migrationName = `set_permission_role_${userRole}_function_${functionName}`;
const requestMsg = `Setting permisions for ${userRole} role on ${functionName}`;
const successMsg = `Successfully set permissions for ${userRole}`;
const errorMsg = `Failed to set permissions for ${userRole}`;
const customOnSuccess = () => {
dispatch({ type: PERMISSION_CUSTOM_FUNCTION_SET_SUCCESS });
dispatch(exportMetadata(onSuccessCb));
};
const customOnError = err => {
dispatch({ type: PERMISSION_CUSTOM_FUNCTION_SET_FAIL, data: err });
};
return dispatch(
makeRequest(
[upQuery],
[downQuery],
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
)
);
};
const dropFunctionPermission = (userRole, onSuccessCb) => (
dispatch,
getState
) => {
const { currentDataSource, currentSchema } = getState().tables;
const { functionName } = getState().functions;
const currentFunction = { schema: currentSchema, name: functionName };
const upQuery = dropFunctionPermissionQuery(
currentDataSource,
currentFunction,
userRole
);
const downQuery = createFunctionPermissionQuery(
currentDataSource,
currentFunction,
userRole
);
const migrationName = `drop_permission_role_${userRole}_function_${functionName}`;
const requestMsg = `Dropping permisions for ${userRole} role on ${functionName}`;
const successMsg = `Successfully dropped permissions for ${userRole}`;
const errorMsg = `Failed to drop permissions for ${userRole}`;
const customOnSuccess = () => {
dispatch({ type: PERMISSION_CUSTOM_FUNCTION_DROP_SUCCESS });
dispatch(exportMetadata(onSuccessCb));
};
const customOnError = err => {
dispatch({ type: PERMISSION_CUSTOM_FUNCTION_DROP_FAIL, data: err });
};
return dispatch(
makeRequest(
[upQuery],
[downQuery],
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
)
);
};
/* Reducer */
const customFunctionReducer = (state = functionData, action) => {
switch (action.type) {
case RESET:
return {
...functionData,
};
return functionData;
case FETCHING_INDIV_CUSTOM_FUNCTION:
return {
...state,
@ -300,7 +396,6 @@ const customFunctionReducer = (state = functionData, action) => {
isDeleting: true,
isError: null,
};
case UNTRACK_CUSTOM_FUNCTION_FAIL:
return {
...state,
@ -331,20 +426,42 @@ const customFunctionReducer = (state = functionData, action) => {
isUpdating: false,
isError: null,
};
default:
case PERMISSION_CUSTOM_FUNCTION_SET_SUCCESS:
return {
...state,
isPermissionSet: true,
isError: null,
};
case PERMISSION_CUSTOM_FUNCTION_SET_FAIL:
return {
...state,
isPermissionSet: false,
isError: action.data,
};
case PERMISSION_CUSTOM_FUNCTION_DROP_SUCCESS:
return {
...state,
isPermissionDrop: true,
isError: null,
};
case PERMISSION_CUSTOM_FUNCTION_DROP_FAIL:
return {
...state,
isPermissionDrop: false,
isError: action.data,
};
default:
return state;
}
};
/* End of it */
export {
RESET,
deleteFunction,
dropFunctionPermission,
fetchCustomFunction,
RESET,
setFunctionPermission,
unTrackCustomFunction,
updateSessVar,
deleteFunction,
};
export default customFunctionReducer;

View File

@ -6,6 +6,8 @@ const asyncState = {
isFetching: false,
isUpdating: false,
isFetchError: null,
isPermissionSet: false,
isPermissionDrop: false,
};
const functionData = {
@ -13,6 +15,7 @@ const functionData = {
functionSchema: '',
functionDefinition: '',
configuration: {},
permissions: {},
setOffTable: '',
setOffTableSchema: '',
inputArgNames: [],

View File

@ -25,7 +25,7 @@ import dataRouterUtils from './DataRouter';
import dataReducer from './DataReducer';
import functionWrapperConnector from './Function/FunctionWrapper';
import ModifyCustomFunction from './Function/Modify/ModifyCustomFunction';
import PermissionCustomFunction from './Function/Permission/Permission';
import FunctionPermissions from './Function/Permission/Permission';
import ConnectedDatabaseManagePage from './Schema/ManageDatabase';
export {
@ -49,6 +49,6 @@ export {
dataReducer,
functionWrapperConnector,
ModifyCustomFunction,
PermissionCustomFunction,
FunctionPermissions,
ConnectedDatabaseManagePage,
};

View File

@ -6,6 +6,7 @@ import {
CustomTypes,
HasuraMetadataV2,
QualifiedTable,
QualifiedFunction,
} from './types';
import { transformHeaders } from '../components/Common/Headers/utils';
import { LocalEventTriggerState } from '../components/Services/Events/EventTriggers/state';
@ -82,6 +83,8 @@ export const metadataQueryTypes = [
'get_event_invocations',
'get_scheduled_events',
'delete_scheduled_event',
'create_function_permission',
'drop_function_permission',
] as const;
export type MetadataQueryType = typeof metadataQueryTypes[number];
@ -753,3 +756,23 @@ export const invokeManualTriggerQuery = (
args: InvokeManualTriggerArgs,
source: string
) => getMetadataQuery('invoke_event_trigger', source, args);
export const createFunctionPermissionQuery = (
source: string,
func: QualifiedFunction,
role: string
) =>
getMetadataQuery('create_function_permission', source, {
function: func,
role,
});
export const dropFunctionPermissionQuery = (
source: string,
func: QualifiedFunction,
role: string
) =>
getMetadataQuery('drop_function_permission', source, {
function: func,
role,
});

View File

@ -155,7 +155,16 @@ export const getTablesInfoSelector = createSelector(
}
);
const getFunctions = createSelector(
// TODO?: make it generic i.e to fetch any property from all tables
export const getCurrentTableInformation = createSelector(
getTables,
tables => (tableName: string, tableSchema: string) =>
tables?.find(
t => tableName === t.table.name && tableSchema === t.table.schema
)?.select_permissions ?? []
);
export const getFunctions = createSelector(
getDataSourceMetadata,
source =>
source?.functions?.map(f => ({
@ -163,6 +172,7 @@ const getFunctions = createSelector(
function_name: f.function.name,
function_schema: f.function.schema,
configuration: f.configuration,
permissions: f?.permissions,
})) || []
);

View File

@ -152,6 +152,11 @@ export interface FunctionConfiguration {
session_argument?: string;
}
export interface FunctionPermission {
role: string;
definition?: Record<string, any>;
}
// ////////////////////////////
// #endregion CUSTOM FUNCTIONS
// /////////////////////////////
@ -865,8 +870,12 @@ export interface MetadataDataSource {
};
tables: TableEntry[];
functions?: Array<{
function: { schema: string; name: string };
configuration: Record<string, any>;
function: QualifiedFunction;
configuration?: {
exposed_as?: 'mutation' | 'query';
session_argument?: string;
};
permissions?: FunctionPermission[];
}>;
query_collections?: QueryCollectionEntry[];
allowlist?: AllowList[];

View File

@ -46,6 +46,7 @@ Sample response
{
"version": "v1.0.0-beta.3",
"is_function_permissions_inferred": true,
"is_admin_secret_set": true,
"is_auth_hook_set": false,
"is_jwt_set": true,

View File

@ -11,6 +11,7 @@ import Data.Aeson.TH
import qualified Hasura.GraphQL.Execute.LiveQuery.Options as LQ
import Hasura.RQL.Types (FunctionPermissionsCtx)
import Hasura.Server.Auth
import Hasura.Server.Auth.JWT
import Hasura.Server.Version (HasVersion, Version, currentVersion)
@ -27,21 +28,23 @@ $(deriveToJSON hasuraJSON ''JWTInfo)
data ServerConfig
= ServerConfig
{ scfgVersion :: !Version
, scfgIsAdminSecretSet :: !Bool
, scfgIsAuthHookSet :: !Bool
, scfgIsJwtSet :: !Bool
, scfgJwt :: !(Maybe JWTInfo)
, scfgIsAllowListEnabled :: !Bool
, scfgLiveQueries :: !LQ.LiveQueriesOptions
, scfgConsoleAssetsDir :: !(Maybe Text)
{ scfgVersion :: !Version
, scfgIsFunctionPermissionsInferred :: !FunctionPermissionsCtx
, scfgIsAdminSecretSet :: !Bool
, scfgIsAuthHookSet :: !Bool
, scfgIsJwtSet :: !Bool
, scfgJwt :: !(Maybe JWTInfo)
, scfgIsAllowListEnabled :: !Bool
, scfgLiveQueries :: !LQ.LiveQueriesOptions
, scfgConsoleAssetsDir :: !(Maybe Text)
} deriving (Show, Eq)
$(deriveToJSON hasuraJSON ''ServerConfig)
runGetConfig :: HasVersion => AuthMode -> Bool -> LQ.LiveQueriesOptions -> Maybe Text -> ServerConfig
runGetConfig am isAllowListEnabled liveQueryOpts consoleAssetsDir = ServerConfig
runGetConfig :: HasVersion => FunctionPermissionsCtx -> AuthMode -> Bool -> LQ.LiveQueriesOptions -> Maybe Text -> ServerConfig
runGetConfig functionPermsCtx am isAllowListEnabled liveQueryOpts consoleAssetsDir = ServerConfig
currentVersion
functionPermsCtx
(isAdminSecretSet am)
(isAuthHookSet am)
(isJWTSet am)

View File

@ -688,7 +688,7 @@ configApiGetHandler serverCtx@ServerCtx{..} consoleAssetsDir =
Spock.get "v1alpha1/config" $ mkSpockAction serverCtx encodeQErr id $
mkGetHandler $ do
onlyAdmin
let res = runGetConfig scAuthMode scEnableAllowlist
let res = runGetConfig scFunctionPermsCtx scAuthMode scEnableAllowlist
(EL._lqsOptions $ scLQState) consoleAssetsDir
return $ JSONResp $ HttpResponse (encJFromJValue res) []