ACL的认证和授权管理功能分离.

This commit is contained in:
许晓东
2022-08-28 21:39:08 +08:00
parent 4c3fe5230c
commit 3bd14a35d6
16 changed files with 1160 additions and 502 deletions

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 205 KiB

View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.xuxd</groupId>
<artifactId>kafka-console-ui</artifactId>
<version>1.0.5</version>
<version>1.0.6</version>
<name>kafka-console-ui</name>
<description>Kafka console manage ui</description>
<properties>

View File

@@ -118,4 +118,14 @@ public class AclAuthController {
return aclService.deleteConsumerAcl(param.toTopicEntry(), param.toGroupEntry());
}
/**
* clear principal acls.
*
* @param param acl principal.
* @return true or false.
*/
@DeleteMapping("/clear")
public Object clearAcl(@RequestBody DeleteAclDTO param) {
return aclService.clearAcl(param.toUserEntry());
}
}

View File

@@ -1,7 +1,9 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.AclEntry;
import com.xuxd.kafka.console.beans.AclUser;
import com.xuxd.kafka.console.service.AclService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -12,7 +14,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* kafka-console-ui.
* kafka-console-ui. sasl scram user.
*
* @author xuxd
* @date 2021-08-28 21:13:05
@@ -49,4 +51,11 @@ public class AclUserController {
public Object getUserDetail(@RequestParam String username) {
return aclService.getUserDetail(username);
}
@GetMapping("/scram")
public Object getSaslScramUserList(@RequestParam(required = false) String username) {
AclEntry entry = new AclEntry();
entry.setPrincipal(StringUtils.isNotBlank(username) ? username : null);
return aclService.getSaslScramUserList(entry);
}
}

View File

@@ -72,6 +72,7 @@ public class ContextSetFilter implements Filter {
config.setProperties(ConvertUtil.toProperties(infoDO.getProperties()));
}
ContextConfigHolder.CONTEXT_CONFIG.set(config);
// log.info("current kafka config: {}", config);
}
}
chain.doFilter(req, response);

View File

@@ -42,4 +42,7 @@ public interface AclService {
ResponseData getUserDetail(String username);
ResponseData clearAcl(AclEntry entry);
ResponseData getSaslScramUserList(AclEntry entry);
}

View File

@@ -10,30 +10,23 @@ import com.xuxd.kafka.console.config.ContextConfigHolder;
import com.xuxd.kafka.console.dao.KafkaUserMapper;
import com.xuxd.kafka.console.service.AclService;
import com.xuxd.kafka.console.utils.SaslUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import kafka.console.KafkaAclConsole;
import kafka.console.KafkaConfigConsole;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.admin.ScramMechanism;
import org.apache.kafka.clients.admin.UserScramCredentialsDescription;
import org.apache.kafka.common.acl.AclBinding;
import org.apache.kafka.common.acl.AclOperation;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.security.auth.SecurityProtocol;
import org.apache.kafka.common.errors.SecurityDisabledException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import scala.Tuple2;
import java.util.*;
import java.util.stream.Collectors;
import static com.xuxd.kafka.console.utils.SaslUtil.isEnableSasl;
import static com.xuxd.kafka.console.utils.SaslUtil.isEnableScram;
@@ -139,39 +132,52 @@ public class AclServiceImpl implements AclService {
}
@Override public ResponseData getAclList(AclEntry entry) {
List<AclBinding> aclBindingList = entry.isNull() ? aclConsole.getAclList(null) : aclConsole.getAclList(entry);
List<AclBinding> aclBindingList = Collections.emptyList();
try {
aclBindingList = entry.isNull() ? aclConsole.getAclList(null) : aclConsole.getAclList(entry);
}catch (Exception ex) {
if (ex.getCause() instanceof SecurityDisabledException) {
Throwable e = ex.getCause();
log.info("SecurityDisabledException: {}", e.getMessage());
Map<String, String> hint = new HashMap<>(2);
hint.put("hint", "Security Disabled: " + e.getMessage());
return ResponseData.create().data(hint).success();
}
throw new RuntimeException(ex.getCause());
}
// List<AclBinding> aclBindingList = entry.isNull() ? aclConsole.getAclList(null) : aclConsole.getAclList(entry);
List<AclEntry> entryList = aclBindingList.stream().map(x -> AclEntry.valueOf(x)).collect(Collectors.toList());
Map<String, List<AclEntry>> entryMap = entryList.stream().collect(Collectors.groupingBy(AclEntry::getPrincipal));
Map<String, Object> resultMap = new HashMap<>();
entryMap.forEach((k, v) -> {
Map<String, List<AclEntry>> map = v.stream().collect(Collectors.groupingBy(e -> e.getResourceType() + "#" + e.getName()));
String username = SaslUtil.findUsername(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties().getProperty(SaslConfigs.SASL_JAAS_CONFIG));
if (k.equals(username)) {
Map<String, Object> map2 = new HashMap<>(map);
Map<String, Object> userMap = new HashMap<>();
userMap.put("role", "admin");
map2.put("USER", userMap);
}
// String username = SaslUtil.findUsername(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties().getProperty(SaslConfigs.SASL_JAAS_CONFIG));
// if (k.equals(username)) {
// Map<String, Object> map2 = new HashMap<>(map);
// Map<String, Object> userMap = new HashMap<>();
// userMap.put("role", "admin");
// map2.put("USER", userMap);
// }
resultMap.put(k, map);
});
if (entry.isNull() || StringUtils.isNotBlank(entry.getPrincipal())) {
Map<String, UserScramCredentialsDescription> detailList = configConsole.getUserDetailList(StringUtils.isNotBlank(entry.getPrincipal()) ? Collections.singletonList(entry.getPrincipal()) : null);
detailList.values().forEach(u -> {
if (!resultMap.containsKey(u.name()) && !u.credentialInfos().isEmpty()) {
String username = SaslUtil.findUsername(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties().getProperty(SaslConfigs.SASL_JAAS_CONFIG));
if (!u.name().equals(username)) {
resultMap.put(u.name(), Collections.emptyMap());
} else {
Map<String, Object> map2 = new HashMap<>();
Map<String, Object> userMap = new HashMap<>();
userMap.put("role", "admin");
map2.put("USER", userMap);
resultMap.put(u.name(), map2);
}
}
});
}
// if (entry.isNull() || StringUtils.isNotBlank(entry.getPrincipal())) {
// Map<String, UserScramCredentialsDescription> detailList = configConsole.getUserDetailList(StringUtils.isNotBlank(entry.getPrincipal()) ? Collections.singletonList(entry.getPrincipal()) : null);
//
// detailList.values().forEach(u -> {
// if (!resultMap.containsKey(u.name()) && !u.credentialInfos().isEmpty()) {
// String username = SaslUtil.findUsername(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties().getProperty(SaslConfigs.SASL_JAAS_CONFIG));
// if (!u.name().equals(username)) {
// resultMap.put(u.name(), Collections.emptyMap());
// } else {
// Map<String, Object> map2 = new HashMap<>();
// Map<String, Object> userMap = new HashMap<>();
// userMap.put("role", "admin");
// map2.put("USER", userMap);
// resultMap.put(u.name(), map2);
// }
// }
// });
// }
return ResponseData.create().data(new CounterMap<>(resultMap)).success();
}
@@ -236,6 +242,37 @@ public class AclServiceImpl implements AclService {
return ResponseData.create().data(vo).success();
}
@Override
public ResponseData clearAcl(AclEntry entry) {
log.info("Start clear acl, principal: {}", entry);
return aclConsole.deleteUserAcl(entry) ? ResponseData.create().success() : ResponseData.create().failed("操作失败");
}
@Override
public ResponseData getSaslScramUserList(AclEntry entry) {
Map<String, Object> resultMap = new HashMap<>();
if (entry.isNull() || StringUtils.isNotBlank(entry.getPrincipal())) {
Map<String, UserScramCredentialsDescription> detailList = configConsole.getUserDetailList(StringUtils.isNotBlank(entry.getPrincipal()) ? Collections.singletonList(entry.getPrincipal()) : null);
detailList.values().forEach(u -> {
if (!resultMap.containsKey(u.name()) && !u.credentialInfos().isEmpty()) {
String username = SaslUtil.findUsername(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties().getProperty(SaslConfigs.SASL_JAAS_CONFIG));
if (!u.name().equals(username)) {
resultMap.put(u.name(), Collections.emptyMap());
} else {
Map<String, Object> map2 = new HashMap<>();
Map<String, Object> userMap = new HashMap<>();
userMap.put("role", "admin");
map2.put("USER", userMap);
resultMap.put(u.name(), map2);
}
}
});
}
return ResponseData.create().data(new CounterMap<>(resultMap)).success();
}
// @Override public void afterSingletonsInstantiated() {
// if (kafkaConfig.isEnableAcl() && kafkaConfig.isAdminCreate()) {
// log.info("Start create admin user, username: {}, password: {}", kafkaConfig.getAdminUsername(), kafkaConfig.getAdminPassword());

View File

@@ -73,6 +73,10 @@ public class ClusterServiceImpl implements ClusterService {
}
@Override public ResponseData updateClusterInfo(ClusterInfoDO infoDO) {
if (infoDO.getProperties() == null) {
// null 的话不更新这个是bug设置为空字符串解决
infoDO.setProperties("");
}
clusterInfoMapper.updateById(infoDO);
return ResponseData.create().success();
}

View File

@@ -11,10 +11,8 @@
><router-link to="/group-page" class="pad-l-r">消费组</router-link>
<span>|</span
><router-link to="/message-page" class="pad-l-r">消息</router-link>
<span v-show="enableSasl">|</span
><router-link to="/acl-page" class="pad-l-r" v-show="enableSasl"
>Acl</router-link
>
<span>|</span
><router-link to="/acl-page" class="pad-l-r">Acl</router-link>
<span>|</span
><router-link to="/op-page" class="pad-l-r">运维</router-link>
<span class="right">集群{{ clusterName }}</span>

View File

@@ -47,6 +47,18 @@ export const KafkaAclApi = {
url: "/acl",
method: "delete",
},
clearAcl: {
url: "/acl/clear",
method: "delete",
},
getSaslScramUserList: {
url: "/user/scram",
method: "get",
},
deleteSaslScramUser: {
url: "/user",
method: "delete",
},
};
export const KafkaConfigApi = {

View File

@@ -1,461 +1,36 @@
<template>
<div class="content">
<a-spin :spinning="loading">
<div class="acl">
<div id="components-form-acl-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`用户名`">
<a-input
placeholder="username"
class="input-w"
v-decorator="['username']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`消费组`">
<a-input
placeholder="groupId"
class="input-w"
v-decorator="['groupId']"
/>
</a-form-item>
</a-col>
<a-col :span="24" :style="{ textAlign: 'right' }">
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-col>
</a-row>
</a-form>
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="资源授权">
<acl-list></acl-list>
</a-tab-pane>
<a-tab-pane key="2" tab="SaslScram用户管理">
<div v-show="enableSasl">
<sasl-scram></sasl-scram>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="updateUser">新增/更新用户</a-button>
<UpdateUser
:visible="showUpdateUser"
@updateUserDialogData="closeUpdateUserDialog"
></UpdateUser>
<div v-show="!enableSasl">
<h2>未启用SASL SCRAM认证不支持该认证的用户管理操作</h2>
</div>
<a-table :columns="columns" :data-source="data" bordered>
<div slot="username" slot-scope="username">
<span>{{ username }}</span
><a-button
size="small"
shape="round"
type="dashed"
style="float: right"
@click="onUserDetail(username)"
>详情</a-button
>
</div>
<div slot="topicList" slot-scope="topicList, record">
<a
href="#"
v-for="t in topicList"
:key="t"
@click="onTopicDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
<div slot="groupList" slot-scope="groupList, record">
<a
href="#"
v-for="t in groupList"
:key="t"
@click="onGroupDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
<div
slot="operation"
slot-scope="record"
v-show="!record.user || record.user.role != 'admin'"
>
<a-popconfirm
:title="'删除用户: ' + record.username + '及相关权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUser(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除</a-button
>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(record)"
>管理生产权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
>管理消费权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
>增加权限
</a-button>
</div>
</a-table>
<UserDetail
:visible="openUserDetailDialog"
:username="selectDetail.username"
@userDetailDialog="closeUserDetailDialog"
></UserDetail>
<AclDetail
:visible="openAclDetailDialog"
:selectDetail="selectDetail"
@aclDetailDialog="closeAclDetailDialog"
></AclDetail>
<ManageProducerAuth
:visible="openManageProducerAuthDialog"
:record="selectRow"
@manageProducerAuthDialog="closeManageProducerAuthDialog"
></ManageProducerAuth>
<ManageConsumerAuth
:visible="openManageConsumerAuthDialog"
:record="selectRow"
@manageConsumerAuthDialog="closeManageConsumerAuthDialog"
></ManageConsumerAuth>
<AddAuth
:visible="openAddAuthDialog"
:record="selectRow"
@addAuthDialog="closeAddAuthDialog"
></AddAuth>
</div>
</a-spin>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import UpdateUser from "@/views/acl/UpdateUser";
import { KafkaAclApi } from "@/utils/api";
import ManageProducerAuth from "@/views/acl/ManageProducerAuth";
import ManageConsumerAuth from "@/views/acl/ManageConsumerAuth";
import AddAuth from "@/views/acl/AddAuth";
import AclDetail from "@/views/acl/AclDetail";
import UserDetail from "@/views/acl/UserDetail";
import AclList from "@/views/acl/AclList";
import SaslScram from "@/views/acl/SaslScram";
import { mapState } from "vuex";
export default {
name: "Acl",
components: {
UpdateUser,
ManageProducerAuth,
ManageConsumerAuth,
AddAuth,
AclDetail,
UserDetail,
AclList,
SaslScram,
},
data() {
return {
queryParam: {},
data: [],
columns,
selectRow: {},
form: this.$form.createForm(this, { name: "advanced_search" }),
showUpdateUser: false,
deleteUserConfirm: false,
openManageProducerAuthDialog: false,
openManageConsumerAuthDialog: false,
openAddAuthDialog: false,
openAclDetailDialog: false,
openUserDetailDialog: false,
selectDetail: {
resourceName: "",
resourceType: "",
username: "",
},
loading: false,
};
},
methods: {
handleSearch(e) {
e.preventDefault();
this.form.validateFields((error, values) => {
let queryParam = {};
if (values.username) {
queryParam.username = values.username;
}
if (values.topic) {
queryParam.resourceType = "TOPIC";
queryParam.resourceName = values.topic;
} else if (values.groupId) {
queryParam.resourceType = "GROUP";
queryParam.resourceName = values.groupId;
}
Object.assign(this.queryParam, queryParam);
this.getAclList();
});
},
handleReset() {
this.form.resetFields();
},
updateUser() {
this.showUpdateUser = true;
},
closeUpdateUserDialog(data) {
this.showUpdateUser = data.show;
if (data.ok) {
this.getAclList();
}
},
onDeleteUser(row) {
this.loading = true;
request({
url: KafkaAclApi.deleteKafkaUser.url,
method: KafkaAclApi.deleteKafkaUser.method,
data: { username: row.username },
}).then((res) => {
this.loading = false;
this.getAclList();
if (res.code == 0) {
this.$message.success(res.msg);
} else {
this.$message.error(res.msg);
}
});
},
onManageProducerAuth(row) {
this.openManageProducerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onManageConsumerAuth(row) {
this.openManageConsumerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onAddAuth(row) {
this.openAddAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onTopicDetail(topic, username) {
this.selectDetail.resourceType = "TOPIC";
this.selectDetail.resourceName = topic;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onGroupDetail(group, username) {
this.selectDetail.resourceType = "GROUP";
this.selectDetail.resourceName = group;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onUserDetail(username) {
this.selectDetail.username = username;
this.openUserDetailDialog = true;
},
closeManageProducerAuthDialog() {
this.openManageProducerAuthDialog = false;
this.getAclList();
},
closeManageConsumerAuthDialog() {
this.openManageConsumerAuthDialog = false;
this.getAclList();
},
closeAddAuthDialog(p) {
this.openAddAuthDialog = false;
if (p.refresh) {
this.getAclList();
}
},
closeAclDetailDialog(p) {
this.openAclDetailDialog = false;
if (p.refresh) {
this.getAclList();
}
},
closeUserDetailDialog() {
this.openUserDetailDialog = false;
},
getAclList() {
this.loading = true;
request({
url: KafkaAclApi.getAclList.url,
method: KafkaAclApi.getAclList.method,
data: this.queryParam,
}).then((response) => {
this.loading = false;
this.data.splice(0, this.data.length);
if (response.code != 0) {
notification.error({
message: response.msg,
});
return;
}
for (let k in response.data.map) {
let v = response.data.map[k];
let topicList = Object.keys(v)
.filter((e) => e.startsWith("TOPIC"))
.map((e) => e.split("#")[1]);
let groupList = Object.keys(v)
.filter((e) => e.startsWith("GROUP"))
.map((e) => e.split("#")[1]);
this.data.push({
key: k,
username: k,
topicList: topicList,
groupList: groupList,
user: response.data.map[k]["USER"],
});
this.data.sort((a, b) => a.username.localeCompare(b.username));
}
});
},
},
created() {
this.getAclList();
computed: {
...mapState({
enableSasl: (state) => state.clusterInfo.enableSasl,
}),
},
};
// function getAclList(data, requestParameters) {
// request({
// url: KafkaAclApi.getAclList.url,
// method: KafkaAclApi.getAclList.method,
// data: requestParameters,
// }).then((response) => {
// data.splice(0, data.length);
// if (response.code != 0) {
// notification.error({
// message: response.msg,
// });
// return;
// }
// for (let k in response.data.map) {
// let v = response.data.map[k];
// let topicList = Object.keys(v)
// .filter((e) => e.startsWith("TOPIC"))
// .map((e) => e.split("#")[1]);
// let groupList = Object.keys(v)
// .filter((e) => e.startsWith("GROUP"))
// .map((e) => e.split("#")[1]);
// data.push({
// key: k,
// username: k,
// topicList: topicList,
// groupList: groupList,
// user: response.data.map[k]["USER"],
// });
// data.sort((a, b) => a.username.localeCompare(b.username));
// }
// });
// }
const columns = [
{
title: "用户名",
dataIndex: "username",
key: "username",
width: 300,
slots: { title: "username" },
scopedSlots: { customRender: "username" },
},
{
title: "topic列表",
dataIndex: "topicList",
key: "topicList",
slots: { title: "topicList" },
scopedSlots: { customRender: "topicList" },
},
{
title: "消费组列表",
dataIndex: "groupList",
key: "groupList",
slots: { title: "groupList" },
scopedSlots: { customRender: "groupList" },
},
{
title: "操作",
key: "operation",
scopedSlots: { customRender: "operation" },
width: 500,
},
];
</script>
<style scoped>
.acl {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-acl-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#components-form-acl-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.input-w {
width: 400px;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 8px;
}
.operation-btn {
margin-right: 3%;
}
</style>

View File

@@ -0,0 +1,444 @@
<template>
<div class="acl">
<a-spin :spinning="loading">
<div v-show="!hint" class="acl">
<div id="components-form-acl-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`主体`">
<a-input
placeholder="比如, 用户名"
class="input-w"
v-decorator="['username']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`消费组`">
<a-input
placeholder="groupId"
class="input-w"
v-decorator="['groupId']"
/>
</a-form-item>
</a-col>
<a-col :span="24" :style="{ textAlign: 'right' }">
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="onAddPrincipalAuth"
>新增主体权限</a-button
>
</div>
<a-table :columns="columns" :data-source="data" bordered>
<div slot="username" slot-scope="username">
<span>{{ username }}</span>
</div>
<div slot="topicList" slot-scope="topicList, record">
<a
href="#"
v-for="t in topicList"
:key="t"
@click="onTopicDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
<div slot="groupList" slot-scope="groupList, record">
<a
href="#"
v-for="t in groupList"
:key="t"
@click="onGroupDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
<div slot="operation" slot-scope="record">
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(record)"
>管理生产权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
>管理消费权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
>增加权限
</a-button>
<a-popconfirm
:title="'清除: ' + record.username + '所有资源权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onClearUserAcl(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>清除权限</a-button
>
</a-popconfirm>
</div>
</a-table>
<AclDetail
:visible="openAclDetailDialog"
:selectDetail="selectDetail"
@aclDetailDialog="closeAclDetailDialog"
></AclDetail>
<ManageProducerAuth
:visible="openManageProducerAuthDialog"
:record="selectRow"
@manageProducerAuthDialog="closeManageProducerAuthDialog"
></ManageProducerAuth>
<ManageConsumerAuth
:visible="openManageConsumerAuthDialog"
:record="selectRow"
@manageConsumerAuthDialog="closeManageConsumerAuthDialog"
></ManageConsumerAuth>
<AddAuth
:visible="openAddAuthDialog"
:record="selectRow"
@addAuthDialog="closeAddAuthDialog"
></AddAuth>
<AddPrincipalAuth
:visible="openAddPrincipalAuthDialog"
@closeAddPrincipalAuthDialog="closeAddPrincipalAuthDialog"
></AddPrincipalAuth>
</div>
<div v-show="hint">
<h2>{{ hint }}</h2>
</div>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import { KafkaAclApi } from "@/utils/api";
import ManageProducerAuth from "@/views/acl/ManageProducerAuth";
import ManageConsumerAuth from "@/views/acl/ManageConsumerAuth";
import AddAuth from "@/views/acl/AddAuth";
import AclDetail from "@/views/acl/AclDetail";
import AddPrincipalAuth from "@/views/acl/AddPrincipalAuth";
export default {
name: "AclList",
components: {
ManageProducerAuth,
ManageConsumerAuth,
AddAuth,
AclDetail,
AddPrincipalAuth,
},
data() {
return {
queryParam: {},
data: [],
columns,
selectRow: {},
form: this.$form.createForm(this, { name: "advanced_search" }),
openManageProducerAuthDialog: false,
openManageConsumerAuthDialog: false,
openAddAuthDialog: false,
openAclDetailDialog: false,
openAddPrincipalAuthDialog: false,
selectDetail: {
resourceName: "",
resourceType: "",
username: "",
},
loading: false,
hint: "",
};
},
methods: {
handleSearch(e) {
e.preventDefault();
this.form.validateFields((error, values) => {
let queryParam = {};
queryParam.username = values.username ? values.username : null;
// if (values.username) {
// queryParam.username = values.username;
// }
if (values.topic) {
queryParam.resourceType = "TOPIC";
queryParam.resourceName = values.topic;
} else if (values.groupId) {
queryParam.resourceType = "GROUP";
queryParam.resourceName = values.groupId;
}
Object.assign(this.queryParam, queryParam);
this.getAclList();
});
},
handleReset() {
this.form.resetFields();
},
onClearUserAcl(row) {
this.loading = true;
request({
url: KafkaAclApi.clearAcl.url,
method: KafkaAclApi.clearAcl.method,
data: { username: row.username },
}).then((res) => {
this.loading = false;
this.getAclList();
if (res.code == 0) {
this.$message.success(res.msg);
} else {
this.$message.error(res.msg);
}
});
},
onManageProducerAuth(row) {
this.openManageProducerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onManageConsumerAuth(row) {
this.openManageConsumerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onAddAuth(row) {
this.openAddAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onTopicDetail(topic, username) {
this.selectDetail.resourceType = "TOPIC";
this.selectDetail.resourceName = topic;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onGroupDetail(group, username) {
this.selectDetail.resourceType = "GROUP";
this.selectDetail.resourceName = group;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onAddPrincipalAuth() {
this.openAddPrincipalAuthDialog = true;
},
closeManageProducerAuthDialog() {
this.openManageProducerAuthDialog = false;
this.getAclList();
},
closeManageConsumerAuthDialog() {
this.openManageConsumerAuthDialog = false;
this.getAclList();
},
closeAddAuthDialog(p) {
this.openAddAuthDialog = false;
if (p.refresh) {
this.getAclList();
}
},
closeAddPrincipalAuthDialog(p) {
this.openAddPrincipalAuthDialog = false;
if (p.refresh) {
this.getAclList();
}
},
closeAclDetailDialog(p) {
this.openAclDetailDialog = false;
if (p.refresh) {
this.getAclList();
}
},
getAclList() {
this.loading = true;
request({
url: KafkaAclApi.getAclList.url,
method: KafkaAclApi.getAclList.method,
data: this.queryParam,
}).then((response) => {
this.loading = false;
this.data.splice(0, this.data.length);
if (response.code != 0) {
notification.error({
message: response.msg,
});
return;
}
if (!response.data.total) {
this.hint = response.data.hint;
return;
}
this.hint = "";
for (let k in response.data.map) {
let v = response.data.map[k];
let topicList = Object.keys(v)
.filter((e) => e.startsWith("TOPIC"))
.map((e) => e.split("#")[1]);
let groupList = Object.keys(v)
.filter((e) => e.startsWith("GROUP"))
.map((e) => e.split("#")[1]);
this.data.push({
key: k,
username: k,
topicList: topicList,
groupList: groupList,
user: response.data.map[k]["USER"],
});
this.data.sort((a, b) => a.username.localeCompare(b.username));
}
});
},
},
created() {
this.getAclList();
},
};
// function getAclList(data, requestParameters) {
// request({
// url: KafkaAclApi.getAclList.url,
// method: KafkaAclApi.getAclList.method,
// data: requestParameters,
// }).then((response) => {
// data.splice(0, data.length);
// if (response.code != 0) {
// notification.error({
// message: response.msg,
// });
// return;
// }
// for (let k in response.data.map) {
// let v = response.data.map[k];
// let topicList = Object.keys(v)
// .filter((e) => e.startsWith("TOPIC"))
// .map((e) => e.split("#")[1]);
// let groupList = Object.keys(v)
// .filter((e) => e.startsWith("GROUP"))
// .map((e) => e.split("#")[1]);
// data.push({
// key: k,
// username: k,
// topicList: topicList,
// groupList: groupList,
// user: response.data.map[k]["USER"],
// });
// data.sort((a, b) => a.username.localeCompare(b.username));
// }
// });
// }
const columns = [
{
title: "主体标识",
dataIndex: "username", //历史原因使用变量username
key: "username",
width: 300,
slots: { title: "username" },
scopedSlots: { customRender: "username" },
},
{
title: "topic列表",
dataIndex: "topicList",
key: "topicList",
slots: { title: "topicList" },
scopedSlots: { customRender: "topicList" },
},
{
title: "消费组列表",
dataIndex: "groupList",
key: "groupList",
slots: { title: "groupList" },
scopedSlots: { customRender: "groupList" },
},
{
title: "操作",
key: "operation",
scopedSlots: { customRender: "operation" },
width: 500,
},
];
</script>
<style scoped>
.acl {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-acl-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#components-form-acl-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.input-w {
width: 400px;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 8px;
}
.operation-btn {
margin-right: 3%;
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<a-modal
title="增加权限"
:visible="show"
:confirm-loading="confirmLoading"
:width="800"
@ok="handleOk"
@cancel="handleCancel"
okText="提交"
cancelText="取消"
:mask="false"
:destroyOnClose="true"
>
<a-form :form="form" :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }">
<a-form-item label="主体标识">
<a-input
v-decorator="[
'username',
{ rules: [{ required: true, message: '请输入!' }] },
]"
/>
</a-form-item>
<a-form-item label="资源类型">
<a-radio-group
v-decorator="['resourceType', { initialValue: 'TOPIC' }]"
>
<a-radio value="TOPIC"> topic</a-radio>
<a-radio value="GROUP"> 消费组</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="资源名称">
<a-input
v-decorator="[
'resourceName',
{ rules: [{ required: true, message: '请输入!' }] },
]"
placeholder="请输入topic或消费组名称"
/>
</a-form-item>
<a-form-item label="主机">
<a-input
v-decorator="[
'host',
{
rules: [{ required: true, message: '请输入!' }],
initialValue: '*',
},
]"
placeholder="请输入主机地址,比如:*,全部匹配"
/>
</a-form-item>
<a-form-item label="操作类型" has-feedback>
<a-select
v-decorator="[
'operation',
{ rules: [{ required: true, message: '请选择!' }] },
]"
placeholder="请选择!"
>
<a-select-option v-for="i in operations" :key="i">
{{ i }}</a-select-option
>
</a-select>
</a-form-item>
<a-form-item label="权限类型">
<a-radio-group
v-decorator="['permissionType', { initialValue: 'ALLOW' }]"
>
<a-radio value="ALLOW"> 允许</a-radio>
<a-radio value="DENY"> 拒绝</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
import { KafkaAclApi } from "@/utils/api";
import request from "@/utils/request";
export default {
name: "AddPrincipalAuth",
props: {
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
formLayout: "horizontal",
form: this.$form.createForm(this, { name: "AddPrincipalAuthForm" }),
confirmLoading: false,
show: this.visible,
operations: operationList,
};
},
watch: {
visible(v) {
if (this.show != v) {
this.show = v;
if (this.show) {
this.getOperationList();
}
}
},
},
methods: {
handleOk() {
const form = this.form;
form.validateFields((e, v) => {
if (e) {
return;
}
const param = Object.assign({}, v);
const api = KafkaAclApi.addAclAuth;
this.confirmLoading = true;
request({
url: api.url,
method: api.method,
data: param,
}).then((res) => {
this.confirmLoading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeAddPrincipalAuthDialog", { refresh: true });
} else {
this.$message.error(res.msg);
}
});
});
},
handleCancel() {
this.$emit("closeAddPrincipalAuthDialog", { refresh: false });
},
getOperationList() {
request({
url: KafkaAclApi.getOperationList.url,
method: KafkaAclApi.getOperationList.method,
}).then((res) => {
if (res.code != 0) {
this.$message.error(res.msg);
} else {
operationList.splice(0, operationList.length);
operationList.push(...res.data);
}
});
},
},
};
const operationList = [];
</script>
<style scoped></style>

View File

@@ -0,0 +1,403 @@
<template>
<a-spin :spinning="loading">
<div class="acl">
<div id="components-form-acl-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`用户名`">
<a-input
placeholder="username"
class="input-w"
v-decorator="['username']"
/>
</a-form-item>
</a-col>
<a-col :span="12" :style="{ textAlign: 'right' }">
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="updateUser">新增/更新用户</a-button>
<UpdateUser
:visible="showUpdateUser"
@updateUserDialogData="closeUpdateUserDialog"
></UpdateUser>
</div>
<a-table :columns="columns" :data-source="data" bordered>
<div slot="username" slot-scope="username">
<span>{{ username }}</span
><a-button
size="small"
shape="round"
type="dashed"
style="float: right"
@click="onUserDetail(username)"
>详情</a-button
>
</div>
<div
slot="operation"
slot-scope="record"
v-show="!record.user || record.user.role != 'admin'"
>
<a-popconfirm
:title="'删除用户: ' + record.username + ''"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUser(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除</a-button
>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(record)"
>管理生产权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
>管理消费权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
>增加权限
</a-button>
<a-popconfirm
:title="'删除用户: ' + record.username + '及相关权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUserAndAuth(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>彻底删除</a-button
>
</a-popconfirm>
</div>
</a-table>
<UserDetail
:visible="openUserDetailDialog"
:username="selectDetail.username"
@userDetailDialog="closeUserDetailDialog"
></UserDetail>
<AclDetail
:visible="openAclDetailDialog"
:selectDetail="selectDetail"
@aclDetailDialog="closeAclDetailDialog"
></AclDetail>
<ManageProducerAuth
:visible="openManageProducerAuthDialog"
:record="selectRow"
@manageProducerAuthDialog="closeManageProducerAuthDialog"
></ManageProducerAuth>
<ManageConsumerAuth
:visible="openManageConsumerAuthDialog"
:record="selectRow"
@manageConsumerAuthDialog="closeManageConsumerAuthDialog"
></ManageConsumerAuth>
<AddAuth
:visible="openAddAuthDialog"
:record="selectRow"
@addAuthDialog="closeAddAuthDialog"
></AddAuth>
</div>
</a-spin>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import UpdateUser from "@/views/acl/UpdateUser";
import { KafkaAclApi } from "@/utils/api";
import ManageProducerAuth from "@/views/acl/ManageProducerAuth";
import ManageConsumerAuth from "@/views/acl/ManageConsumerAuth";
import AddAuth from "@/views/acl/AddAuth";
import AclDetail from "@/views/acl/AclDetail";
import UserDetail from "@/views/acl/UserDetail";
import { mapState } from "vuex";
export default {
name: "SaslScram",
components: {
UpdateUser,
ManageProducerAuth,
ManageConsumerAuth,
AddAuth,
AclDetail,
UserDetail,
},
data() {
return {
queryParam: {},
data: [],
columns,
selectRow: {},
form: this.$form.createForm(this, { name: "advanced_search" }),
showUpdateUser: false,
deleteUserConfirm: false,
openManageProducerAuthDialog: false,
openManageConsumerAuthDialog: false,
openAddAuthDialog: false,
openAclDetailDialog: false,
openUserDetailDialog: false,
selectDetail: {
resourceName: "",
resourceType: "",
username: "",
},
loading: false,
};
},
methods: {
handleSearch(e) {
e.preventDefault();
this.form.validateFields((error, values) => {
let queryParam = {};
if (values.username) {
queryParam.username = values.username;
}
if (values.topic) {
queryParam.resourceType = "TOPIC";
queryParam.resourceName = values.topic;
} else if (values.groupId) {
queryParam.resourceType = "GROUP";
queryParam.resourceName = values.groupId;
}
this.queryParam = {};
Object.assign(this.queryParam, queryParam);
this.getSaslScramUserList();
});
},
handleReset() {
this.form.resetFields();
},
updateUser() {
this.showUpdateUser = true;
},
closeUpdateUserDialog(data) {
this.showUpdateUser = data.show;
if (data.ok) {
this.getSaslScramUserList();
}
},
onDeleteUser(row) {
this.loading = true;
request({
url: KafkaAclApi.deleteSaslScramUser.url,
method: KafkaAclApi.deleteSaslScramUser.method,
data: { username: row.username },
}).then((res) => {
this.loading = false;
this.getSaslScramUserList();
if (res.code == 0) {
this.$message.success(res.msg);
} else {
this.$message.error(res.msg);
}
});
},
onDeleteUserAndAuth(row) {
this.loading = true;
request({
url: KafkaAclApi.deleteKafkaUser.url,
method: KafkaAclApi.deleteKafkaUser.method,
data: { username: row.username },
}).then((res) => {
this.loading = false;
this.getSaslScramUserList();
if (res.code == 0) {
this.$message.success(res.msg);
} else {
this.$message.error(res.msg);
}
});
},
onManageProducerAuth(row) {
this.openManageProducerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onManageConsumerAuth(row) {
this.openManageConsumerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onAddAuth(row) {
this.openAddAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onTopicDetail(topic, username) {
this.selectDetail.resourceType = "TOPIC";
this.selectDetail.resourceName = topic;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onGroupDetail(group, username) {
this.selectDetail.resourceType = "GROUP";
this.selectDetail.resourceName = group;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onUserDetail(username) {
this.selectDetail.username = username;
this.openUserDetailDialog = true;
},
closeManageProducerAuthDialog() {
this.openManageProducerAuthDialog = false;
},
closeManageConsumerAuthDialog() {
this.openManageConsumerAuthDialog = false;
},
closeAddAuthDialog() {
this.openAddAuthDialog = false;
},
closeAclDetailDialog(p) {
this.openAclDetailDialog = false;
if (p.refresh) {
this.getSaslScramUserList();
}
},
closeUserDetailDialog() {
this.openUserDetailDialog = false;
},
getSaslScramUserList() {
if (!this.enableSasl) {
return;
}
this.loading = true;
request({
url: KafkaAclApi.getSaslScramUserList.url,
method: KafkaAclApi.getSaslScramUserList.method,
params: this.queryParam,
}).then((response) => {
this.loading = false;
this.data.splice(0, this.data.length);
if (response.code != 0) {
notification.error({
message: response.msg,
});
return;
}
for (let k in response.data.map) {
let v = response.data.map[k];
let topicList = Object.keys(v)
.filter((e) => e.startsWith("TOPIC"))
.map((e) => e.split("#")[1]);
let groupList = Object.keys(v)
.filter((e) => e.startsWith("GROUP"))
.map((e) => e.split("#")[1]);
this.data.push({
key: k,
username: k,
topicList: topicList,
groupList: groupList,
user: response.data.map[k]["USER"],
});
this.data.sort((a, b) => a.username.localeCompare(b.username));
}
});
},
},
created() {
this.getSaslScramUserList();
},
computed: {
...mapState({
enableSasl: (state) => state.clusterInfo.enableSasl,
}),
},
};
const columns = [
{
title: "用户名",
dataIndex: "username",
key: "username",
width: 300,
slots: { title: "username" },
scopedSlots: { customRender: "username" },
},
{
title: "操作",
key: "operation",
scopedSlots: { customRender: "operation" },
width: 500,
},
];
</script>
<style scoped>
.acl {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-acl-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#components-form-acl-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.input-w {
width: 400px;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 8px;
}
.operation-btn {
margin-right: 3%;
}
</style>

View File

@@ -132,9 +132,9 @@ export default {
if (
clusterInfo &&
clusterInfo.id &&
clusterInfo.id == this.clusterInfo.id &&
clusterInfo.clusterName != data.clusterName
clusterInfo.id == this.clusterInfo.id
) {
// &&clusterInfo.clusterName != data.clusterName
this.switchCluster(data);
}
}

View File

@@ -50,25 +50,33 @@
>新增</a-button
>
<a-popconfirm
title="删除这些Topic?"
ok-text="确认"
cancel-text="取消"
@confirm="deleteTopics(selectedRowKeys)"
title="删除这些Topic?"
ok-text="确认"
cancel-text="取消"
@confirm="deleteTopics(selectedRowKeys)"
>
<a-button type="danger" class="btn-left" :disabled="!hasSelected" :loading="loading">
<a-button
type="danger"
class="btn-left"
:disabled="!hasSelected"
:loading="loading"
>
批量删除
</a-button>
</a-popconfirm>
<span style="margin-left: 8px">
<template v-if="hasSelected">
{{ `已选择 ${selectedRowKeys.length} 个Topic` }}
</template>
</span>
<template v-if="hasSelected">
{{ `已选择 ${selectedRowKeys.length} 个Topic` }}
</template>
</span>
</div>
<a-table
:columns="columns"
:data-source="filteredData"
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
:row-selection="{
selectedRowKeys: selectedRowKeys,
onChange: onSelectChange,
}"
bordered
row-key="name"
>
@@ -282,7 +290,7 @@ export default {
request({
url: KafkaTopicApi.deleteTopic.url,
method: KafkaTopicApi.deleteTopic.method,
data: topics
data: topics,
}).then((res) => {
if (res.code == 0) {
this.$message.success(res.msg);
@@ -297,7 +305,7 @@ export default {
});
},
deleteTopic(topic) {
this.deleteTopics([topic])
this.deleteTopics([topic]);
},
onTopicUpdate(input) {
this.filterTopic = input.target.value;