kafka acl user search and show list

This commit is contained in:
许晓东
2021-09-01 16:23:30 +08:00
parent 5d0b85ef45
commit aeab25939d
13 changed files with 537 additions and 203 deletions

20
pom.xml
View File

@@ -78,14 +78,30 @@
</dependencies> </dependencies>
<build> <build>
<finalName>${project.artifactId}</finalName>
<!-- <sourceDirectory>${basedir}/src/main</sourceDirectory>-->
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
</plugin> </plugin>
</plugins>
</build>
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>deploy</id>
<build>
<plugins>
<plugin> <plugin>
<groupId>org.scala-tools</groupId> <groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId> <artifactId>maven-scala-plugin</artifactId>
@@ -190,5 +206,7 @@
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
</profile>
</profiles>
</project> </project>

View File

@@ -1,7 +1,6 @@
package com.xuxd.kafka.console.beans; package com.xuxd.kafka.console.beans;
import java.util.Objects; import java.util.Objects;
import lombok.Data;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.common.acl.AccessControlEntry; import org.apache.kafka.common.acl.AccessControlEntry;
import org.apache.kafka.common.acl.AccessControlEntryFilter; import org.apache.kafka.common.acl.AccessControlEntryFilter;
@@ -21,7 +20,6 @@ import org.apache.kafka.common.security.auth.KafkaPrincipal;
* @author xuxd * @author xuxd
* @date 2021-08-28 20:17:27 * @date 2021-08-28 20:17:27
**/ **/
@Data
public class AclEntry { public class AclEntry {
private String resourceType; private String resourceType;
@@ -100,4 +98,72 @@ public class AclEntry {
entry.setPermissionType(this.permissionType); entry.setPermissionType(this.permissionType);
return entry; return entry;
} }
public String getResourceType() {
return resourceType;
}
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPatternType() {
return patternType;
}
public void setPatternType(String patternType) {
this.patternType = patternType;
}
public String getPrincipal() {
return principal;
}
public void setPrincipal(String principal) {
this.principal = principal;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getOperation() {
return operation;
}
public void setOperation(String operation) {
this.operation = operation;
}
public String getPermissionType() {
return permissionType;
}
public void setPermissionType(String permissionType) {
this.permissionType = permissionType;
}
@Override public String toString() {
return "AclEntry{" +
"resourceType='" + resourceType + '\'' +
", name='" + name + '\'' +
", patternType='" + patternType + '\'' +
", principal='" + principal + '\'' +
", host='" + host + '\'' +
", operation='" + operation + '\'' +
", permissionType='" + permissionType + '\'' +
'}';
}
} }

View File

@@ -6,6 +6,7 @@ import com.xuxd.kafka.console.beans.CounterMap;
import com.xuxd.kafka.console.beans.ResponseData; import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.service.AclService; import com.xuxd.kafka.console.service.AclService;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -13,6 +14,7 @@ import java.util.stream.Collectors;
import kafka.console.KafkaAclConsole; import kafka.console.KafkaAclConsole;
import kafka.console.KafkaConfigConsole; import kafka.console.KafkaConfigConsole;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.common.acl.AclBinding; import org.apache.kafka.common.acl.AclBinding;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -35,7 +37,7 @@ public class AclServiceImpl implements AclService {
@Override public ResponseData<Set<String>> getUserList() { @Override public ResponseData<Set<String>> getUserList() {
try { try {
return ResponseData.create(Set.class).data(configConsole.getUserList()).success(); return ResponseData.create(Set.class).data(configConsole.getUserList(null)).success();
} catch (Exception e) { } catch (Exception e) {
log.error("getUserList error.", e); log.error("getUserList error.", e);
return ResponseData.create().failed(); return ResponseData.create().failed();
@@ -60,8 +62,21 @@ public class AclServiceImpl implements AclService {
List<AclBinding> aclBindingList = entry.isNull() ? aclConsole.getAclList(null) : aclConsole.getAclList(entry); List<AclBinding> aclBindingList = entry.isNull() ? aclConsole.getAclList(null) : aclConsole.getAclList(entry);
List<AclEntry> entryList = aclBindingList.stream().map(x -> AclEntry.valueOf(x)).collect(Collectors.toList()); 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, List<AclEntry>> entryMap = entryList.stream().collect(Collectors.groupingBy(AclEntry::getPrincipal));
Map<String, Map<String, List<AclEntry>>> resultMap = new HashMap<>();
entryMap.forEach((k, v) -> {
Map<String, List<AclEntry>> map = v.stream().collect(Collectors.groupingBy(e -> e.getResourceType() + "#" + e.getName()));
resultMap.put(k, map);
});
if (entry.isNull() || StringUtils.isNotBlank(entry.getPrincipal())) {
Set<String> userList = configConsole.getUserList(StringUtils.isNotBlank(entry.getPrincipal()) ? Collections.singletonList(entry.getPrincipal()) : null);
userList.forEach(u -> {
if (!resultMap.containsKey(u)) {
resultMap.put(u, Collections.emptyMap());
}
});
}
return ResponseData.create().data(new CounterMap<>(entryMap)).success(); return ResponseData.create().data(new CounterMap<>(resultMap)).success();
} }
@Override public ResponseData deleteAcl(AclEntry entry) { @Override public ResponseData deleteAcl(AclEntry entry) {

View File

@@ -1,5 +1,7 @@
server: server:
port: 7766 port: 7766
servlet:
context-path: /kafka-console
kafka: kafka:
config: config:

View File

@@ -41,7 +41,7 @@ class KafkaAclConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaCon
} }
var principal: String = null var principal: String = null
if ( StringUtils.isNotBlank(entry.getPrincipal) && !KafkaPrincipal.ANONYMOUS.toString.equalsIgnoreCase(f.entryFilter().principal())) { if (StringUtils.isNotBlank(entry.getPrincipal()) && !KafkaPrincipal.ANONYMOUS.toString.equalsIgnoreCase(f.entryFilter().principal())) {
principal = f.entryFilter().principal(); principal = f.entryFilter().principal();
} }
val filter = new AclBindingFilter(new ResourcePatternFilter(resourceType, name, f.patternFilter().patternType()), val filter = new AclBindingFilter(new ResourcePatternFilter(resourceType, name, f.patternFilter().patternType()),

View File

@@ -17,9 +17,9 @@ class KafkaConfigConsole(config: KafkaConfig) extends KafkaConsole(config: Kafka
private val defaultIterations = 4096 private val defaultIterations = 4096
def getUserList(): Set[String] = { def getUserList(users: util.List[String]): Set[String] = {
withAdminClient({ withAdminClient({
adminClient => adminClient.describeUserScramCredentials().all().get().keySet() adminClient => adminClient.describeUserScramCredentials(users).all().get().keySet()
}).asInstanceOf[Set[String]] }).asInstanceOf[Set[String]]
} }

11
ui/package-lock.json generated
View File

@@ -2587,6 +2587,14 @@
"integrity": "sha1-1h9G2DslGSUOJ4Ta9bCUeai0HFk=", "integrity": "sha1-1h9G2DslGSUOJ4Ta9bCUeai0HFk=",
"dev": true "dev": true
}, },
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"babel-eslint": { "babel-eslint": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npm.taobao.org/babel-eslint/download/babel-eslint-10.1.0.tgz", "resolved": "https://registry.npm.taobao.org/babel-eslint/download/babel-eslint-10.1.0.tgz",
@@ -5826,8 +5834,7 @@
"follow-redirects": { "follow-redirects": {
"version": "1.14.2", "version": "1.14.2",
"resolved": "https://registry.nlark.com/follow-redirects/download/follow-redirects-1.14.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.nlark.com%2Ffollow-redirects%2Fdownload%2Ffollow-redirects-1.14.2.tgz", "resolved": "https://registry.nlark.com/follow-redirects/download/follow-redirects-1.14.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.nlark.com%2Ffollow-redirects%2Fdownload%2Ffollow-redirects-1.14.2.tgz",
"integrity": "sha1-zsuCUEfAD15msUL5D+1PUV3seJs=", "integrity": "sha1-zsuCUEfAD15msUL5D+1PUV3seJs="
"dev": true
}, },
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",

View File

@@ -9,6 +9,7 @@
}, },
"dependencies": { "dependencies": {
"ant-design-vue": "^1.7.8", "ant-design-vue": "^1.7.8",
"axios": "^0.21.1",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"moment": "^2.29.1", "moment": "^2.29.1",
"vue": "^2.6.11", "vue": "^2.6.11",

View File

@@ -5,9 +5,11 @@ import store from "./store";
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import Antd from "ant-design-vue"; import Antd from "ant-design-vue";
import "ant-design-vue/dist/antd.css"; import "ant-design-vue/dist/antd.css";
import { VueAxios } from "./utils/request";
Vue.config.productionTip = false; Vue.config.productionTip = false;
Vue.use(Antd); Vue.use(Antd);
Vue.use(VueAxios);
new Vue({ new Vue({
router, router,

33
ui/src/utils/axios.js Normal file
View File

@@ -0,0 +1,33 @@
const VueAxios = {
vm: {},
// eslint-disable-next-line no-unused-vars
install(Vue, instance) {
if (this.installed) {
return;
}
this.installed = true;
if (!instance) {
// eslint-disable-next-line no-console
console.error("You have to install axios");
return;
}
Vue.axios = instance;
Object.defineProperties(Vue.prototype, {
axios: {
get: function get() {
return instance;
},
},
$http: {
get: function get() {
return instance;
},
},
});
},
};
export { VueAxios };

44
ui/src/utils/request.js Normal file
View File

@@ -0,0 +1,44 @@
import axios from "axios";
import notification from "ant-design-vue/es/notification";
import { VueAxios } from "./axios";
// 创建 axios 实例
const request = axios.create({
// API 请求的默认前缀
baseURL: "/kafka-console",
timeout: 10000, // 请求超时时间
});
// 异常拦截处理器
const errorHandler = (error) => {
if (error.response) {
const data = error.response.data;
notification.error({
message: error.response.status,
description: JSON.stringify(data),
});
}
return Promise.reject(error);
};
// request interceptor
// request.interceptors.request.use(config => {
//
// return config
// }, errorHandler)
// response interceptor
request.interceptors.response.use((response) => {
return response.data;
}, errorHandler);
const installer = {
vm: {},
install(Vue) {
Vue.use(VueAxios, request);
},
};
export default request;
export { installer as VueAxios, request as axios };

View File

@@ -1,6 +1,50 @@
<template> <template>
<div class="content"> <div class="content">
<div class="acl"> <div class="acl">
<div id="components-form-demo-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>
</div>
<a-table :columns="columns" :data-source="data" bordered> <a-table :columns="columns" :data-source="data" bordered>
<a slot="operation" slot-scope="{}">删除</a> <a slot="operation" slot-scope="{}">删除</a>
<!-- <a-table--> <!-- <a-table-->
@@ -33,19 +77,87 @@
</template> </template>
<script> <script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
export default { export default {
name: "Acl", name: "Acl",
data() { data() {
return { return {
data, queryParam: {},
data: [],
columns, columns,
innerColumns, innerColumns,
innerData, innerData,
form: this.$form.createForm(this, { name: "advanced_search" }),
}; };
}, },
methods: {}, 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;
}
getAclList(this.data, queryParam);
});
},
handleReset() {
this.form.resetFields();
},
},
created() {
getAclList(this.data, this.queryParam);
},
}; };
const api = {
getAclList: {
url: "/acl/list",
method: "post",
},
};
function getAclList(data, requestParameters) {
request({
url: api.getAclList.url,
method: api.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.join(", "),
groupList: groupList.join(", "),
});
}
});
}
const columns = [ const columns = [
{ title: "用户名", dataIndex: "username", key: "username" }, { title: "用户名", dataIndex: "username", key: "username" },
{ title: "topic列表", dataIndex: "topicList", key: "topicList" }, { title: "topic列表", dataIndex: "topicList", key: "topicList" },
@@ -57,20 +169,6 @@ const columns = [
}, },
]; ];
const data = [];
for (let i = 0; i < 30; ++i) {
data.push({
key: i,
username: "amdin",
topicList: ["test_topic1", "test_topic2", "test_topic3"].join(", "),
groupList: [
"test_topic1_consumer",
"test_topic2_consumer",
"test_topic3_consumer",
].join(", "),
});
}
const innerColumns = [ const innerColumns = [
{ title: "Date", dataIndex: "date", key: "date" }, { title: "Date", dataIndex: "date", key: "date" },
{ title: "Name", dataIndex: "name", key: "name" }, { title: "Name", dataIndex: "name", key: "name" },
@@ -100,4 +198,38 @@ for (let i = 0; i < 3; ++i) {
width: 100%; width: 100%;
height: 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-demo-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#components-form-demo-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;
}
</style> </style>

14
ui/vue.config.js Normal file
View File

@@ -0,0 +1,14 @@
module.exports = {
productionSourceMap: process.env.NODE_ENV !== "production",
devServer: {
proxy: {
"/kafka-console": {
target: `${process.env.SW_PROXY_TARGET || "http://127.0.0.1:7766"}`,
changeOrigin: true,
// pathRewrite: {
// '^/kafka-console': '/'
// }
},
},
},
};