ACL to firewall rules (#163)

ACL based on the firewall rules
This commit is contained in:
Givi Khojanashvili
2023-06-01 10:06:08 +04:00
committed by GitHub
parent 6cadce1598
commit 53ed514803
12 changed files with 947 additions and 368 deletions

View File

@@ -1,7 +1,7 @@
import React, {useEffect, useRef, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {actions as ruleActions} from '../store/rule';
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "typesafe-actions";
import { actions as policyActions } from '../store/policy';
import {
Button,
Col,
@@ -13,98 +13,123 @@ import {
RadioChangeEvent,
Row,
Select,
SelectProps,
Space,
Switch,
Tag,
Typography
} from "antd";
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
import type {CustomTagProps} from 'rc-select/lib/BaseSelect'
import {Rule, RuleToSave} from "../store/rule/types";
import {uniq} from "lodash"
import {Header} from "antd/es/layout/layout";
import {RuleObject} from "antd/lib/form";
import {useGetTokenSilently} from "../utils/token";
import { CloseOutlined, FlagFilled, QuestionCircleFilled } from "@ant-design/icons";
import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
import { Policy, PolicyToSave } from "../store/policy/types";
import { uniq } from "lodash";
import { Header } from "antd/es/layout/layout";
import { RuleObject } from "antd/lib/form";
import { useGetTokenSilently } from "../utils/token";
const {Paragraph} = Typography;
const {Option} = Select;
const { Paragraph } = Typography;
const { Option } = Select;
interface FormRule extends Rule {
interface FormPolicy {
id?: string
name: string
description: string
enabled: boolean
query: string
bidirectional: boolean
protocol: string
ports: string[]
action: string
tagSourceGroups: string[]
tagDestinationGroups: string[]
}
const AccessControlNew = () => {
const {getTokenSilently} = useGetTokenSilently()
const { getTokenSilently } = useGetTokenSilently()
const dispatch = useDispatch()
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible)
const setupNewPolicyVisible = useSelector((state: RootState) => state.policy.setupNewPolicyVisible)
const groups = useSelector((state: RootState) => state.group.data)
const rule = useSelector((state: RootState) => state.rule.rule)
const savedRule = useSelector((state: RootState) => state.rule.savedRule)
const actions: SelectProps['options'] = [
{ label: 'Accept', value: 'accept' },
{ label: 'Drop', value: 'drop' },
]
const protocols: SelectProps['options'] = [
{ label: 'All', value: 'all' },
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
{ label: 'ICMP', value: 'icmp' },
]
const policy = useSelector((state: RootState) => state.policy.policy)
const savedPolicy = useSelector((state: RootState) => state.policy.savedPolicy)
const [editName, setEditName] = useState(false)
const [editDescription, setEditDescription] = useState(false)
const [tagGroups, setTagGroups] = useState([] as string[])
const [formRule, setFormRule] = useState({} as FormRule)
const [formPolicy, setFormPolicy] = useState({} as FormPolicy)
const [form] = Form.useForm()
const inputNameRef = useRef<any>(null)
const inputDescriptionRef = useRef<any>(null)
const optionsDisabledEnabled = [{label: 'Enabled', value: false}, {label: 'Disabled', value: true}]
const optionsStatusEnabled = [{ label: 'Enabled', value: true }, { label: 'Disabled', value: false }]
useEffect(() => { if (editName) inputNameRef.current!.focus({ cursor: 'end' }) }, [editName])
useEffect(() => { if (editDescription) inputDescriptionRef.current!.focus({ cursor: 'end' }) }, [editDescription])
useEffect(() => { setTagGroups(groups?.map(g => g.name) || []) }, [groups])
useEffect(() => {
if (editName) inputNameRef.current!.focus({
cursor: 'end',
});
}, [editName]);
if (!policy) return
const fPolicy = {
id: policy.id,
name: policy.name,
description: policy.description,
enabled: policy.enabled,
query: '',
bidirectional: policy.rules[0].bidirectional,
protocol: policy.rules[0].protocol,
ports: policy.rules[0].ports,
action: policy.rules[0].action,
tagSourceGroups: policy.rules[0].sources ? policy.rules[0].sources?.map(t => t.name) : [],
tagDestinationGroups: policy.rules[0].destinations ? policy.rules[0].destinations?.map(t => t.name) : [],
} as FormPolicy
setFormPolicy(fPolicy)
form.setFieldsValue(fPolicy)
}, [policy, form])
useEffect(() => {
if (editDescription) inputDescriptionRef.current!.focus({
cursor: 'end',
});
}, [editDescription]);
useEffect(() => {
if (!rule) return
const fRule = {
...rule,
tagSourceGroups: rule.sources ? rule.sources?.map(t => t.name) : [],
tagDestinationGroups: rule.destinations ? rule.destinations?.map(t => t.name) : []
} as FormRule
setFormRule(fRule)
form.setFieldsValue(fRule)
}, [rule])
useEffect(() => {
setTagGroups(groups?.map(g => g.name) || [])
}, [groups])
const createRuleToSave = (): RuleToSave => {
const sources = groups?.filter(g => formRule.tagSourceGroups.includes(g.name)).map(g => g.id || '') || []
const destinations = groups?.filter(g => formRule.tagDestinationGroups.includes(g.name)).map(g => g.id || '') || []
const sourcesNoId = formRule.tagSourceGroups.filter(s => !tagGroups.includes(s))
const destinationsNoId = formRule.tagDestinationGroups.filter(s => !tagGroups.includes(s))
const createPolicyToSave = (): PolicyToSave => {
const sources = groups?.filter(g => formPolicy.tagSourceGroups.includes(g.name)).map(g => g.id || '') || []
const destinations = groups?.filter(g => formPolicy.tagDestinationGroups.includes(g.name)).map(g => g.id || '') || []
const sourcesNoId = formPolicy.tagSourceGroups.filter(s => !tagGroups.includes(s))
const destinationsNoId = formPolicy.tagDestinationGroups.filter(s => !tagGroups.includes(s))
const groupsToSave = uniq([...sourcesNoId, ...destinationsNoId])
return {
id: formRule.id,
name: formRule.name,
description: formRule.description,
sources,
destinations,
id: formPolicy.id,
name: formPolicy.name,
description: formPolicy.description,
enabled: formPolicy.enabled,
sourcesNoId,
destinationsNoId,
groupsToSave,
flow: formRule.flow,
disabled: formRule.disabled
} as RuleToSave
rules: [{
id: formPolicy.id,
name: formPolicy.name,
description: formPolicy.description,
enabled: formPolicy.enabled,
sources,
destinations,
bidirectional: formPolicy.bidirectional,
protocol: formPolicy.protocol,
ports: formPolicy.ports,
action: 'accept',
}],
} as PolicyToSave
}
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
const ruleToSave = createRuleToSave()
dispatch(ruleActions.saveRule.request({
.then((_) => {
const policyToSave = createPolicyToSave()
dispatch(policyActions.savePolicy.request({
getAccessTokenSilently: getTokenSilently,
payload: ruleToSave
payload: policyToSave
}))
})
.catch((errorInfo) => {
@@ -113,50 +138,82 @@ const AccessControlNew = () => {
};
const setVisibleNewRule = (status: boolean) => {
dispatch(ruleActions.setSetupNewRuleVisible(status));
dispatch(policyActions.setSetupNewPolicyVisible(status));
}
const onCancel = () => {
if (savedRule.loading) return
if (savedPolicy.loading) return
setEditName(false)
dispatch(ruleActions.setRule({
dispatch(policyActions.setPolicy({
name: '',
description: '',
sources: [],
destinations: [],
flow: 'bidirect',
disabled: false
} as Rule))
enabled: true,
query: '',
rules: [{
name: '',
description: '',
enabled: true,
sources: [],
destinations: [],
bidirectional: true,
protocol: 'all',
ports: [],
action: 'accept',
}],
} as Policy))
setVisibleNewRule(false)
}
const onChange = (data: any) => {
setFormRule({...formRule, ...data})
setFormPolicy({ ...formPolicy, ...data })
}
const handleChangeSource = (value: string[]) => {
setFormRule({
...formRule,
setFormPolicy({
...formPolicy,
tagSourceGroups: value
})
};
}
const handleChangeDestination = (value: string[]) => {
setFormRule({
...formRule,
setFormPolicy({
...formPolicy,
tagDestinationGroups: value
})
};
}
const handleChangeDisabled = ({target: {value}}: RadioChangeEvent) => {
setFormRule({
...formRule,
disabled: value
const handleChangeProtocol = (value: string) => {
setFormPolicy({
...formPolicy,
bidirectional: (value === 'all' || value === 'icmp') ? true : formPolicy.bidirectional,
ports: (value === 'all' || value === 'icmp') ? [] : formPolicy.ports,
protocol: value
})
}
const handleChangePorts = (value: string[]) => {
setFormPolicy({
...formPolicy,
ports: value
})
}
const handleChangeDisabled = ({ target: { value } }: RadioChangeEvent) => {
setFormPolicy({
...formPolicy,
enabled: value
})
}
const handleChangeBidirect = (checked: boolean) => {
setFormPolicy({
...formPolicy,
bidirectional: checked,
})
};
const tagRender = (props: CustomTagProps) => {
const {label, value, closable, onClose} = props;
const { value, closable, onClose } = props;
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
@@ -168,7 +225,7 @@ const AccessControlNew = () => {
onMouseDown={onPreventMouseDown}
closable={closable}
onClose={onClose}
style={{marginRight: 3}}
style={{ marginRight: 3 }}
>
<strong>{value}</strong>
</Tag>
@@ -183,28 +240,47 @@ const AccessControlNew = () => {
<>
<Tag
color="blue"
style={{marginRight: 3}}
style={{ marginRight: 3 }}
>
<strong>{label}</strong>
</Tag>
<span style={{fontSize: ".85em"}}>{peersCount}</span>
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
</>
)
}
const dropDownRender = (menu: React.ReactElement) => (
const dropDownRenderGroups = (menu: React.ReactElement) => (
<>
{menu}
<Divider style={{margin: '8px 0'}}/>
<Row style={{padding: '0 8px 4px'}}>
<Divider style={{ margin: '8px 0' }} />
<Row style={{ padding: '0 8px 4px' }}>
<Col flex="auto">
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
<span style={{ color: "#9CA3AF" }}>Add new group by pressing "Enter"</span>
</Col>
<Col flex="none">
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
fill="#9CA3AF"/>
fill="#9CA3AF" />
</svg>
</Col>
</Row>
</>
)
const dropDownRenderPorts = (menu: React.ReactElement) => (
<>
{menu}
<Divider style={{ margin: '8px 0' }} />
<Row style={{ padding: '0 8px 4px' }}>
<Col flex="auto">
<span style={{ color: "#9CA3AF" }}>Add new ports or range by pressing "Enter"</span>
</Col>
<Col flex="none">
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
fill="#9CA3AF" />
</svg>
</Col>
</Row>
@@ -231,7 +307,7 @@ const AccessControlNew = () => {
return Promise.reject(new Error("Please enter at least one group"))
}
value.forEach(function (v: string) {
value.forEach(function(v: string) {
if (!v.trim().length) {
hasSpaceNamed.push(v)
}
@@ -244,47 +320,69 @@ const AccessControlNew = () => {
return Promise.resolve()
}
const selectPortRangeValidator = (_: RuleObject, value: string[]) => {
if (value) {
var failed = false
value.forEach(function(v: string) {
let p = Number(v)
if (Number.isNaN(p) || p < 1 || p > 65535) {
failed = true
return
}
})
if (failed) {
return Promise.reject(new Error("Port value must be in 1..65535 range"))
}
}
return Promise.resolve()
}
const selectPortProtocolValidator = (_: RuleObject, value: string[]) => {
if (!formPolicy.bidirectional && value.length === 0) {
return Promise.reject(new Error("Directional traffic require ports"))
}
return Promise.resolve()
}
return (
<>
{rule &&
{policy &&
<Drawer
//title={`${formRule.ID ? 'Edit Rule' : 'New Rule'}`}
headerStyle={{display: "none"}}
headerStyle={{ display: "none" }}
forceRender={true}
// width={512}
visible={setupNewRuleVisible}
bodyStyle={{paddingBottom: 80}}
visible={setupNewPolicyVisible}
bodyStyle={{ paddingBottom: 80 }}
onClose={onCancel}
autoFocus={true}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button onClick={onCancel} disabled={savedRule.loading}>Cancel</Button>
<Button type="primary" disabled={savedRule.loading}
onClick={handleFormSubmit}>{`${formRule.id ? 'Save' : 'Create'}`}</Button>
<Space style={{ display: 'flex', justifyContent: 'end' }}>
<Button onClick={onCancel} disabled={savedPolicy.loading}>Cancel</Button>
<Button type="primary" disabled={savedPolicy.loading}
onClick={handleFormSubmit}>{`${formPolicy.id ? 'Save' : 'Create'}`}</Button>
</Space>
}
>
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
<Row gutter={16}>
<Col span={24}>
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
<Header style={{ margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px" }}>
<Row align="top">
<Col flex="none" style={{display: "flex"}}>
{!editName && !editDescription && formRule.id &&
<Col flex="none" style={{ display: "flex" }}>
{!editName && !editDescription && formPolicy.id &&
<button type="button" aria-label="Close" className="ant-drawer-close"
style={{paddingTop: 3}}
onClick={onCancel}>
style={{ paddingTop: 3 }}
onClick={onCancel}>
<span role="img" aria-label="close"
className="anticon anticon-close">
<CloseOutlined size={16}/>
className="anticon anticon-close">
<CloseOutlined size={16} />
</span>
</button>
}
</Col>
<Col flex="auto">
{!editName && formRule.id ? (
{!editName && formPolicy.id ? (
<div className={"access-control input-text ant-drawer-title"}
onClick={() => toggleEditName(true)}>{formRule.id ? formRule.name : 'New Rule'}</div>
onClick={() => toggleEditName(true)}>{formPolicy.id ? formPolicy.name : 'New Rule'}</div>
) : (
<Form.Item
name="name"
@@ -296,25 +394,25 @@ const AccessControlNew = () => {
}]}
>
<Input placeholder="Add rule name..." ref={inputNameRef}
onPressEnter={() => toggleEditName(false)}
onBlur={() => toggleEditName(false)} autoComplete="off"/>
onPressEnter={() => toggleEditName(false)}
onBlur={() => toggleEditName(false)} autoComplete="off" />
</Form.Item>
)}
{!editDescription ? (
<div className={"access-control input-text ant-drawer-subtitle"}
onClick={() => toggleEditDescription(true)}>
{formRule.description && formRule.description.trim() !== "" ? formRule.description : 'Add description...'}
onClick={() => toggleEditDescription(true)}>
{formPolicy.description && formPolicy.description.trim() !== "" ? formPolicy.description : 'Add description...'}
</div>
) : (
<Form.Item
name="description"
label="Description"
style={{marginTop: 24}}
style={{ marginTop: 24 }}
>
<Input placeholder="Add description..." ref={inputDescriptionRef}
onPressEnter={() => toggleEditDescription(false)}
onBlur={() => toggleEditDescription(false)}
autoComplete="off"/>
onPressEnter={() => toggleEditDescription(false)}
onBlur={() => toggleEditDescription(false)}
autoComplete="off" />
</Form.Item>
)}
</Col>
@@ -332,15 +430,16 @@ const AccessControlNew = () => {
</Col>
<Col span={24}>
<Form.Item
name="disabled"
name="enabled"
label="Status"
>
<Radio.Group
options={optionsDisabledEnabled}
options={optionsStatusEnabled}
onChange={handleChangeDisabled}
optionType="button"
buttonStyle="solid"
defaultValue={false}
/>
</Form.Item>
</Col>
@@ -348,14 +447,14 @@ const AccessControlNew = () => {
<Form.Item
name="tagSourceGroups"
label="Source groups"
rules={[{validator: selectValidator}]}
rules={[{ validator: selectValidator }]}
>
<Select mode="tags"
style={{width: '100%'}}
placeholder="Tags Mode"
tagRender={tagRender}
onChange={handleChangeSource}
dropdownRender={dropDownRender}
style={{ width: '100%' }}
placeholder="Tags Mode"
tagRender={tagRender}
onChange={handleChangeSource}
dropdownRender={dropDownRenderGroups}
>
{
tagGroups.map(m =>
@@ -365,18 +464,32 @@ const AccessControlNew = () => {
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="bidirectional"
label="Bi-Direct traffic flow"
tooltip="Protocol type 'All' or 'ICMP' must be bi-directional. Directional traffic for TCP and UDP protocol requires at least one port to be defined."
>
<Switch
size={"small"}
disabled={formPolicy.protocol === "all" || formPolicy.protocol === "icmp"}
checked={formPolicy.bidirectional}
onChange={handleChangeBidirect}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="tagDestinationGroups"
label="Destination groups"
rules={[{validator: selectValidator}]}
rules={[{ validator: selectValidator }]}
>
<Select
mode="tags" style={{width: '100%'}}
mode="tags" style={{ width: '100%' }}
placeholder="Tags Mode"
tagRender={tagRender}
onChange={handleChangeDestination}
dropdownRender={dropDownRender}
dropdownRender={dropDownRenderGroups}
>
{
tagGroups.map(m =>
@@ -386,22 +499,71 @@ const AccessControlNew = () => {
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="protocol"
label="Protocol"
>
<Select
style={{ width: '100%' }}
options={protocols}
onChange={handleChangeProtocol}
defaultValue={'all'}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="ports"
label="Ports"
rules={[
{ message: "Directional traffic requires at least one port",
validator: selectPortProtocolValidator, required: false },
{ message: "Port value must be in 1..65535 range",
validator: selectPortRangeValidator, required: false },
]}
>
<Select
mode="tags" style={{ width: '100%' }}
placeholder="Tags Mode"
tagRender={tagRender}
onChange={handleChangePorts}
dropdownRender={dropDownRenderPorts}
disabled={formPolicy.protocol === "all" || formPolicy.protocol === "icmp"}
>
{
formPolicy &&
formPolicy.ports?.map(m =>
<Option key={m}>
<Tag
color="blue"
style={{ marginRight: 3 }}
>
<strong>{m}</strong>
</Tag>
</Option>
)
}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Row wrap={false} gutter={12}>
<Col flex="none">
<FlagFilled/>
<FlagFilled />
</Col>
<Col flex="auto">
<Paragraph>
At the moment access rules are bi-directional by default, this means both
source and destination can talk to each-other in both directions. However
destination peers will not be able to communicate with each other, nor will
the source peers.
The default behavior is to drop all traffic that doesn't match an Access control rule.
</Paragraph>
<Paragraph>
If you want to enable all peers of the same group to talk to each other -
you can add that group both as a receiver and as a destination.
</Paragraph>
<Paragraph>
Protocol type <strong>All</strong> or <strong>ICMP</strong> must be bi-directional.
Directional traffic for <strong>TCP</strong> and <strong>UDP</strong> protocol requires at least one port to be defined.
</Paragraph>
</Col>
</Row>
</Col>
@@ -419,4 +581,4 @@ const AccessControlNew = () => {
)
}
export default AccessControlNew
export default AccessControlNew

View File

@@ -5,6 +5,7 @@ import { composeWithDevTools } from 'redux-devtools-extension';
import { sagas as peerSagas } from './peer';
import { sagas as setupKeySagas } from './setup-key';
import { sagas as userSagas } from './user';
import { sagas as policySagas } from './policy';
import { sagas as ruleSagas } from './rule';
import { sagas as groupSagas } from './group';
import { sagas as routeSagas } from './route';
@@ -28,6 +29,7 @@ sagaMiddleware.run(peerSagas);
sagaMiddleware.run(setupKeySagas);
sagaMiddleware.run(userSagas);
sagaMiddleware.run(ruleSagas);
sagaMiddleware.run(policySagas);
sagaMiddleware.run(groupSagas);
sagaMiddleware.run(routeSagas);
sagaMiddleware.run(nameserverGroupSagas);
@@ -36,4 +38,4 @@ sagaMiddleware.run(dnsSettingsSagas);
sagaMiddleware.run(accountSagas);
sagaMiddleware.run(personalAccessTokenSagas);
export { apiClient, rootReducer, store };
export { apiClient, rootReducer, store };

View File

@@ -0,0 +1,34 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import { Policy, PolicyToSave } from './types';
import { ApiError, CreateResponse, DeleteResponse, RequestPayload } from '../../services/api-client/types';
const actions = {
getPolicies: createAsyncAction(
'GET_POLICIES_REQUEST',
'GET_POLICIES_SUCCESS',
'GET_POLICIES_FAILURE',
)<RequestPayload<null>, Policy[], ApiError>(),
savePolicy: createAsyncAction(
'SAVE_POLICY_REQUEST',
'SAVE_POLICY_SUCCESS',
'SAVE_POLICY_FAILURE',
)<RequestPayload<PolicyToSave>, CreateResponse<Policy | null>, CreateResponse<Policy | null>>(),
setSavedPolicy: createAction('SET_CREATE_POLICY')<CreateResponse<Policy | null>>(),
resetSavedPolicy: createAction('RESET_CREATE_POLICY')<null>(),
deletePolicy: createAsyncAction(
'DELETE_POLICY_REQUEST',
'DELETE_POLICY_SUCCESS',
'DELETE_POLICY_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletedPolicy: createAction('SET_DELETED_POLICY')<DeleteResponse<string | null>>(),
resetDeletedPolicy: createAction('RESET_DELETED_POLICY')<null>(),
removePolicy: createAction('REMOVE_POLICY')<string>(),
setPolicy: createAction('SET_POLICY')<Policy>(),
setSetupNewPolicyVisible: createAction('SET_SETUP_NEW_POLICY_VISIBLE')<boolean>()
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;

View File

@@ -0,0 +1,7 @@
import actions, { ActionTypes as _actionTypes } from './actions';
import reducer from './reducer';
import sagas from './sagas';
export type ActionTypes = _actionTypes;
export { actions, reducer, sagas };

View File

@@ -0,0 +1,89 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Policy } from './types';
import actions, { ActionTypes } from './actions';
import { ApiError, DeleteResponse, CreateResponse } from "../../services/api-client/types";
type StateType = Readonly<{
data: Policy[] | null;
policy: Policy | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deletePolicy: DeleteResponse<string | null>;
savedPolicy: CreateResponse<Policy | null>;
setupNewPolicyVisible: boolean
}>;
const initialState: StateType = {
data: [],
policy: null,
loading: false,
failed: null,
saving: false,
deletePolicy: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data: null
},
savedPolicy: <CreateResponse<Policy | null>>{
loading: false,
success: false,
failure: false,
error: null,
data: null
},
setupNewPolicyVisible: false
};
const data = createReducer<Policy[], ActionTypes>(initialState.data as Policy[])
.handleAction(actions.getPolicies.success, (_, action) => action.payload)
.handleAction(actions.getPolicies.failure, () => []);
const policy = createReducer<Policy, ActionTypes>(initialState.policy as Policy)
.handleAction(actions.setPolicy, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getPolicies.request, () => true)
.handleAction(actions.getPolicies.success, () => false)
.handleAction(actions.getPolicies.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getPolicies.request, () => null)
.handleAction(actions.getPolicies.success, () => null)
.handleAction(actions.getPolicies.failure, (store, action) => action.payload);
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
.handleAction(actions.getPolicies.request, () => true)
.handleAction(actions.getPolicies.success, () => false)
.handleAction(actions.getPolicies.failure, () => false);
const deletedPolicy = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletePolicy)
.handleAction(actions.deletePolicy.request, () => initialState.deletePolicy)
.handleAction(actions.deletePolicy.success, (store, action) => action.payload)
.handleAction(actions.deletePolicy.failure, (store, action) => action.payload)
.handleAction(actions.setDeletedPolicy, (store, action) => action.payload)
.handleAction(actions.resetDeletedPolicy, () => initialState.deletePolicy)
const savedPolicy = createReducer<CreateResponse<Policy | null>, ActionTypes>(initialState.savedPolicy)
.handleAction(actions.savePolicy.request, () => initialState.savedPolicy)
.handleAction(actions.savePolicy.success, (store, action) => action.payload)
.handleAction(actions.savePolicy.failure, (store, action) => action.payload)
.handleAction(actions.setSavedPolicy, (store, action) => action.payload)
.handleAction(actions.resetSavedPolicy, () => initialState.savedPolicy)
const setupNewPolicyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewPolicyVisible)
.handleAction(actions.setSetupNewPolicyVisible, (store, action) => action.payload)
export default combineReducers({
data,
policy,
loading,
failed,
saving,
deletedPolicy,
savedPolicy,
setupNewPolicyVisible
});

167
src/store/policy/sagas.ts Normal file
View File

@@ -0,0 +1,167 @@
import { all, call, put, select, takeLatest } from 'redux-saga/effects';
import { ApiError, ApiResponse, CreateResponse, DeleteResponse } from '../../services/api-client/types';
import { Policy, PolicyRule } from './types'
import service from './service';
import serviceGroup from '../group/service';
import actions from './actions';
import { actions as groupActions } from '../group';
import { Group } from "../group/types";
export function* getPolicies(action: ReturnType<typeof actions.getPolicies.request>): Generator {
try {
yield put(actions.setDeletedPolicy({
loading: false,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>))
const effect = yield call(service.getPolicies, action.payload);
const response = effect as ApiResponse<Policy[]>;
yield put(actions.getPolicies.success(response.body));
} catch (err) {
yield put(actions.getPolicies.failure(err as ApiError));
}
}
export function* setCreatedPolicy(action: ReturnType<typeof actions.setSavedPolicy>): Generator {
yield put(actions.setSavedPolicy(action.payload))
}
function getNewGroupIds(dataString: string[], responses: Group[]): string[] {
return responses.filter(r => dataString.includes(r.name)).map(r => r.id || '')
}
export function* savePolicy(action: ReturnType<typeof actions.savePolicy.request>): Generator {
try {
yield put(actions.setSavedPolicy({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<Policy | null>))
const policyToSave = action.payload.payload
const responsesGroup = yield all(policyToSave.groupsToSave.map(g => call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: { name: g }
})
))
const resGroups = (responsesGroup as ApiResponse<Policy>[]).filter(r => r.statusCode === 200).map(r => (r.body as Group))
const currentGroups = [...(yield select(state => state.group.data)) as Policy[]]
const newGroups = [...currentGroups, ...resGroups]
yield put(groupActions.getGroups.success(newGroups));
const newSources = getNewGroupIds(policyToSave.sourcesNoId, resGroups)
const newDestinations = getNewGroupIds(policyToSave.destinationsNoId, resGroups)
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {
name: policyToSave.name,
description: policyToSave.description,
enabled: policyToSave.enabled,
query: policyToSave.query
} as Policy
}
if (policyToSave.rules.length > 0) {
payloadToSave.payload.rules = []
}
policyToSave.rules.forEach((r) => {
payloadToSave.payload.rules.push({
name: r.name,
description: r.description,
enabled: r.enabled,
sources: [...r.sources as string[], ...newSources],
destinations: [...r.destinations as string[], ...newDestinations],
bidirectional: r.bidirectional,
protocol: r.protocol,
ports: r.ports,
action: r.action
} as PolicyRule)
})
let effect
if (!policyToSave.id) {
effect = yield call(service.createPolicy, payloadToSave);
} else {
payloadToSave.payload.id = policyToSave.id
effect = yield call(service.editPolicy, payloadToSave);
}
const response = effect as ApiResponse<Policy>;
yield put(actions.savePolicy.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<Policy | null>));
yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(actions.getPolicies.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
} catch (err) {
yield put(actions.savePolicy.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as CreateResponse<Policy | null>));
}
}
export function* setDeletePolicy(action: ReturnType<typeof actions.setDeletedPolicy>): Generator {
yield put(actions.setDeletedPolicy(action.payload))
}
export function* deletePolicy(action: ReturnType<typeof actions.deletePolicy.request>): Generator {
try {
yield call(actions.setDeletedPolicy, {
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
const effect = yield call(service.deletedPolicy, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deletePolicy.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const policies = (yield select(state => state.policy.data)) as Policy[]
yield put(actions.getPolicies.success(policies.filter((p: Policy) => p.id !== action.payload.payload)))
} catch (err) {
yield put(actions.deletePolicy.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getPolicies.request, getPolicies),
takeLatest(actions.savePolicy.request, savePolicy),
takeLatest(actions.deletePolicy.request, deletePolicy)
]);
}

View File

@@ -0,0 +1,32 @@
import { ApiResponse, RequestPayload } from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import { Policy } from './types';
export default {
async getPolicies(payload: RequestPayload<null>): Promise<ApiResponse<Policy[]>> {
return apiClient.get<Policy[]>(
`/api/policies`,
payload
);
},
async deletedPolicy(payload: RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/policies/` + payload.payload,
payload
);
},
async createPolicy(payload: RequestPayload<Policy>): Promise<ApiResponse<Policy>> {
return apiClient.post<Policy>(
`/api/policies`,
payload
);
},
async editPolicy(payload: RequestPayload<Policy>): Promise<ApiResponse<Policy>> {
const id = payload.payload.id
delete payload.payload.id
return apiClient.put<Policy>(
`/api/policies/${id}`,
payload
);
},
};

29
src/store/policy/types.ts Normal file
View File

@@ -0,0 +1,29 @@
import { Group } from "../group/types";
export interface PolicyRule {
id?: string
name: string
description: string
enabled: boolean
sources: Group[] | string[] | null
destinations: Group[] | string[] | null
bidirectional: boolean
action: string
protocol: string
ports: string[]
}
export interface Policy {
id?: string
name: string
description: string
enabled: boolean
query: string
rules: PolicyRule[]
};
export interface PolicyToSave extends Policy {
sourcesNoId: string[],
destinationsNoId: string[],
groupsToSave: string[]
};

View File

@@ -5,6 +5,7 @@ import { reducer as setupKey } from './setup-key';
import { reducer as user } from './user';
import { reducer as group } from './group';
import { reducer as rule } from './rule';
import { reducer as policy } from './policy';
import { reducer as route } from './route';
import { reducer as nameserverGroup } from './nameservers';
import { reducer as event } from './event';
@@ -13,15 +14,16 @@ import { reducer as account } from './account';
import { reducer as personalAccessToken } from './personal-access-token';
export default combineReducers({
peer,
setupKey,
user,
group,
rule,
route,
nameserverGroup,
event,
dnsSettings,
account,
personalAccessToken
peer,
setupKey,
user,
group,
rule,
policy,
route,
nameserverGroup,
event,
dnsSettings,
account,
personalAccessToken
});

View File

@@ -1,33 +1,32 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { ApiResponse, RequestPayload } from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import { Rule } from './types';
import {SetupKey} from "../setup-key/types";
export default {
async getRules(payload:RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
return apiClient.get<Rule[]>(
`/api/rules`,
payload
);
},
async deletedRule(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/rules/` + payload.payload,
payload
);
},
async createRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
return apiClient.post<Rule>(
`/api/rules`,
payload
);
},
async editRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
const id = payload.payload.id
delete payload.payload.id
return apiClient.put<Rule>(
`/api/rules/${id}`,
payload
);
},
async getRules(payload: RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
return apiClient.get<Rule[]>(
`/api/rules`,
payload
);
},
async deletedRule(payload: RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/rules/` + payload.payload,
payload
);
},
async createRule(payload: RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
return apiClient.post<Rule>(
`/api/rules`,
payload
);
},
async editRule(payload: RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
const id = payload.payload.id
delete payload.payload.id
return apiClient.put<Rule>(
`/api/rules/${id}`,
payload
);
},
};

View File

@@ -1,4 +1,4 @@
import {Group} from "../group/types";
import { Group } from "../group/types";
export interface Rule {
id?: string
@@ -7,6 +7,8 @@ export interface Rule {
sources: Group[] | string[] | null
destinations: Group[] | string[] | null
flow: string
protocol: string
ports: string[]
disabled: boolean
}
@@ -14,4 +16,4 @@ export interface RuleToSave extends Rule {
sourcesNoId: string[],
destinationsNoId: string[],
groupsToSave: string[]
}
}

View File

@@ -1,4 +1,4 @@
import React, {useEffect, useState} from 'react';
import React, { useEffect, useState } from 'react';
import {
Alert,
Button,
@@ -23,68 +23,76 @@ import {
import {Container} from "../components/Container";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {Rule} from "../store/rule/types";
import {actions as ruleActions} from "../store/rule";
import {Policy} from "../store/policy/types";
import {actions as policyActions} from "../store/policy";
import {actions as groupActions} from "../store/group";
import {filter, sortBy} from "lodash";
import {CloseOutlined, EllipsisOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
import {EllipsisOutlined, ExclamationCircleOutlined } from "@ant-design/icons";
import bidirect from '../assets/direct_bi.svg';
import inbound from '../assets/direct_in.svg';
import outbound from '../assets/direct_out.svg';
import AccessControlNew from "../components/AccessControlNew";
import {Group} from "../store/group/types";
import { Group } from "../store/group/types";
import AccessControlModalGroups from "../components/AccessControlModalGroups";
import tableSpin from "../components/Spin";
import {useGetTokenSilently} from "../utils/token";
import {usePageSizeHelpers} from "../utils/pageSize";
import {PeerDataTable} from "../store/peer/types";
const {Title, Paragraph, Text} = Typography;
const {Column} = Table;
const {confirm} = Modal;
const { Title, Paragraph, Text } = Typography;
const { Column } = Table;
const { confirm } = Modal;
interface RuleDataTable extends Rule {
interface PolicyDataTable {
id?: string
key: string;
sourceCount: number;
sourceLabel: '';
destinationCount: number;
destinationLabel: '';
name: string
description: string
enabled: boolean
query: string
sources: string[]
destinations: string[]
bidirectional: boolean
protocol: string
ports: string[]
sourceCount: number
sourceLabel: ''
destinationCount: number
destinationLabel: ''
}
interface GroupsToShow {
title: string,
groups: Group[] | string[] | null,
title: string
groups: Group[] | string[] | null
modalVisible: boolean
}
export const AccessControl = () => {
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
const {getTokenSilently} = useGetTokenSilently()
const { onChangePageSize, pageSizeOptions, pageSize } = usePageSizeHelpers()
const { getTokenSilently } = useGetTokenSilently()
const dispatch = useDispatch()
const rules = useSelector((state: RootState) => state.rule.data);
const failed = useSelector((state: RootState) => state.rule.failed);
const loading = useSelector((state: RootState) => state.rule.loading);
const deletedRule = useSelector((state: RootState) => state.rule.deletedRule);
const savedRule = useSelector((state: RootState) => state.rule.savedRule);
const policies = useSelector((state: RootState) => state.policy.data);
const failed = useSelector((state: RootState) => state.policy.failed);
const loading = useSelector((state: RootState) => state.policy.loading);
const deletedPolicy = useSelector((state: RootState) => state.policy.deletedPolicy);
const savedPolicy = useSelector((state: RootState) => state.policy.savedPolicy);
const [showTutorial, setShowTutorial] = useState(true)
const [textToSearch, setTextToSearch] = useState('');
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
const [currentPage, setCurrentPage] = useState(1);
const [dataTable, setDataTable] = useState([] as RuleDataTable[]);
const [ruleToAction, setRuleToAction] = useState(null as RuleDataTable | null);
const [dataTable, setDataTable] = useState([] as PolicyDataTable[]);
const [policyToAction, setPolicyToAction] = useState(null as PolicyDataTable | null);
const [groupsToShow, setGroupsToShow] = useState({} as GroupsToShow)
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible);
const setupNewPolicyVisible = useSelector((state: RootState) => state.policy.setupNewPolicyVisible);
const [groupPopupVisible, setGroupPopupVisible] = useState("")
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}]
const optionsAllEnabled = [{ label: 'Enabled', value: 'enabled' }, { label: 'All', value: 'all' }]
const itemsMenuAction = [
{
key: "view",
label: (<Button type="text" block onClick={() => onClickViewRule()}>View</Button>)
label: (<Button type="text" block onClick={() => onClickViewPolicy()}>View</Button>)
},
// {
// key: "delete",
@@ -101,88 +109,97 @@ export const AccessControl = () => {
return (!data) ? "No group" : (data.length > 1) ? `${data.length} Groups` : (data.length === 1) ? data[0].name : "No group"
}
const isShowTutorial = (rules: Rule[]): boolean => {
return (!rules.length || (rules.length === 1 && rules[0].name === "Default"))
const isShowTutorial = (policy: Policy[]): boolean => {
return (!policy.length || (policy.length === 1 && policy[0].name === "Default"))
}
const transformDataTable = (d: Rule[]): RuleDataTable[] => {
return d.map(p => {
const sourceLabel = getSourceDestinationLabel(p.sources as Group[])
const destinationLabel = getSourceDestinationLabel(p.destinations as Group[])
const transformDataTable = (d: Policy[]): PolicyDataTable[] => {
return d.map(policy => {
const sourceLabel = getSourceDestinationLabel(policy.rules[0].sources as Group[])
const destinationLabel = getSourceDestinationLabel(policy.rules[0].destinations as Group[])
return {
key: p.id, ...p,
sourceCount: p.sources?.length,
id: policy.id,
key: policy.id,
name: policy.name,
description: policy.description,
enabled: policy.enabled,
sources: policy.rules[0].sources,
destinations: policy.rules[0].destinations,
bidirectional: policy.rules[0].bidirectional,
sourceCount: policy.rules[0].sources?.length,
sourceLabel,
destinationCount: p.destinations?.length,
destinationLabel
} as RuleDataTable
destinationCount: policy.rules[0].destinations?.length,
destinationLabel,
protocol: policy.rules[0].protocol,
ports: policy.rules[0].ports,
} as PolicyDataTable
})
}
useEffect(() => {
dispatch(ruleActions.getRules.request({getAccessTokenSilently: getTokenSilently, payload: null}));
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
dispatch(policyActions.getPolicies.request({ getAccessTokenSilently: getTokenSilently, payload: null }));
dispatch(groupActions.getGroups.request({ getAccessTokenSilently: getTokenSilently, payload: null }));
}, [])
useEffect(() => {
if (failed) {
setShowTutorial(false)
} else {
setShowTutorial(isShowTutorial(rules))
setShowTutorial(isShowTutorial(policies))
setDataTable(sortBy(transformDataTable(filterDataTable()), "name"))
}
}, [rules])
}, [policies])
useEffect(() => {
setDataTable(transformDataTable(filterDataTable()))
}, [textToSearch, optionAllEnable])
const styleNotification = {marginTop: 85}
const styleNotification = { marginTop: 85 }
const saveKey = 'saving';
useEffect(() => {
if (savedRule.loading) {
message.loading({content: 'Saving...', key: saveKey, duration: 0, style: styleNotification})
} else if (savedRule.success) {
if (savedPolicy.loading) {
message.loading({ content: 'Saving...', key: saveKey, duration: 0, style: styleNotification })
} else if (savedPolicy.success) {
message.success({
content: 'Rule has been successfully saved.',
key: saveKey,
duration: 2,
style: styleNotification
});
dispatch(ruleActions.setSetupNewRuleVisible(false))
dispatch(ruleActions.setSavedRule({...savedRule, success: false}))
dispatch(ruleActions.resetSavedRule(null))
} else if (savedRule.error) {
dispatch(policyActions.setSetupNewPolicyVisible(false))
dispatch(policyActions.setSavedPolicy({ ...savedPolicy, success: false }))
dispatch(policyActions.resetSavedPolicy(null))
} else if (savedPolicy.error) {
message.error({
content: 'Failed to update rule. You might not have enough permissions.',
key: saveKey,
duration: 2,
style: styleNotification
});
dispatch(ruleActions.setSavedRule({...savedRule, error: null}))
dispatch(ruleActions.resetSavedRule(null))
dispatch(policyActions.setSavedPolicy({ ...savedPolicy, error: null }))
dispatch(policyActions.resetSavedPolicy(null))
}
}, [savedRule])
}, [savedPolicy])
const deleteKey = 'deleting';
useEffect(() => {
const style = {marginTop: 85}
if (deletedRule.loading) {
message.loading({content: 'Deleting...', key: deleteKey, style})
} else if (deletedRule.success) {
message.success({content: 'Rule has been successfully disabled.', key: deleteKey, duration: 2, style})
dispatch(ruleActions.resetDeletedRule(null))
} else if (deletedRule.error) {
const style = { marginTop: 85 }
if (deletedPolicy.loading) {
message.loading({ content: 'Deleting...', key: deleteKey, style })
} else if (deletedPolicy.success) {
message.success({ content: 'Rule has been successfully disabled.', key: deleteKey, duration: 2, style })
dispatch(policyActions.resetDeletedPolicy(null))
} else if (deletedPolicy.error) {
message.error({
content: 'Failed to remove rule. You might not have enough permissions.',
key: deleteKey,
duration: 2,
style
})
dispatch(ruleActions.resetDeletedRule(null))
dispatch(policyActions.resetDeletedPolicy(null))
}
}, [deletedRule])
}, [deletedPolicy])
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setTextToSearch(e.target.value)
@@ -193,14 +210,14 @@ export const AccessControl = () => {
setDataTable(transformDataTable(data))
}
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
const onChangeAllEnabled = ({ target: { value } }: RadioChangeEvent) => {
setOptionAllEnable(value)
}
const showConfirmDelete = () => {
let name = ruleToAction ? ruleToAction.name : '';
let name = policyToAction ? policyToAction.name : '';
confirm({
icon: <ExclamationCircleOutlined/>,
icon: <ExclamationCircleOutlined />,
title: "Delete rule \"" + name + "\"",
width: 600,
content: <Space direction="vertical" size="small">
@@ -208,25 +225,25 @@ export const AccessControl = () => {
</Space>,
okType: 'danger',
onOk() {
dispatch(ruleActions.deleteRule.request({
dispatch(policyActions.deletePolicy.request({
getAccessTokenSilently: getTokenSilently,
payload: ruleToAction?.id || ''
payload: policyToAction?.id || ''
}));
},
onCancel() {
setRuleToAction(null);
setPolicyToAction(null);
},
});
}
const showConfirmDeactivate = () => {
confirm({
icon: <ExclamationCircleOutlined/>,
icon: <ExclamationCircleOutlined />,
width: 600,
content: <Space direction="vertical" size="small">
{ruleToAction &&
{policyToAction &&
<>
<Title level={5}>Deactivate rule "{ruleToAction ? ruleToAction.name : ''}"</Title>
<Title level={5}>Deactivate rule "{policyToAction ? policyToAction.name : ''}"</Title>
<Paragraph>Are you sure you want to deactivate peer from your account?</Paragraph>
</>
}
@@ -236,58 +253,78 @@ export const AccessControl = () => {
//dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.id || ''}));
},
onCancel() {
setRuleToAction(null);
setPolicyToAction(null);
},
});
}
const filterDataTable = (): Rule[] => {
const filterDataTable = (): Policy[] => {
const t = textToSearch.toLowerCase().trim()
let f: Rule[] = filter(rules, (f: Rule) =>
let f: Policy[] = filter(policies, (f: Policy) =>
(f.name.toLowerCase().includes(t) || f.description.toLowerCase().includes(t) || t === "")
) as Rule[]
) as Policy[]
if (optionAllEnable !== "all") {
f = filter(f, (f: Rule) => !f.disabled)
f = filter(f, (f: Policy) => f.enabled)
}
return f
}
const onClickAddNewRule = () => {
dispatch(ruleActions.setSetupNewRuleVisible(true));
dispatch(ruleActions.setRule({
const onClickAddNewPolicy = () => {
dispatch(policyActions.setSetupNewPolicyVisible(true));
dispatch(policyActions.setPolicy({
name: '',
description: '',
sources: [],
destinations: [],
flow: 'bidirect',
disabled: false
} as Rule))
enabled: true,
rules: [{
name: '',
description: '',
enabled: true,
bidirectional: true,
action: 'accept',
protocol: 'all',
}]
} as Policy))
}
const onClickViewRule = () => {
dispatch(ruleActions.setSetupNewRuleVisible(true));
dispatch(ruleActions.setRule({
id: ruleToAction?.id || null,
name: ruleToAction?.name,
description: ruleToAction?.description,
sources: ruleToAction?.sources,
destinations: ruleToAction?.destinations,
flow: ruleToAction?.flow,
disabled: ruleToAction?.disabled
} as Rule))
const onClickViewPolicy = () => {
dispatch(policyActions.setSetupNewPolicyVisible(true));
dispatch(policyActions.setPolicy({
id: policyToAction?.id || null,
name: policyToAction?.name,
description: policyToAction?.description,
enabled: policyToAction?.enabled,
rules: [{
name: policyToAction?.name,
description: policyToAction?.description,
enabled: policyToAction?.enabled,
sources: policyToAction?.sources,
destinations: policyToAction?.destinations,
bidirectional: policyToAction?.bidirectional,
protocol: policyToAction?.protocol,
ports: policyToAction?.ports,
}]
} as Policy))
}
const setRuleAndView = (rule: RuleDataTable) => {
dispatch(ruleActions.setSetupNewRuleVisible(true));
dispatch(ruleActions.setRule({
id: rule.id || null,
name: rule.name,
description: rule.description,
sources: rule.sources,
destinations: rule.destinations,
flow: rule.flow,
disabled: rule.disabled
} as Rule))
const setPolicyAndView = (p: PolicyDataTable) => {
dispatch(policyActions.setSetupNewPolicyVisible(true));
dispatch(policyActions.setPolicy({
id: p.id || null,
name: p.name,
description: p.description,
enabled: p.enabled,
rules: [{
id: p.id || null,
name: p.name,
description: p.description,
enabled: p.enabled,
sources: p.sources,
destinations: p.destinations,
bidirectional: p.bidirectional,
protocol: p.protocol,
ports: p.ports,
}]
} as Policy))
}
const toggleModalGroups = (title: string, groups: Group[] | string[] | null, modalVisible: boolean) => {
@@ -299,13 +336,13 @@ export const AccessControl = () => {
}
useEffect(() => {
if (setupNewRuleVisible) {
if (setupNewPolicyVisible) {
setGroupPopupVisible("")
}
}, [setupNewRuleVisible])
}, [setupNewPolicyVisible])
const onPopoverVisibleChange = (b: boolean, key: string) => {
if (setupNewRuleVisible) {
if (setupNewPolicyVisible) {
setGroupPopupVisible("")
} else {
if (b) {
@@ -316,20 +353,20 @@ export const AccessControl = () => {
}
}
const renderPopoverGroups = (label: string, groups: Group[] | string[] | null, rule: RuleDataTable) => {
const renderPopoverGroups = (label: string, groups: Group[] | string[] | null, rule: PolicyDataTable) => {
const content = groups?.map((g, i) => {
const _g = g as Group
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
return (
<div key={i}>
<Tag
color="blue"
style={{marginRight: 3}}
>
<strong>{_g.name}</strong>
</Tag>
<span style={{fontSize: ".85em"}}>{peersCount}</span>
</div>
<div key={i}>
<Tag
color="blue"
style={{ marginRight: 3 }}
>
<strong>{_g.name}</strong>
</Tag>
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
</div>
)
})
const mainContent = (<Space direction="vertical">{content}</Space>)
@@ -339,11 +376,20 @@ export const AccessControl = () => {
open={groupPopupVisible === rule.key}
content={mainContent}
title={null}>
<Button type="link" onClick={() => setRuleAndView(rule)}>{label}</Button>
<Button type="link" onClick={() => setPolicyAndView(rule)}>{label}</Button>
</Popover>
)
}
const renderPorts = (ports: string[]) => {
const content = ports?.map((p, i) => {
return (
<Tag key={i} style={{ marginRight: 3 }}><strong>{p}</strong></Tag>
)
})
return (<div>{content}</div>)
}
return (
<>
<Container className="container-main">
@@ -351,11 +397,11 @@ export const AccessControl = () => {
<Col span={24}>
<Title level={4}>Access Control</Title>
<Paragraph>Access rules help you manage access permissions in your organisation.</Paragraph>
<Space direction="vertical" size="large" style={{display: 'flex'}}>
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
<Row gutter={[16, 24]}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
placeholder="Search..." onChange={onChangeTextToSearch}/>
placeholder="Search..." onChange={onChangeTextToSearch} />
</Col>
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Space size="middle">
@@ -367,28 +413,28 @@ export const AccessControl = () => {
buttonStyle="solid"
/>
<Select value={pageSize.toString()} options={pageSizeOptions}
onChange={onChangePageSize} className="select-rows-per-page-en"/>
onChange={onChangePageSize} className="select-rows-per-page-en" />
</Space>
</Col>
<Col xs={24}
sm={24}
md={5}
lg={5}
xl={5}
xxl={5} span={5}>
sm={24}
md={5}
lg={5}
xl={5}
xxl={5} span={5}>
<Row justify="end">
<Col>
<Button type="primary" disabled={savedRule.loading}
onClick={onClickAddNewRule}>Add Rule</Button>
<Button type="primary" disabled={savedPolicy.loading}
onClick={onClickAddNewPolicy}>Add Rule</Button>
</Col>
</Row>
</Col>
</Row>
{failed &&
<Alert message={failed.message} description={failed.data ? failed.data.message : " "} type="error" showIcon
closable/>
closable />
}
<Card bodyStyle={{padding: 0}}>
<Card bodyStyle={{ padding: 0 }}>
<Table
pagination={{
current: currentPage, hideOnSinglePage: showTutorial, disabled: showTutorial,
@@ -400,74 +446,82 @@ export const AccessControl = () => {
}}
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "card-table"}`}
showSorterTooltip={false}
scroll={{x: true}}
scroll={{ x: true }}
loading={tableSpin(loading)}
dataSource={dataTable}>
<Column title="Name" dataIndex="name"
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
defaultSortOrder='ascend'
render={(text, record, index) => {
const desc = (record as RuleDataTable).description.trim()
return <Tooltip title={desc !== "" ? desc : "no description"}
arrowPointAtCenter>
<span onClick={() => setRuleAndView(record as RuleDataTable)}
className="tooltip-label"><Text strong>{text}</Text></span>
</Tooltip>
}}
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
defaultSortOrder='ascend'
render={(text, record, index) => {
const desc = (record as PolicyDataTable).description.trim()
return <Tooltip title={desc !== "" ? desc : "no description"}
arrowPointAtCenter>
<span onClick={() => setPolicyAndView(record as PolicyDataTable)}
className="tooltip-label"><Text strong>{text}</Text></span>
</Tooltip>
}}
/>
<Column title="Status" dataIndex="disabled"
render={(text: Boolean, record: RuleDataTable, index) => {
return text ? <Tag color="red">disabled</Tag> :
<Tag color="green">enabled</Tag>
}}
<Column title="Status" dataIndex="enabled"
render={(text: Boolean, record: PolicyDataTable, index) => {
return text ? <Tag color="green">enabled</Tag> : <Tag color="red">disabled</Tag>
}}
/>
<Column title="Sources" dataIndex="sourceLabel"
render={(text, record: RuleDataTable, index) => {
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Sources`, record.Source, true)}>{text}</Button>
return renderPopoverGroups(text, record.sources, record as RuleDataTable)
}}
render={(text, record: PolicyDataTable, index) => {
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Sources`, record.Source, true)}>{text}</Button>
return renderPopoverGroups(text, record.sources, record as PolicyDataTable)
}}
/>
<Column title="Direction" dataIndex="flow"
render={(text, record: RuleDataTable, index) => {
const s = {minWidth: 50, textAlign: "center"} as React.CSSProperties
if (text === "bidirect")
return <Tag color="processing" style={s}><img src={bidirect}/></Tag>
else if (text === "srcToDest") {
return <Tag color="green" style={s}><img src={outbound}/></Tag>
} else if (text === "destToSrc") {
return <Tag color="green" style={s}><img src={inbound}/></Tag>
}
return <Tag color="red" style={s}><CloseOutlined/></Tag>
}}
<Column title="Direction" dataIndex="bidirectional"
render={(text, record: PolicyDataTable, index) => {
const s = { minWidth: 50, textAlign: "center" } as React.CSSProperties
if (record.bidirectional) {
return <Tag color="processing" style={s}><img src={bidirect} /></Tag>
}
return <Tag color="green" style={s}><img src={outbound} /></Tag>
}}
/>
<Column title="Destinations" dataIndex="destinationLabel"
render={(text, record: RuleDataTable, index) => {
//return <Button type="link" onClick={() => toggleModalGroups(`${record.name} - Destinations`, record.destinations, true)}>{text}</Button>
return renderPopoverGroups(text, record.destinations, record as RuleDataTable)
}}
render={(text, record: PolicyDataTable, index) => {
//return <Button type="link" onClick={() => toggleModalGroups(`${record.name} - Destinations`, record.destinations, true)}>{text}</Button>
return renderPopoverGroups(text, record.destinations, record as PolicyDataTable)
}}
/>
<Column title="Protocol" dataIndex="protocol"
render={(text, record: PolicyDataTable, index) => {
return <Tag
style={{ marginRight: "3", textTransform: "uppercase" }}>
{record.protocol}
</Tag>
}}
/>
<Column title="Ports" dataIndex="ports"
render={(text, record: PolicyDataTable, index) => {
return renderPorts(record.ports)
}}
/>
<Column title="" align="center"
render={(text, record, index) => {
if (deletedRule.loading || savedRule.loading) return <></>
return (
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
if (visible) setRuleToAction(record as RuleDataTable)
}}>
<Button type="text">
<Space>
<EllipsisOutlined />
</Space>
</Button>
</Dropdown>
)
}}
render={(text, record, index) => {
if (deletedPolicy.loading || savedPolicy.loading) return <></>
return (
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
if (visible) setPolicyToAction(record as PolicyDataTable)
}}>
<Button type="text">
<Space>
<EllipsisOutlined />
</Space>
</Button>
</Dropdown>
)
}}
/>
</Table>
{showTutorial &&
<Space direction="vertical" size="small" align="center"
style={{display: 'flex', padding: '45px 15px'}}>
<Button type="link" onClick={onClickAddNewRule}>Add new access rule</Button>
style={{ display: 'flex', padding: '45px 15px' }}>
<Button type="link" onClick={onClickAddNewPolicy}>Add new access rule</Button>
</Space>
}
</Card>
@@ -476,11 +530,11 @@ export const AccessControl = () => {
</Row>
</Container>
<AccessControlModalGroups data={groupsToShow.groups} title={groupsToShow.title}
visible={groupsToShow.modalVisible}
onCancel={() => toggleModalGroups("", [], false)}/>
<AccessControlNew/>
visible={groupsToShow.modalVisible}
onCancel={() => toggleModalGroups("", [], false)} />
<AccessControlNew />
</>
)
}
export default AccessControl;
export default AccessControl;