UI : Integrating Permissions API to get permissions for LoggedIn user. (#2557)

* UI : Integrating Permissions API to get permissions for LoggedIn user.

* Adding permission check for Lineage

* Adding Permission check for manage settings.

* Adding permission check for entity level tags

* Adding permission check for entities detail page.

* Adding permission check on Tags page

* Adding permission check for teams page

* Addressing review comments
This commit is contained in:
Sachin Chaurasiya 2022-02-02 21:30:02 +05:30 committed by GitHub
parent 60aa5f3939
commit 199ca106e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 181 additions and 74 deletions

View File

@ -12,7 +12,7 @@
*/
import { action, makeAutoObservable } from 'mobx';
import { ClientAuth, NewUser } from 'Models';
import { ClientAuth, NewUser, UserPermissions } from 'Models';
import { CurrentTourPageType } from './enums/tour.enum';
import { Role } from './generated/entity/teams/role';
import {
@ -33,6 +33,7 @@ class AppState {
userDetails: User = {} as User;
userTeams: Array<UserTeams> = [];
userRoles: Array<Role> = [];
userPermissions: UserPermissions = {} as UserPermissions;
inPageSearchText = '';
explorePageTab = 'tables';
@ -50,6 +51,7 @@ class AppState {
updateAuthState: action,
updateUserRole: action,
updateUsers: action,
updateUserPermissions: action,
});
}
@ -74,6 +76,9 @@ class AppState {
updateAuthState(state: boolean) {
this.authDisabled = state;
}
updateUserPermissions(permissions: UserPermissions) {
this.userPermissions = permissions;
}
}
export default new AppState();

View File

@ -15,6 +15,7 @@ import { AxiosResponse } from 'axios';
import { CookieStorage } from 'cookie-storage';
import { isEmpty, isNil } from 'lodash';
import { observer } from 'mobx-react';
import { UserPermissions } from 'Models';
import { UserManager, WebStorageStateStore } from 'oidc-client';
import React, {
ComponentType,
@ -32,7 +33,10 @@ import {
} from 'react-router-dom';
import appState from '../AppState';
import axiosClient from '../axiosAPIs';
import { fetchAuthorizerConfig } from '../axiosAPIs/miscAPI';
import {
fetchAuthorizerConfig,
getLoggedInUserPermissions,
} from '../axiosAPIs/miscAPI';
import {
getLoggedInUser,
getUserByName,
@ -130,6 +134,21 @@ const AuthProvider: FunctionComponent<AuthProviderProps> = ({
}
};
const getUserPermissions = () => {
setLoading(true);
getLoggedInUserPermissions()
.then((res: AxiosResponse) => {
appState.updateUserPermissions(res.data.metadataOperations);
})
.catch(() =>
showToast({
variant: 'error',
body: 'Error while getting user permissions',
})
)
.finally(() => setLoading(false));
};
const fetchUserByEmail = (user: OidcUser) => {
getUserByName(getNameFromEmail(user.profile.email), userAPIQueryFields)
.then((res: AxiosResponse) => {
@ -137,6 +156,7 @@ const AuthProvider: FunctionComponent<AuthProviderProps> = ({
if (res.data?.isAdmin) {
getUpdatedUser(res.data, user);
}
getUserPermissions();
appState.updateUserDetails(res.data);
fetchAllUsers();
handledVerifiedUser();
@ -148,6 +168,7 @@ const AuthProvider: FunctionComponent<AuthProviderProps> = ({
if (err.response.data.code === 404) {
appState.updateNewUser(user.profile);
appState.updateUserDetails({} as User);
appState.updateUserPermissions({} as UserPermissions);
history.push(ROUTES.SIGNUP);
}
});
@ -155,6 +176,7 @@ const AuthProvider: FunctionComponent<AuthProviderProps> = ({
const resetUserDetails = () => {
appState.updateUserDetails({} as User);
appState.updateUserPermissions({} as UserPermissions);
cookieStorage.removeItem(oidcTokenKey);
cookieStorage.removeItem(
`oidc.user:${userManagerConfig?.authority}:${userManagerConfig?.client_id}`
@ -167,11 +189,12 @@ const AuthProvider: FunctionComponent<AuthProviderProps> = ({
getLoggedInUser(userAPIQueryFields)
.then((res: AxiosResponse) => {
if (res.data) {
getUserPermissions();
appState.updateUserDetails(res.data);
} else {
resetUserDetails();
setLoading(false);
}
setLoading(false);
})
.catch((err) => {
if (err.response.data.code === 404) {
@ -179,6 +202,7 @@ const AuthProvider: FunctionComponent<AuthProviderProps> = ({
}
});
};
const fetchAuthConfig = (): void => {
fetchAuthorizerConfig()
.then((res: AxiosResponse) => {

View File

@ -85,3 +85,8 @@ export const deleteLineageEdge: Function = (
`/lineage/${fromEntity}/${fromId}/${toEntity}/${toId}`
);
};
export const getLoggedInUserPermissions: Function =
(): Promise<AxiosResponse> => {
return APIClient.get('/permissions');
};

View File

@ -18,6 +18,7 @@ import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { getTeamDetailsPath } from '../../constants/constants';
import { Dashboard } from '../../generated/entity/data/dashboard';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { User } from '../../generated/entity/teams/user';
import { LabelType, State, TagLabel } from '../../generated/type/tagLabel';
import { useAuth } from '../../hooks/authHooks';
@ -425,6 +426,7 @@ const DashboardDetails = ({
Boolean(owner)
)}
isOwner={hasEditAccess()}
permission={Operation.UpdateDescription}
position="top">
<button
className="tw-self-start tw-w-8 tw-h-auto tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none"
@ -469,6 +471,7 @@ const DashboardDetails = ({
Boolean(owner)
)}
isOwner={hasEditAccess()}
permission={Operation.UpdateTags}
position="left"
trigger="click">
<TagsContainer

View File

@ -38,12 +38,14 @@ import ReactFlow, {
} from 'react-flow-renderer';
import { getTableDetails } from '../../axiosAPIs/tableAPI';
import { Column } from '../../generated/entity/data/table';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import {
Edge as EntityEdge,
EntityLineage,
} from '../../generated/type/entityLineage';
import { EntityReference } from '../../generated/type/entityReference';
import { withLoader } from '../../hoc/withLoader';
import { useAuth } from '../../hooks/authHooks';
import useToastContext from '../../hooks/useToastContext';
import {
dragHandle,
@ -60,6 +62,7 @@ import {
} from '../../utils/EntityLineageUtils';
import SVGIcons from '../../utils/SvgUtils';
import { getEntityIcon } from '../../utils/TableUtils';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import EntityInfoDrawer from '../EntityInfoDrawer/EntityInfoDrawer.component';
import Loader from '../Loader/Loader';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
@ -88,6 +91,7 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
entityLineageHandler,
}: EntityLineageProp) => {
const showToast = useToastContext();
const { userPermissions, isAuthDisabled } = useAuth();
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [lineageData, setLineageData] = useState<EntityLineage>(entityLineage);
const [reactFlowInstance, setReactFlowInstance] = useState<OnLoadParams>();
@ -659,41 +663,54 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
className="tw-absolute tw-top-1 tw-right-1 tw-bottom-full tw-ml-4 tw-mt-4"
fitViewParams={{ minZoom: 0.5, maxZoom: 2.5 }}>
{!deleted && (
<ControlButton
className={classNames(
'tw-h-9 tw-w-9 tw-rounded-full tw-px-1 tw-shadow-lg tw-cursor-pointer',
{
'tw-bg-primary': isEditMode,
'tw-bg-primary-hover-lite': !isEditMode,
}
)}
onClick={() => {
setEditMode((pre) => !pre && !deleted);
setSelectedNode({} as SelectedNode);
setIsDrawerOpen(false);
setNewAddedNode({} as FlowElement);
}}>
{loading ? (
<Loader size="small" type="white" />
) : status === 'success' ? (
<i
aria-hidden="true"
className="fa fa-check tw-text-white"
/>
) : (
<SVGIcons
alt="icon-edit-lineag"
className="tw--mt-1"
data-testid="edit-lineage"
icon={
!isEditMode
? 'icon-edit-lineage-color'
: 'icon-edit-lineage'
<NonAdminAction
html={
<>
<p>You do not have permission to edit the lineage</p>
</>
}
permission={Operation.UpdateLineage}>
<ControlButton
className={classNames(
'tw-h-9 tw-w-9 tw-rounded-full tw-px-1 tw-shadow-lg tw-cursor-pointer',
{
'tw-bg-primary': isEditMode,
'tw-bg-primary-hover-lite': !isEditMode,
},
{
'tw-opacity-40':
!userPermissions[Operation.UpdateLineage] &&
!isAuthDisabled,
}
width="14"
/>
)}
</ControlButton>
)}
onClick={() => {
setEditMode((pre) => !pre && !deleted);
setSelectedNode({} as SelectedNode);
setIsDrawerOpen(false);
setNewAddedNode({} as FlowElement);
}}>
{loading ? (
<Loader size="small" type="white" />
) : status === 'success' ? (
<i
aria-hidden="true"
className="fa fa-check tw-text-white"
/>
) : (
<SVGIcons
alt="icon-edit-lineag"
className="tw--mt-1"
data-testid="edit-lineage"
icon={
!isEditMode
? 'icon-edit-lineage-color'
: 'icon-edit-lineage'
}
width="14"
/>
)}
</ControlButton>
</NonAdminAction>
)}
</CustomControls>
{isEditMode ? (

View File

@ -25,6 +25,7 @@ import {
JoinedWith,
Table,
} from '../../generated/entity/data/table';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { LabelType, State, TagLabel } from '../../generated/type/tagLabel';
import {
getHtmlForNonAdminAction,
@ -435,6 +436,7 @@ const EntityTable = ({
<NonAdminAction
html={getHtmlForNonAdminAction(Boolean(owner))}
isOwner={hasEditAccess}
permission={Operation.UpdateTags}
position="left"
trigger="click">
<TagsContainer
@ -496,6 +498,7 @@ const EntityTable = ({
Boolean(owner)
)}
isOwner={hasEditAccess}
permission={Operation.UpdateDescription}
position="top">
<button
className="tw-self-start tw-w-8 tw-h-auto tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none"

View File

@ -19,6 +19,8 @@ import { TableDetail } from 'Models';
import React, { FunctionComponent, useEffect, useState } from 'react';
import appState from '../../AppState';
import { getCategory } from '../../axiosAPIs/tagAPI';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { useAuth } from '../../hooks/authHooks';
import { getUserTeams } from '../../utils/CommonUtils';
import SVGIcons from '../../utils/SvgUtils';
import { Button } from '../buttons/Button/Button';
@ -44,6 +46,7 @@ const ManageTab: FunctionComponent<Props> = ({
onSave,
hasEditAccess,
}: Props) => {
const { userPermissions, isAuthDisabled } = useAuth();
const getOwnerList = () => {
const user = !isEmpty(appState.userDetails)
? appState.userDetails
@ -220,33 +223,46 @@ const ManageTab: FunctionComponent<Props> = ({
<div className="tw-mt-2 tw-mb-4 tw-pb-4 tw-border-b tw-border-separator">
<span className="tw-mr-2">Owner:</span>
<span className="tw-relative">
<Button
className="tw-underline"
data-testid="owner-dropdown"
disabled={!listOwners.length}
size="custom"
theme="primary"
variant="link"
onClick={() => setListVisible((visible) => !visible)}>
{ownerName ? (
<span
className={classNames('tw-truncate', {
'tw-w-52': ownerName.length > 32,
})}
title={ownerName}>
{ownerName}
</span>
) : (
'Select Owner'
)}
<SVGIcons
alt="edit"
className="tw-ml-1"
icon="icon-edit"
title="Edit"
width="12px"
/>
</Button>
<NonAdminAction
html={
<>
<p>You do not have permissions to update the owner.</p>
</>
}
isOwner={hasEditAccess || Boolean(owner)}
permission={Operation.UpdateOwner}
position="left">
<Button
className={classNames('tw-underline', {
'tw-opacity-40':
!userPermissions[Operation.UpdateOwner] && !isAuthDisabled,
})}
data-testid="owner-dropdown"
disabled={!listOwners.length}
size="custom"
theme="primary"
variant="link"
onClick={() => setListVisible((visible) => !visible)}>
{ownerName ? (
<span
className={classNames('tw-truncate', {
'tw-w-52': ownerName.length > 32,
})}
title={ownerName}>
{ownerName}
</span>
) : (
'Select Owner'
)}
<SVGIcons
alt="edit"
className="tw-ml-1"
icon="icon-edit"
title="Edit"
width="12px"
/>
</Button>
</NonAdminAction>
{listVisible && (
<DropDownList
showSearchBar
@ -273,6 +289,7 @@ const ManageTab: FunctionComponent<Props> = ({
}
isOwner={hasEditAccess || Boolean(owner)}
key={i}
permission={Operation.UpdateTags}
position="left">
<CardListItem
card={card}

View File

@ -18,6 +18,7 @@ import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { getTeamDetailsPath } from '../../constants/constants';
import { Pipeline, Task } from '../../generated/entity/data/pipeline';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { User } from '../../generated/entity/teams/user';
import { LabelType, State } from '../../generated/type/tagLabel';
import { useAuth } from '../../hooks/authHooks';
@ -377,6 +378,7 @@ const PipelineDetails = ({
Boolean(owner)
)}
isOwner={hasEditAccess()}
permission={Operation.UpdateDescription}
position="top">
<button
className="tw-self-start tw-w-8 tw-h-auto tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none"

View File

@ -14,6 +14,7 @@
import classNames from 'classnames';
import React from 'react';
import { Table } from '../../../generated/entity/data/table';
import { Operation } from '../../../generated/entity/policies/accessControl/rule';
import { getHtmlForNonAdminAction } from '../../../utils/CommonUtils';
import SVGIcons from '../../../utils/SvgUtils';
import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
@ -94,6 +95,7 @@ const Description = ({
<NonAdminAction
html={getHtmlForNonAdminAction(Boolean(owner))}
isOwner={hasEditAccess}
permission={Operation.UpdateDescription}
position="right">
<button
className="focus:tw-outline-none"

View File

@ -16,6 +16,7 @@ import { isEmpty, isUndefined } from 'lodash';
import { EntityTags, ExtraInfo, TableDetail } from 'Models';
import React, { useEffect, useState } from 'react';
import { FOLLOWERS_VIEW_CAP, LIST_SIZE } from '../../../constants/constants';
import { Operation } from '../../../generated/entity/policies/accessControl/rule';
import { User } from '../../../generated/entity/teams/user';
import { TagLabel } from '../../../generated/type/tagLabel';
import { getHtmlForNonAdminAction } from '../../../utils/CommonUtils';
@ -341,6 +342,7 @@ const EntityPageInfo = ({
<NonAdminAction
html={getHtmlForNonAdminAction(Boolean(owner))}
isOwner={hasEditAccess}
permission={Operation.UpdateTags}
position="bottom"
trigger="click">
<div

View File

@ -11,7 +11,9 @@
* limitations under the License.
*/
import { UserPermissions } from 'Models';
import React from 'react';
import { Operation } from '../../../generated/entity/policies/accessControl/rule';
import { useAuth } from '../../../hooks/authHooks';
import PopOver from '../popover/PopOver';
@ -23,6 +25,7 @@ type Props = {
isOwner?: boolean;
html?: React.ReactElement;
trigger?: 'mouseenter' | 'focus' | 'click' | 'manual';
permission?: Operation;
};
const NonAdminAction = ({
@ -33,8 +36,9 @@ const NonAdminAction = ({
isOwner = false,
html,
trigger = 'mouseenter',
permission,
}: Props) => {
const { isAuthDisabled, isAdminUser } = useAuth();
const { isAuthDisabled, isAdminUser, userPermissions } = useAuth();
const handleCapturedEvent = (
e: React.KeyboardEvent | React.MouseEvent
@ -45,7 +49,10 @@ const NonAdminAction = ({
return (
<span className={className}>
{isAdminUser || isOwner || isAuthDisabled ? (
{isAdminUser ||
isOwner ||
isAuthDisabled ||
userPermissions[permission as keyof UserPermissions] ? (
<span>{children}</span>
) : (
<PopOver

View File

@ -16,7 +16,8 @@ import AppState from '../AppState';
import { ROUTES } from '../constants/constants';
export const useAuth = (pathname = '') => {
const { authDisabled, userDetails, newUser, authProvider } = AppState;
const { authDisabled, userDetails, newUser, authProvider, userPermissions } =
AppState;
const isAuthenticatedRoute =
pathname !== ROUTES.SIGNUP &&
pathname !== ROUTES.SIGNIN &&
@ -37,5 +38,6 @@ export const useAuth = (pathname = '') => {
isAdminUser: userDetails?.isAdmin,
isFirstTimeUser: !isEmpty(userDetails) && !isEmpty(newUser),
isTourRoute,
userPermissions,
};
};

View File

@ -484,4 +484,13 @@ declare module 'Models' {
env?: string;
pipelineUrl?: string;
};
export interface UserPermissions {
UpdateOwner: boolean;
UpdateDescription: boolean;
SuggestDescription: boolean;
UpdateLineage: boolean;
SuggestTags: boolean;
UpdateTags: boolean;
}
}

View File

@ -43,6 +43,7 @@ import {
CreateTagCategory,
TagCategoryType,
} from '../../generated/api/tags/createTagCategory';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { TagCategory, TagClass } from '../../generated/entity/tags/tagCategory';
import { useAuth } from '../../hooks/authHooks';
import {
@ -389,6 +390,7 @@ const TagsPage = () => {
)}
</div>
<NonAdminAction
permission={Operation.UpdateDescription}
position="left"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<button
@ -435,6 +437,7 @@ const TagsPage = () => {
setEditTag(tag);
}}>
<NonAdminAction
permission={Operation.UpdateTags}
position="left"
title={TITLE_FOR_NON_ADMIN_ACTION}
trigger="click">

View File

@ -479,13 +479,19 @@ const TeamsPage = () => {
</>
) : (
<ErrorPlaceHolder>
<p className="w-text-lg tw-text-center">No Teams Added.</p>
<p className="w-text-lg tw-text-center">
<button
className="link-text tw-underline"
onClick={() => setIsAddingTeam(true)}>
Click here
</button>
<p className="tw-text-lg tw-text-center">No Teams Added.</p>
<p className="tw-text-lg tw-text-center">
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<button
className={classNames('link-text tw-underline', {
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
})}
onClick={() => setIsAddingTeam(true)}>
Click here
</button>
</NonAdminAction>
{' to add new Team'}
</p>
</ErrorPlaceHolder>