/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 React, {useEffect, useState} from 'react'; import {Button, Form, Input, message, Modal, Popconfirm, Select, Space, Table, Tabs, Tag} from 'antd'; import {DeleteOutlined, EditOutlined, EyeInvisibleOutlined, EyeOutlined} from '@ant-design/icons'; import {remoteApi} from "../../api/remoteApi/remoteApi"; import ResourceInput from '../../components/acl/ResourceInput'; import SubjectInput from "../../components/acl/SubjectInput"; import {useLanguage} from "../../i18n/LanguageContext"; const {TabPane} = Tabs; const {Search} = Input; const Acl = () => { const [activeTab, setActiveTab] = useState('users'); const [userListData, setUserListData] = useState([]); const [aclListData, setAclListData] = useState([]); const [loading, setLoading] = useState(false); const [searchText, setSearchText] = useState(''); const [isUserModalVisible, setIsUserModalVisible] = useState(false); const [currentUser, setCurrentUser] = useState(null); const [userForm] = Form.useForm(); const [showPassword, setShowPassword] = useState(false); const [isAclModalVisible, setIsAclModalVisible] = useState(false); const [writeOperationEnabled, setWriteOperationEnabled] = useState(true); const [currentAcl, setCurrentAcl] = useState(null); const [aclForm] = Form.useForm(); const [messageApi, msgContextHolder] = message.useMessage(); const [isUpdate, setIsUpdate] = useState(false); const [ips, setIps] = useState([]); const {t} = useLanguage(); // 校验IP地址的正则表达式 const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,1}|(?:[0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,2}|(?:[0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,3}|(?:[0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,4}|(?:[0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5}|(?:[0-9A-Fa-f]{1,4}:){1}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6}|(?::(?::[0-9A-Fa-f]{1,4}){1,7}|::))(\/(?:12[0-7]|1[0-1][0-9]|[1-9]?[0-9]))?$/; // 支持 IPv4 和 IPv6,包括 CIDR 表示法 // State to store the entire clusterInfo object for easy access const [clusterData, setClusterData] = useState(null); // State for the list of available cluster names for the dropdown const [clusterNamesOptions, setClusterNamesOptions] = useState([]); // State for the currently selected cluster name const [selectedCluster, setSelectedCluster] = useState(undefined); // State for the list of available broker names for the dropdown (depends on selectedCluster) const [brokerNamesOptions, setBrokerNamesOptions] = useState([]); // State for the currently selected broker name const [selectedBroker, setSelectedBroker] = useState(undefined); // State for the address of the selected broker const [brokerAddress, setBrokerAddress] = useState(undefined); const [searchValue, setSearchValue] = useState(''); // --- Data Fetching and Initial Setup --- useEffect(() => { const fetchData = async () => { const clusterResponse = await remoteApi.getClusterList(); if (clusterResponse.status === 0 && clusterResponse.data) { const {clusterInfo} = clusterResponse.data; setClusterData(clusterInfo); // Store the entire clusterInfo // Populate cluster names for the first dropdown const clusterNames = Object.keys(clusterInfo?.clusterAddrTable || {}); setClusterNamesOptions(clusterNames.map(name => ({label: name, value: name}))); // Set initial selections if clusters are available if (clusterNames.length > 0) { const defaultCluster = clusterNames[0]; setSelectedCluster(defaultCluster); // Manually trigger broker list update for the default cluster updateBrokerOptions(defaultCluster, clusterInfo); // Set default broker and its address if available const brokersInDefaultCluster = clusterInfo.clusterAddrTable[defaultCluster] || []; if (brokersInDefaultCluster.length > 0) { const defaultBroker = brokersInDefaultCluster[0]; setSelectedBroker(defaultBroker); // Get the address from brokerAddrTable using the defaultBroker name const addr = clusterInfo.brokerAddrTable?.[defaultBroker]?.brokerAddrs?.["0"]; setBrokerAddress(addr); } } } else { console.error('Failed to fetch cluster list:', clusterResponse.errMsg); } }; if (!clusterData) { setLoading(true); fetchData().finally(() => setLoading(false)); } if (brokerAddress) { if (activeTab === 'users') { fetchUsers().finally(() => setLoading(false)); } else { fetchAcls().finally(() => setLoading(false)); } } }, [activeTab]); // Dependencies for useEffect useEffect(() => { const userPermission = localStorage.getItem('userrole'); if (userPermission == 2) { setWriteOperationEnabled(false); } else { setWriteOperationEnabled(true); } }, []); // --- Helper function to update broker options based on selected cluster --- const updateBrokerOptions = (clusterName, info = clusterData) => { if (!info || !info.clusterAddrTable) { setBrokerNamesOptions([]); return; } const brokersInCluster = info.clusterAddrTable[clusterName] || []; setBrokerNamesOptions(brokersInCluster.map(broker => ({label: broker, value: broker}))); }; // --- Event Handlers --- const handleClusterChange = (value) => { setSelectedCluster(value); setSelectedBroker(undefined); // Reset broker selection setBrokerAddress(undefined); // Reset broker address // Update the broker options based on the newly selected cluster updateBrokerOptions(value); }; const handleBrokerChange = (value) => { setSelectedBroker(value); // Find the corresponding broker address from clusterData if (clusterData && clusterData.brokerAddrTable && clusterData.brokerAddrTable[value]) { const addr = clusterData.brokerAddrTable[value].brokerAddrs?.["0"]; setBrokerAddress(addr); } else { setBrokerAddress(undefined); } }; const handleIpChange = value => { // 过滤掉重复的IP地址 const uniqueIps = Array.from(new Set(value)); setIps(uniqueIps); }; const handleIpDeselect = value => { // 移除被取消选择的IP setIps(ips.filter(ip => ip !== value)); }; const validateIp = (rule, value) => { if (!value || value.length === 0) { return Promise.resolve(); // Allow empty } const invalidIps = value.filter(ip => !ipRegex.test(ip)); if (invalidIps.length > 0) { return Promise.reject(t.INVALID_IP_ADDRESSES + "ips:" + invalidIps.join(', ')); } return Promise.resolve(); }; // --- Data Loading Functions --- const fetchUsers = async () => { setLoading(true); try { const result = await remoteApi.listUsers(selectedBroker, selectedCluster); if (result && result.status === 0 && result.data) { const formattedUsers = result.data.map(user => ({ ...user, key: user.username, // Table needs key userStatus: user.userStatus === 'enable' ? t.ENABLED : t.DISABLED // Format status })); setUserListData(formattedUsers); } else { messageApi.error(t.GET_USERS_FAILED + result?.errMsg); } } catch (error) { console.error("Failed to fetch users:", error); messageApi.error(t.GET_USERS_EXCEPTION); } finally { setLoading(false); } }; const fetchAcls = async () => { setLoading(true); try { const result = await remoteApi.listAcls(selectedBroker, searchValue, selectedCluster); if (result && result.status === 0) { const formattedAcls = []; if (result && result.data && Array.isArray(result.data)) { result.data.forEach((acl, aclIndex) => { const subject = acl.subject; if (acl.policies && Array.isArray(acl.policies)) { acl.policies.forEach((policy, policyIndex) => { const policyType = policy.policyType; if (policy.entries && Array.isArray(policy.entries)) { policy.entries.forEach((entry, entryIndex) => { const resources = Array.isArray(entry.resource) ? entry.resource : (entry.resource ? [entry.resource] : []); resources.forEach((singleResource, resourceIndex) => { formattedAcls.push({ key: `acl-${aclIndex}-policy-${policyIndex}-entry-${entryIndex}-resource-${singleResource}`, subject: subject, policyType: policyType, resource: singleResource || t.N_A, actions: (entry.actions && Array.isArray(entry.actions)) ? entry.actions.join(', ') : '', sourceIps: (entry.sourceIps && Array.isArray(entry.sourceIps)) ? entry.sourceIps.join(', ') : t.N_A, decision: entry.decision || t.N_A }); }); }); } }); } }); } else { console.warn(t.INVALID_OR_EMPTY_ACL_DATA); } setAclListData(formattedAcls); } else { messageApi.error(t.GET_ACLS_FAILED + result?.errMsg); } } catch (error) { console.error("Failed to fetch ACLs:", error); messageApi.error(t.GET_ACLS_EXCEPTION); } finally { setLoading(false); } }; // --- User Management Logic --- const handleAddUser = () => { setCurrentUser(null); userForm.resetFields(); setShowPassword(false); setIsUserModalVisible(true); }; const handleEditUser = (record) => { setCurrentUser(record); userForm.setFieldsValue({ username: record.username, password: record.password, userType: record.userType, userStatus: record.userStatus === t.ENABLED ? 'enable' : 'disable' }); setShowPassword(false); setIsUserModalVisible(true); }; const handleDeleteUser = async (username) => { setLoading(true); try { const result = await remoteApi.deleteUser(selectedBroker, username, selectedCluster); if (result.status === 0) { messageApi.success(t.USER_DELETE_SUCCESS); fetchUsers(); } else { messageApi.error(t.USER_DELETE_FAILED + result.errMsg); } } catch (error) { console.error("Failed to delete user:", error); messageApi.error(t.USER_DELETE_EXCEPTION); } finally { setLoading(false); } }; const handleUserModalOk = async () => { try { const values = await userForm.validateFields(); setLoading(true); let result; const userInfoParam = { username: values.username, password: values.password, userType: values.userType, userStatus: values.userStatus, }; if (currentUser) { result = await remoteApi.updateUser(selectedBroker, userInfoParam, selectedCluster); if (result.status === 0) { messageApi.success(t.USER_UPDATE_SUCCESS); } else { messageApi.error(result.errMsg); } } else { result = await remoteApi.createUser(selectedBroker, userInfoParam, selectedCluster); if (result.status === 0) { messageApi.success(t.USER_CREATE_SUCCESS); } else { messageApi.error(result.errMsg); } } setIsUserModalVisible(false); fetchUsers(); } catch (error) { console.error("Failed to save user:", error); messageApi.error(t.SAVE_USER_FAILED); } finally { setLoading(false); } }; // --- ACL Permission Management Logic --- const handleAddAcl = () => { setCurrentAcl(null); setIsUpdate(false) aclForm.resetFields(); setIsAclModalVisible(true); }; const handleEditAcl = (record) => { setCurrentAcl(record); setIsUpdate(true); aclForm.setFieldsValue({ subject: record.subject, policyType: record.policyType, resource: record.resource, actions: record.actions ? record.actions.split(', ') : [], sourceIps: record.sourceIps ? record.sourceIps.split(', ') : [], decision: record.decision }); setIsAclModalVisible(true); }; const handleDeleteAcl = async (subject, resource) => { setLoading(true); try { const result = await remoteApi.deleteAcl(selectedBroker, subject, resource, selectedCluster); if (result.status === 0) { messageApi.success(t.ACL_DELETE_SUCCESS); fetchAcls(); } else { messageApi.error(t.ACL_DELETE_FAILED + result.errMsg); } } catch (error) { console.error("Failed to delete ACL:", error); messageApi.error(t.ACL_DELETE_EXCEPTION); } finally { setLoading(false); } }; const handleAclModalOk = async () => { try { const values = await aclForm.validateFields(); setLoading(true); let result; const policiesParam = [ { policyType: values.policyType, entries: [ { resource: isUpdate ? [values.resource] : values.resource, actions: values.actions, sourceIps: values.sourceIps, decision: values.decision } ] } ]; if (isUpdate) { // This condition seems reversed for update/create based on the current logic. result = await remoteApi.updateAcl(selectedBroker, values.subject, policiesParam, selectedCluster); if (result.status === 0) { messageApi.success(t.ACL_UPDATE_SUCCESS); setIsAclModalVisible(false); fetchAcls(); } else { messageApi.error(t.ACL_UPDATE_FAILED + result.errMsg); } setIsUpdate(false) } else { result = await remoteApi.createAcl(selectedBroker, values.subject, policiesParam, selectedCluster); if (result.status === 0) { messageApi.success(t.ACL_CREATE_SUCCESS); setIsAclModalVisible(false); fetchAcls(); } else { messageApi.error(t.ACL_CREATE_FAILED + result.errMsg); } } } catch (error) { console.error("Failed to save ACL:", error); messageApi.error(t.SAVE_ACL_FAILED); } finally { setLoading(false); } }; const handleInputChange = (e) => { setSearchValue(e.target.value); }; // --- Search Functionality --- const handleSearch = (value) => { if (activeTab === 'users') { const filteredData = userListData.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(value.toLowerCase()) ) ); if (value === '') { fetchUsers(); } else { setUserListData(filteredData); } } else { fetchAcls(); } }; // --- User Table Column Definitions --- const userColumns = [ { title: t.USERNAME, dataIndex: 'username', key: 'username', }, { title: t.PASSWORD, dataIndex: 'password', key: 'password', render: (text) => ( {showPassword ? text : '********'} ), }, { title: t.USER_TYPE, dataIndex: 'userType', key: 'userType', }, { title: t.USER_STATUS, dataIndex: 'userStatus', key: 'userStatus', render: (status) => ( {status} ), }, { title: t.OPERATION, key: 'action', render: (_, record) => ( writeOperationEnabled ? ( handleDeleteUser(record.username)} okText={t.YES} cancelText={t.NO} > ) : null ), }, ]; // --- ACL Permission Table Column Definitions --- const aclColumns = [ { title: t.USERNAME_SUBJECT, dataIndex: 'subject', key: 'subject', }, { title: t.POLICY_TYPE, dataIndex: 'policyType', key: 'policyType', }, { title: t.RESOURCE_NAME, dataIndex: 'resource', key: 'resource', }, { title: t.OPERATION_TYPE, dataIndex: 'actions', key: 'actions', render: (text) => text ? text.split(', ').map((action, index) => ( {action} )) : null, }, { title: t.SOURCE_IP, dataIndex: 'sourceIps', key: 'sourceIps', }, { title: t.DECISION, dataIndex: 'decision', key: 'decision', render: (text) => ( {text} ), }, { title: t.OPERATION, key: 'action', render: (_, record) => ( writeOperationEnabled ? ( handleDeleteAcl(record.subject, record.resource)} okText={t.YES} cancelText={t.NO} > ) : null ), }, ]; return ( <> {msgContextHolder}

{t.ACL_MANAGEMENT}

{activeTab === 'users' && ( )} {activeTab === 'acls' && (
)} {/* User Management Modal */} setIsUserModalVisible(false)} confirmLoading={loading} footer={[ , , ]} >
(visible ? : )} />
{/* ACL Permission Management Modal */} setIsAclModalVisible(false)} confirmLoading={loading} >
{isUpdate ? ( ) : ( )}
); } export default Acl;