mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-19 06:28:03 +00:00
UI: Adding Policy UI. (#2259)
* [WIP]UI: Adding Policy UI. * Integrating API to get Roles and Policy. * Adding API integration to create and update role * Adding form for Rule * changing types * removing suggested rules * Changing rule form modal layout. * Adding support for adding rules * Adding support for deleting rule * Adding support for editing rules * Adding validation for rules * Adding isAdminOnly check for roles page * Adding license to new file * Minor Fix * replacing break-all with break-words class. * Addressing review comments
This commit is contained in:
parent
6693b4ae40
commit
590a0ab624
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { Policy } from '../pages/RolesPage/policy.interface';
|
||||
import { getURLWithQueryFields } from '../utils/APIUtils';
|
||||
import APIClient from './index';
|
||||
|
||||
export const getRoles = (
|
||||
arrQueryFields?: string | string[]
|
||||
): Promise<AxiosResponse> => {
|
||||
const url = getURLWithQueryFields('/roles', arrQueryFields);
|
||||
|
||||
return APIClient.get(`${url}${arrQueryFields ? '&' : '?'}limit=1000000`);
|
||||
};
|
||||
export const getRoleByName = (
|
||||
name: string,
|
||||
arrQueryFields?: string | string[]
|
||||
): Promise<AxiosResponse> => {
|
||||
const url = getURLWithQueryFields(`/roles/name/${name}`, arrQueryFields);
|
||||
|
||||
return APIClient.get(url);
|
||||
};
|
||||
|
||||
export const createRole = (
|
||||
data: Record<string, string>
|
||||
): Promise<AxiosResponse> => {
|
||||
return APIClient.post('/roles', data);
|
||||
};
|
||||
|
||||
export const updateRole = (
|
||||
id: string,
|
||||
patch: Operation[]
|
||||
): Promise<AxiosResponse> => {
|
||||
const configOptions = {
|
||||
headers: { 'Content-type': 'application/json-patch+json' },
|
||||
};
|
||||
|
||||
return APIClient.patch(`/roles/${id}`, patch, configOptions);
|
||||
};
|
||||
|
||||
export const getPolicy = (
|
||||
id: string,
|
||||
arrQueryFields?: string | string[]
|
||||
): Promise<AxiosResponse> => {
|
||||
const url = getURLWithQueryFields(`/policies/${id}`, arrQueryFields);
|
||||
|
||||
return APIClient.get(url);
|
||||
};
|
||||
|
||||
export const updatePolicy = (
|
||||
data: Pick<Policy, 'name' | 'policyType' | 'rules'>
|
||||
): Promise<AxiosResponse> => {
|
||||
return APIClient.put(`/policies`, data);
|
||||
};
|
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { FormErrorData } from 'Models';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { RuleAccess } from '../../../enums/rule.enum';
|
||||
import {
|
||||
Operation,
|
||||
Rule,
|
||||
} from '../../../generated/entity/policies/accessControl/rule';
|
||||
import { errorMsg } from '../../../utils/CommonUtils';
|
||||
import { Button } from '../../buttons/Button/Button';
|
||||
|
||||
interface AddRuleProps {
|
||||
header: string;
|
||||
errorData?: FormErrorData;
|
||||
initialData: Rule;
|
||||
isEditing?: boolean;
|
||||
onCancel: () => void;
|
||||
onSave: (data: Rule) => void;
|
||||
onChange?: (data: Rule) => void;
|
||||
}
|
||||
|
||||
const AddRuleModal: FC<AddRuleProps> = ({
|
||||
onCancel,
|
||||
header,
|
||||
initialData,
|
||||
errorData,
|
||||
onSave,
|
||||
isEditing = false,
|
||||
onChange,
|
||||
}: AddRuleProps) => {
|
||||
const [data, setData] = useState<Rule>(initialData);
|
||||
const [access, setAccess] = useState<RuleAccess>(
|
||||
initialData.allow ? RuleAccess.ALLOW : RuleAccess.DENY
|
||||
);
|
||||
const [isEnabled, setIsEnabled] = useState<boolean>(
|
||||
Boolean(initialData.enabled)
|
||||
);
|
||||
const onChangeHadler = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
e.persist();
|
||||
let rule = data;
|
||||
setData((prevState) => {
|
||||
rule = {
|
||||
...prevState,
|
||||
[e.target.name]: e.target.value,
|
||||
};
|
||||
|
||||
return rule;
|
||||
});
|
||||
onChange?.(rule);
|
||||
};
|
||||
|
||||
const onSubmitHandler = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const rule = {
|
||||
...data,
|
||||
allow: access === RuleAccess.ALLOW,
|
||||
enabled: isEnabled,
|
||||
};
|
||||
onSave(rule);
|
||||
onChange?.(rule);
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog className="tw-modal" data-testid="modal-container">
|
||||
<div className="tw-modal-backdrop" onClick={() => onCancel()} />
|
||||
<div className="tw-modal-container tw-overflow-y-auto tw-max-h-screen tw-w-120">
|
||||
<form onSubmit={onSubmitHandler}>
|
||||
<div className="tw-modal-header">
|
||||
<p
|
||||
className="tw-modal-title tw-text-grey-body"
|
||||
data-testid="header">
|
||||
{header}
|
||||
</p>
|
||||
</div>
|
||||
<div className="tw-modal-body">
|
||||
{!isUndefined(initialData.operation) && (
|
||||
<div className="tw-mb-4">
|
||||
<label className="tw-form-label required-field">
|
||||
Operation
|
||||
</label>
|
||||
<select
|
||||
className={classNames(
|
||||
'tw-text-sm tw-appearance-none tw-border tw-border-main',
|
||||
'tw-rounded tw-w-full tw-py-2 tw-px-3 tw-text-grey-body tw-leading-tight',
|
||||
'focus:tw-outline-none focus:tw-border-focus hover:tw-border-hover tw-h-10 tw-bg-white',
|
||||
{ 'tw-cursor-not-allowed tw-opacity-60': isEditing }
|
||||
)}
|
||||
disabled={isEditing}
|
||||
name="operation"
|
||||
value={data.operation}
|
||||
onChange={onChangeHadler}>
|
||||
<option value="">Select Operation</option>
|
||||
<option value={Operation.UpdateDescription}>
|
||||
Update Description
|
||||
</option>
|
||||
<option value={Operation.UpdateOwner}>Update Owner</option>
|
||||
<option value={Operation.UpdateTags}>Update Tags</option>
|
||||
</select>
|
||||
{errorData?.operation && errorMsg(errorData.operation)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="tw-mb-4">
|
||||
<label className="tw-form-label">Access</label>
|
||||
<select
|
||||
required
|
||||
className="tw-text-sm tw-appearance-none tw-border tw-border-main
|
||||
tw-rounded tw-w-full tw-py-2 tw-px-3 tw-text-grey-body tw-leading-tight
|
||||
focus:tw-outline-none focus:tw-border-focus hover:tw-border-hover tw-h-10 tw-bg-white"
|
||||
name="access"
|
||||
value={access}
|
||||
onChange={(e) => setAccess(e.target.value as RuleAccess)}>
|
||||
<option value={RuleAccess.ALLOW}>ALLOW</option>
|
||||
<option value={RuleAccess.DENY}>DENY</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="tw-flex tw-items-center">
|
||||
<label>Enable</label>
|
||||
<div
|
||||
className={classNames(
|
||||
'toggle-switch tw-ml-4',
|
||||
isEnabled ? 'open' : null
|
||||
)}
|
||||
data-testid="rule-switch"
|
||||
onClick={() => setIsEnabled((pre) => !pre)}>
|
||||
<div className="switch" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tw-modal-footer" data-testid="cta-container">
|
||||
<Button
|
||||
size="regular"
|
||||
theme="primary"
|
||||
variant="link"
|
||||
onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="saveButton"
|
||||
size="regular"
|
||||
theme="primary"
|
||||
type="submit"
|
||||
variant="contained">
|
||||
{isEditing ? 'Update' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddRuleModal;
|
@ -49,7 +49,9 @@ const Toast = (props: ToastProps) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tw-font-semibold tw-self-center tw-px-1">{body}</div>
|
||||
<div className="tw-font-semibold tw-self-center tw-px-1 tw-break-words">
|
||||
{body}
|
||||
</div>
|
||||
<button
|
||||
className="tw-font-semibold"
|
||||
data-testid="dismiss"
|
||||
|
@ -150,6 +150,7 @@ export const ROUTES = {
|
||||
ONBOARDING: '/onboarding',
|
||||
INGESTION: '/ingestion',
|
||||
USER_LIST: '/user-list',
|
||||
ROLES: '/roles',
|
||||
};
|
||||
|
||||
export const IN_PAGE_SEARCH_ROUTES: Record<string, Array<string>> = {
|
||||
@ -271,6 +272,7 @@ export const navLinkDevelop = [
|
||||
|
||||
export const navLinkSettings = [
|
||||
{ name: 'Teams', to: '/teams', disabled: false },
|
||||
{ name: 'Roles', to: '/roles', disabled: false, isAdminOnly: true },
|
||||
{ name: 'Users', to: '/user-list', disabled: false, isAdminOnly: true },
|
||||
{ name: 'Tags', to: '/tags', disabled: false },
|
||||
// { name: 'Store', to: '/store', disabled: false },
|
||||
|
17
openmetadata-ui/src/main/resources/ui/src/enums/rule.enum.ts
Normal file
17
openmetadata-ui/src/main/resources/ui/src/enums/rule.enum.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum RuleAccess {
|
||||
ALLOW = 'allow',
|
||||
DENY = 'deny',
|
||||
}
|
@ -38,6 +38,10 @@ export interface Role {
|
||||
href?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
/**
|
||||
* Policy that is attached to this role.
|
||||
*/
|
||||
policy?: EntityReference;
|
||||
/**
|
||||
* Last update time corresponding to the new version of the entity in Unix epoch time
|
||||
* milliseconds.
|
||||
@ -47,6 +51,10 @@ export interface Role {
|
||||
* User who made the update.
|
||||
*/
|
||||
updatedBy?: string;
|
||||
/**
|
||||
* Users that have this role assigned.
|
||||
*/
|
||||
users?: EntityReference[];
|
||||
/**
|
||||
* Metadata version of the entity.
|
||||
*/
|
||||
@ -93,3 +101,42 @@ export interface FieldChange {
|
||||
*/
|
||||
oldValue?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Policy that is attached to this role.
|
||||
*
|
||||
* This schema defines the EntityReference type used for referencing an entity.
|
||||
* EntityReference is used for capturing relationships from one entity to another. For
|
||||
* example, a table has an attribute called database of type EntityReference that captures
|
||||
* the relationship of a table `belongs to a` database.
|
||||
*
|
||||
* Users that have this role assigned.
|
||||
*/
|
||||
export interface EntityReference {
|
||||
/**
|
||||
* Optional description of entity.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Display Name that identifies this entity.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Link to the entity resource.
|
||||
*/
|
||||
href?: string;
|
||||
/**
|
||||
* Unique identifier that identifies an entity instance.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Name of the entity instance. For entities such as tables, databases where the name is not
|
||||
* unique, fullyQualifiedName is returned in this field.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Entity type/class name - Examples: `database`, `table`, `metrics`, `databaseService`,
|
||||
* `dashboardService`...
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This schema defines the EntityRelationship type used for establishing relationship
|
||||
* between two entities. EntityRelationship is used for capturing relationships from one
|
||||
* entity to another. For example, a database contains tables.
|
||||
*/
|
||||
export interface EntityRelationship {
|
||||
/**
|
||||
* `true` indicates the relationship has been soft deleted.
|
||||
*/
|
||||
deleted?: boolean;
|
||||
/**
|
||||
* Type of the entity from which the relationship originates. Examples: `database`, `table`,
|
||||
* `metrics` ...
|
||||
*/
|
||||
fromEntity: string;
|
||||
/**
|
||||
* Fully qualified name of the entity from which the relationship originates.
|
||||
*/
|
||||
fromFQN?: string;
|
||||
/**
|
||||
* Unique identifier that identifies the entity from which the relationship originates.
|
||||
*/
|
||||
fromId?: string;
|
||||
/**
|
||||
* Describes relationship between the two entities.
|
||||
*/
|
||||
relation: string;
|
||||
/**
|
||||
* Type of the entity towards which the relationship refers to. Examples: `database`,
|
||||
* `table`, `metrics` ...
|
||||
*/
|
||||
toEntity: string;
|
||||
/**
|
||||
* Fully qualified name of the entity towards which the relationship refers to.
|
||||
*/
|
||||
toFQN?: string;
|
||||
/**
|
||||
* Unique identifier that identifies the entity towards which the relationship refers to.
|
||||
*/
|
||||
toId?: string;
|
||||
}
|
@ -0,0 +1,665 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { isUndefined, toLower } from 'lodash';
|
||||
import { observer } from 'mobx-react';
|
||||
import { FormErrorData } from 'Models';
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import {
|
||||
createRole,
|
||||
getPolicy,
|
||||
getRoleByName,
|
||||
getRoles,
|
||||
updatePolicy,
|
||||
updateRole,
|
||||
} from '../../axiosAPIs/rolesAPI';
|
||||
import { Button } from '../../components/buttons/Button/Button';
|
||||
import Description from '../../components/common/description/Description';
|
||||
import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||
import NonAdminAction from '../../components/common/non-admin-action/NonAdminAction';
|
||||
import PageContainerV1 from '../../components/containers/PageContainerV1';
|
||||
import PageLayout from '../../components/containers/PageLayout';
|
||||
import Loader from '../../components/Loader/Loader';
|
||||
import ConfirmationModal from '../../components/Modals/ConfirmationModal/ConfirmationModal';
|
||||
import FormModal from '../../components/Modals/FormModal';
|
||||
import AddRuleModal from '../../components/Modals/RulesModal/AddRuleModal';
|
||||
import {
|
||||
ERROR404,
|
||||
TITLE_FOR_NON_ADMIN_ACTION,
|
||||
} from '../../constants/constants';
|
||||
import {
|
||||
Operation,
|
||||
Rule,
|
||||
} from '../../generated/entity/policies/accessControl/rule';
|
||||
import { Role } from '../../generated/entity/teams/role';
|
||||
import { EntityReference } from '../../generated/entity/teams/user';
|
||||
import { useAuth } from '../../hooks/authHooks';
|
||||
import useToastContext from '../../hooks/useToastContext';
|
||||
import { getActiveCatClass, isEven } from '../../utils/CommonUtils';
|
||||
import SVGIcons from '../../utils/SvgUtils';
|
||||
import Form from '../teams/Form';
|
||||
import UserCard from '../teams/UserCard';
|
||||
import { Policy } from './policy.interface';
|
||||
|
||||
const getActiveTabClass = (tab: number, currentTab: number) => {
|
||||
return tab === currentTab ? 'active' : '';
|
||||
};
|
||||
|
||||
const RolesPage = () => {
|
||||
const showToast = useToastContext();
|
||||
const [roles, setRoles] = useState<Array<Role>>([]);
|
||||
const { isAuthDisabled, isAdminUser } = useAuth();
|
||||
const [currentRole, setCurrentRole] = useState<Role>();
|
||||
const [currentPolicy, setCurrentPolicy] = useState<Policy>();
|
||||
const [error, setError] = useState<string>('');
|
||||
const [currentTab, setCurrentTab] = useState<number>(1);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isLoadingPolicy, setIsLoadingPolicy] = useState<boolean>(false);
|
||||
const [isAddingRole, setIsAddingRole] = useState<boolean>(false);
|
||||
const [isAddingRule, setIsAddingRule] = useState<boolean>(false);
|
||||
const [errorData, setErrorData] = useState<FormErrorData>();
|
||||
const [isEditable, setIsEditable] = useState<boolean>(false);
|
||||
const [deletingRule, setDeletingRule] = useState<{
|
||||
rule: Rule | undefined;
|
||||
state: boolean;
|
||||
}>({ rule: undefined, state: false });
|
||||
|
||||
const [editingRule, setEditingRule] = useState<{
|
||||
rule: Rule | undefined;
|
||||
state: boolean;
|
||||
}>({ rule: undefined, state: false });
|
||||
|
||||
const onNewDataChange = (data: Role, forceSet = false) => {
|
||||
if (errorData || forceSet) {
|
||||
const errData: { [key: string]: string } = {};
|
||||
if (!data.name.trim()) {
|
||||
errData['name'] = 'Name is required';
|
||||
} else if (
|
||||
!isUndefined(
|
||||
roles.find((item) => toLower(item.name) === toLower(data.name))
|
||||
)
|
||||
) {
|
||||
errData['name'] = 'Name already exists';
|
||||
} else if (data.name.length < 1 || data.name.length > 128) {
|
||||
errData['name'] = 'Name size must be between 1 and 128';
|
||||
}
|
||||
if (!data.displayName?.trim()) {
|
||||
errData['displayName'] = 'Display name is required';
|
||||
} else if (data.displayName.length < 1 || data.displayName.length > 128) {
|
||||
errData['displayName'] = 'Display name size must be between 1 and 128';
|
||||
}
|
||||
setErrorData(errData);
|
||||
|
||||
return errData;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const validateRuleData = (data: Rule, forceSet = false) => {
|
||||
if (errorData || forceSet) {
|
||||
const errData: { [key: string]: string } = {};
|
||||
if (!data.operation) {
|
||||
errData['operation'] = 'Operation is required.';
|
||||
}
|
||||
setErrorData(errData);
|
||||
|
||||
return errData;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const onDescriptionEdit = (): void => {
|
||||
setIsEditable(true);
|
||||
};
|
||||
|
||||
const onCancel = (): void => {
|
||||
setIsEditable(false);
|
||||
};
|
||||
|
||||
const fetchPolicy = (id: string) => {
|
||||
setIsLoadingPolicy(true);
|
||||
getPolicy(
|
||||
id,
|
||||
'displayName,description,owner,policyUrl,enabled,rules,location'
|
||||
)
|
||||
.then((res: AxiosResponse) => {
|
||||
setCurrentPolicy(res.data);
|
||||
})
|
||||
.catch(() => {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
body: 'Error while getting policy',
|
||||
});
|
||||
})
|
||||
.finally(() => setIsLoadingPolicy(false));
|
||||
};
|
||||
|
||||
const fetchRoles = () => {
|
||||
setIsLoading(true);
|
||||
getRoles(['policy', 'users'])
|
||||
.then((res: AxiosResponse) => {
|
||||
const { data } = res.data;
|
||||
setRoles(data);
|
||||
setCurrentRole(data[0]);
|
||||
})
|
||||
.catch(() => {
|
||||
setError('Error while getting roles');
|
||||
showToast({
|
||||
variant: 'error',
|
||||
body: 'Error while getting roles',
|
||||
});
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
const createNewRole = (data: Role) => {
|
||||
const errData = onNewDataChange(data, true);
|
||||
const { description, name, displayName } = data;
|
||||
if (!Object.values(errData).length) {
|
||||
createRole({
|
||||
description: description as string,
|
||||
name,
|
||||
displayName: displayName as string,
|
||||
})
|
||||
.then((res: AxiosResponse) => {
|
||||
if (res.data) {
|
||||
fetchRoles();
|
||||
}
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
body: error.message ?? 'Something went wrong!',
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsAddingRole(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCurrentRole = (name: string, update = false) => {
|
||||
if (currentRole?.name !== name || update) {
|
||||
setIsLoading(true);
|
||||
getRoleByName(name, ['users', 'policy'])
|
||||
.then((res: AxiosResponse) => {
|
||||
setCurrentRole(res.data);
|
||||
if (roles.length <= 0) {
|
||||
fetchRoles();
|
||||
}
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
if (err?.response?.data.code) {
|
||||
setError(ERROR404);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDescriptionUpdate = (updatedHTML: string) => {
|
||||
if (currentRole?.description !== updatedHTML) {
|
||||
const updatedRole = { ...currentRole, description: updatedHTML };
|
||||
const jsonPatch = compare(currentRole as Role, updatedRole);
|
||||
updateRole(currentRole?.id as string, jsonPatch).then(
|
||||
(res: AxiosResponse) => {
|
||||
if (res.data) {
|
||||
fetchCurrentRole(res.data.name, true);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
setIsEditable(false);
|
||||
} else {
|
||||
setIsEditable(false);
|
||||
}
|
||||
};
|
||||
|
||||
const createRule = (data: Rule) => {
|
||||
const errData = validateRuleData(data, true);
|
||||
if (!Object.values(errData).length) {
|
||||
const newRule = {
|
||||
...data,
|
||||
name: `${currentPolicy?.name}-${data.operation}`,
|
||||
userRoleAttr: currentRole?.name,
|
||||
};
|
||||
const updatedPolicy = {
|
||||
name: currentPolicy?.name as string,
|
||||
policyType: currentPolicy?.policyType as string,
|
||||
rules: [...(currentPolicy?.rules as Rule[]), newRule],
|
||||
};
|
||||
|
||||
updatePolicy(updatedPolicy)
|
||||
.then((res: AxiosResponse) => {
|
||||
setCurrentPolicy(res.data);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
body: err.response?.data?.message ?? 'Error while adding new rule',
|
||||
});
|
||||
})
|
||||
.finally(() => setIsAddingRule(false));
|
||||
}
|
||||
};
|
||||
|
||||
const onRuleUpdate = (data: Rule) => {
|
||||
const rules = currentPolicy?.rules?.map((rule) => {
|
||||
if (rule.name === data.name) {
|
||||
return data;
|
||||
} else {
|
||||
return rule;
|
||||
}
|
||||
});
|
||||
|
||||
const updatedPolicy = {
|
||||
name: currentPolicy?.name as string,
|
||||
policyType: currentPolicy?.policyType as string,
|
||||
rules: rules as Rule[],
|
||||
};
|
||||
updatePolicy(updatedPolicy)
|
||||
.then((res: AxiosResponse) => {
|
||||
setCurrentPolicy(res.data);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
body:
|
||||
err.response?.data?.message ??
|
||||
`Error while updating ${data.name} rule`,
|
||||
});
|
||||
})
|
||||
.finally(() => setEditingRule({ rule: undefined, state: false }));
|
||||
};
|
||||
|
||||
const deleteRule = (data: Rule) => {
|
||||
const updatedPolicy = {
|
||||
name: currentPolicy?.name as string,
|
||||
policyType: currentPolicy?.policyType as string,
|
||||
rules: currentPolicy?.rules?.filter(
|
||||
(rule) => rule.operation !== data.operation
|
||||
) as Rule[],
|
||||
};
|
||||
updatePolicy(updatedPolicy)
|
||||
.then((res: AxiosResponse) => {
|
||||
setCurrentPolicy(res.data);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
body: err.response?.data?.message ?? 'Error while deleting rule',
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setDeletingRule({ rule: undefined, state: false });
|
||||
});
|
||||
};
|
||||
|
||||
const getTabs = () => {
|
||||
return (
|
||||
<div className="tw-mb-3 ">
|
||||
<nav
|
||||
className="tw-flex tw-flex-row tw-gh-tabs-container"
|
||||
data-testid="tabs">
|
||||
<button
|
||||
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(
|
||||
1,
|
||||
currentTab
|
||||
)}`}
|
||||
data-testid="users"
|
||||
onClick={() => {
|
||||
setCurrentTab(1);
|
||||
}}>
|
||||
Policy
|
||||
</button>
|
||||
<button
|
||||
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(
|
||||
2,
|
||||
currentTab
|
||||
)}`}
|
||||
data-testid="assets"
|
||||
onClick={() => {
|
||||
setCurrentTab(2);
|
||||
}}>
|
||||
Users
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const fetchLeftPanel = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="tw-flex tw-justify-between tw-items-center tw-mb-3 tw-border-b">
|
||||
<h6 className="tw-heading tw-text-base">Roles</h6>
|
||||
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
|
||||
<Button
|
||||
className={classNames('tw-h-7 tw-px-2 tw-mb-4', {
|
||||
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
|
||||
})}
|
||||
data-testid="add-role"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setErrorData(undefined);
|
||||
setIsAddingRole(true);
|
||||
}}>
|
||||
<i aria-hidden="true" className="fa fa-plus" />
|
||||
</Button>
|
||||
</NonAdminAction>
|
||||
</div>
|
||||
{roles &&
|
||||
roles.map((role) => (
|
||||
<div
|
||||
className={`tw-group tw-text-grey-body tw-cursor-pointer tw-text-body tw-mb-3 tw-flex tw-justify-between ${getActiveCatClass(
|
||||
role.name,
|
||||
currentRole?.name
|
||||
)}`}
|
||||
key={role.name}
|
||||
onClick={() => setCurrentRole(role)}>
|
||||
<p
|
||||
className="tag-category label-category tw-self-center tw-truncate tw-w-52"
|
||||
title={role.displayName}>
|
||||
{role.displayName}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getPolicyRules = (rules: Array<Rule>) => {
|
||||
if (!rules.length) {
|
||||
return (
|
||||
<div className="tw-text-center tw-py-5">
|
||||
<p className="tw-text-base">No Rules Added.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tw-bg-white">
|
||||
<table className="tw-w-full tw-overflow-x-auto" data-testid="table">
|
||||
<thead>
|
||||
<tr className="tableHead-row">
|
||||
<th className="tableHead-cell" data-testid="heading-description">
|
||||
Operation
|
||||
</th>
|
||||
<th className="tableHead-cell" data-testid="heading-description">
|
||||
Access
|
||||
</th>
|
||||
<th className="tableHead-cell" data-testid="heading-description">
|
||||
Enabled
|
||||
</th>
|
||||
<th className="tableHead-cell" data-testid="heading-description">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="tw-text-sm" data-testid="table-body">
|
||||
{rules.map((rule, index) => (
|
||||
<tr
|
||||
className={`tableBody-row ${!isEven(index + 1) && 'odd-row'}`}
|
||||
key={index}>
|
||||
<td className="tableBody-cell">
|
||||
<p>{rule.operation}</p>
|
||||
</td>
|
||||
<td className="tableBody-cell">
|
||||
<p
|
||||
className={classNames(
|
||||
rule.allow
|
||||
? 'tw-text-status-success'
|
||||
: 'tw-text-status-failed'
|
||||
)}>
|
||||
{rule.allow ? 'ALLOW' : 'DENY'}
|
||||
</p>
|
||||
</td>
|
||||
<td className="tableBody-cell">
|
||||
<div
|
||||
className={classNames(
|
||||
'toggle-switch tw-ml-4',
|
||||
rule.enabled ? 'open' : null
|
||||
)}
|
||||
data-testid="rule-switch"
|
||||
onClick={() =>
|
||||
onRuleUpdate({ ...rule, enabled: !rule.enabled })
|
||||
}>
|
||||
<div className="switch" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="tableBody-cell">
|
||||
<div className="tw-flex">
|
||||
<span onClick={() => setEditingRule({ rule, state: true })}>
|
||||
<SVGIcons
|
||||
alt="icon-edit"
|
||||
className="tw-cursor-pointer"
|
||||
icon="icon-edit"
|
||||
title="Edit"
|
||||
width="12"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
onClick={() => setDeletingRule({ rule, state: true })}>
|
||||
<SVGIcons
|
||||
alt="icon-delete"
|
||||
className="tw-ml-4 tw-cursor-pointer"
|
||||
icon="icon-delete"
|
||||
title="Delete"
|
||||
width="12"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getRoleUsers = (users: Array<EntityReference>) => {
|
||||
if (!users.length) {
|
||||
return (
|
||||
<div className="tw-text-center tw-py-5">
|
||||
<p className="tw-text-base">No Users Added.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tw-grid tw-grid-cols-4 tw-gap-x-2">
|
||||
{users.map((user) => (
|
||||
<UserCard
|
||||
isIconVisible
|
||||
item={{
|
||||
description: user.displayName as string,
|
||||
name: user.name as string,
|
||||
id: user.id,
|
||||
}}
|
||||
key={user.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentRole) {
|
||||
fetchPolicy(currentRole?.policy?.id as string);
|
||||
}
|
||||
}, [currentRole]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{error ? (
|
||||
<ErrorPlaceHolder />
|
||||
) : (
|
||||
<PageContainerV1 className="tw-py-4">
|
||||
<PageLayout leftPanel={fetchLeftPanel()}>
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<div className="tw-pb-3" data-testid="role-container">
|
||||
{roles.length > 0 ? (
|
||||
<>
|
||||
<div
|
||||
className="tw-flex tw-justify-between tw-items-center"
|
||||
data-testid="header">
|
||||
<div className="tw-heading tw-text-link tw-text-base">
|
||||
{currentRole?.displayName}
|
||||
</div>
|
||||
<NonAdminAction
|
||||
position="bottom"
|
||||
title={TITLE_FOR_NON_ADMIN_ACTION}>
|
||||
<Button
|
||||
className={classNames('tw-h-8 tw-rounded tw-mb-3', {
|
||||
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
|
||||
})}
|
||||
data-testid="add-new-user-button"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setErrorData(undefined);
|
||||
setIsAddingRule(true);
|
||||
}}>
|
||||
Add new rule
|
||||
</Button>
|
||||
</NonAdminAction>
|
||||
</div>
|
||||
<div
|
||||
className="tw-mb-3 tw--ml-5"
|
||||
data-testid="description-container">
|
||||
<Description
|
||||
description={currentRole?.description || ''}
|
||||
entityName={currentRole?.displayName}
|
||||
isEdit={isEditable}
|
||||
onCancel={onCancel}
|
||||
onDescriptionEdit={onDescriptionEdit}
|
||||
onDescriptionUpdate={onDescriptionUpdate}
|
||||
/>
|
||||
</div>
|
||||
{getTabs()}
|
||||
{currentTab === 1 ? (
|
||||
<Fragment>
|
||||
{isLoadingPolicy ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<>{getPolicyRules(currentPolicy?.rules ?? [])}</>
|
||||
)}
|
||||
</Fragment>
|
||||
) : null}
|
||||
{currentTab === 2
|
||||
? getRoleUsers(currentRole?.users ?? [])
|
||||
: null}
|
||||
</>
|
||||
) : (
|
||||
<ErrorPlaceHolder>
|
||||
<p className="w-text-lg tw-text-center">No Roles Added.</p>
|
||||
<p className="w-text-lg tw-text-center">
|
||||
<NonAdminAction
|
||||
position="bottom"
|
||||
title={TITLE_FOR_NON_ADMIN_ACTION}>
|
||||
<button
|
||||
className="link-text tw-underline"
|
||||
onClick={() => {
|
||||
setErrorData(undefined);
|
||||
setIsAddingRole(true);
|
||||
}}>
|
||||
Click here
|
||||
</button>
|
||||
{' to add new Role'}
|
||||
</NonAdminAction>
|
||||
</p>
|
||||
</ErrorPlaceHolder>
|
||||
)}
|
||||
{isAddingRole && (
|
||||
<FormModal
|
||||
errorData={errorData}
|
||||
form={Form}
|
||||
header="Adding new role"
|
||||
initialData={{
|
||||
name: '',
|
||||
description: '',
|
||||
displayName: '',
|
||||
}}
|
||||
onCancel={() => setIsAddingRole(false)}
|
||||
onChange={(data) => onNewDataChange(data as Role)}
|
||||
onSave={(data) => createNewRole(data as Role)}
|
||||
/>
|
||||
)}
|
||||
{isAddingRule && (
|
||||
<AddRuleModal
|
||||
errorData={errorData}
|
||||
header={`Adding new rule for ${toLower(
|
||||
currentRole?.displayName
|
||||
)}`}
|
||||
initialData={
|
||||
{ name: '', operation: '' as Operation } as Rule
|
||||
}
|
||||
onCancel={() => setIsAddingRule(false)}
|
||||
onChange={(data) => validateRuleData(data as Rule)}
|
||||
onSave={createRule}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editingRule.state && (
|
||||
<AddRuleModal
|
||||
isEditing
|
||||
header={`Edit rule ${editingRule.rule?.name}`}
|
||||
initialData={editingRule.rule as Rule}
|
||||
onCancel={() =>
|
||||
setEditingRule({ rule: undefined, state: false })
|
||||
}
|
||||
onSave={onRuleUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deletingRule.state && (
|
||||
<ConfirmationModal
|
||||
bodyText={`Are you sure want to delete ${deletingRule.rule?.name}?`}
|
||||
cancelText="Cancel"
|
||||
confirmText="Confirm"
|
||||
header="Deleting rule"
|
||||
onCancel={() =>
|
||||
setDeletingRule({ rule: undefined, state: false })
|
||||
}
|
||||
onConfirm={() => {
|
||||
deleteRule(deletingRule.rule as Rule);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageLayout>
|
||||
</PageContainerV1>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(RolesPage);
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Rule } from '../../generated/entity/policies/accessControl/rule';
|
||||
|
||||
export interface Policy {
|
||||
id: string;
|
||||
name: string;
|
||||
fullyQualifiedName: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
href: string;
|
||||
policyType: string;
|
||||
enabled: boolean;
|
||||
version: number;
|
||||
updatedAt: number;
|
||||
updatedBy: string;
|
||||
rules: Rule[];
|
||||
deleted: boolean;
|
||||
}
|
@ -25,6 +25,7 @@ import ExplorePage from '../pages/explore/ExplorePage.component';
|
||||
import IngestionPage from '../pages/IngestionPage/IngestionPage.component';
|
||||
import MyDataPage from '../pages/MyDataPage/MyDataPage.component';
|
||||
import PipelineDetailsPage from '../pages/PipelineDetails/PipelineDetailsPage.component';
|
||||
import RolesPage from '../pages/RolesPage/RolesPage.component';
|
||||
import ServicePage from '../pages/service';
|
||||
import ServicesPage from '../pages/services';
|
||||
import SignupPage from '../pages/signup';
|
||||
@ -89,7 +90,10 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||
<Route exact component={EntityVersionPage} path={ROUTES.ENTITY_VERSION} />
|
||||
<Route exact component={IngestionPage} path={ROUTES.INGESTION} />
|
||||
{isAuthDisabled || isAdminUser ? (
|
||||
<Route exact component={UserListPage} path={ROUTES.USER_LIST} />
|
||||
<>
|
||||
<Route exact component={RolesPage} path={ROUTES.ROLES} />
|
||||
<Route exact component={UserListPage} path={ROUTES.USER_LIST} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Redirect to={ROUTES.NOT_FOUND} />
|
||||
|
@ -205,7 +205,7 @@
|
||||
}
|
||||
.tw-notification {
|
||||
@apply tw-bg-white tw-transition tw-duration-300 tw-ease-linear tw-relative tw-flex tw-justify-between tw-pointer-events-auto
|
||||
tw-mb-3.5 tw-p-4 tw-w-80 tw-h-auto tw-rounded tw-shadow tw-text-white tw-bg-left-top tw-bg-no-repeat;
|
||||
tw-mb-3.5 tw-p-4 tw-w-96 tw-h-auto tw-rounded tw-shadow tw-text-white tw-bg-left-top tw-bg-no-repeat;
|
||||
}
|
||||
/* Toaster CSS end */
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user