diff --git a/common/src/main/java/org/apache/hertzbeat/common/constants/DesensitizedField.java b/common/src/main/java/org/apache/hertzbeat/common/constants/DesensitizedField.java new file mode 100644 index 00000000000..d78040af098 --- /dev/null +++ b/common/src/main/java/org/apache/hertzbeat/common/constants/DesensitizedField.java @@ -0,0 +1,34 @@ +/* + * 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. + */ + +package org.apache.hertzbeat.common.constants; + +import org.apache.hertzbeat.common.util.DesensitizedUtil; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD}) +@Retention(RUNTIME) +@Documented +public @interface DesensitizedField { + DesensitizedUtil.DesensitizedType desensitizedType(); +} diff --git a/common/src/main/java/org/apache/hertzbeat/common/entity/manager/NoticeReceiver.java b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/NoticeReceiver.java index af03830b279..01ac4017a41 100644 --- a/common/src/main/java/org/apache/hertzbeat/common/entity/manager/NoticeReceiver.java +++ b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/NoticeReceiver.java @@ -19,6 +19,7 @@ import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; + import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -36,6 +37,8 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.hertzbeat.common.constants.DesensitizedField; +import org.apache.hertzbeat.common.util.DesensitizedUtil; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; @@ -83,12 +86,14 @@ public class NoticeReceiver { description = "Mobile number: Valid when the notification method is SMS", example = "18923435643", accessMode = READ_WRITE) @Size(max = 100) + @DesensitizedField(desensitizedType= DesensitizedUtil.DesensitizedType.MOBILE_PHONE) private String phone; @Schema(title = "Email account: Valid when the notification method is email", description = "Email account: Valid when the notification method is email", example = "tom@qq.com", accessMode = READ_WRITE) @Size(max = 100) + @DesensitizedField(desensitizedType= DesensitizedUtil.DesensitizedType.EMAIL) private String email; @Schema(title = "URL address: The notification method is valid for webhook", @@ -147,6 +152,7 @@ public class NoticeReceiver { @Schema(title = "Enterprise weChat secret: The notification method is valid for Enterprise WeChat app message", description = "Enterprise weChat secret: The notification method is valid for Enterprise WeChat app message", example = "oUydwn92ey0lnuY02MixNa57eNK-20dJn5NEOG-u2uE", accessMode = READ_WRITE) + @DesensitizedField(desensitizedType= DesensitizedUtil.DesensitizedType.PASSWORD) private String appSecret; @Schema(title = "Enterprise weChat party id: The notification method is valid for Enterprise WeChat app message", diff --git a/common/src/main/java/org/apache/hertzbeat/common/util/DesensitizedUtil.java b/common/src/main/java/org/apache/hertzbeat/common/util/DesensitizedUtil.java new file mode 100644 index 00000000000..28b87ffcb0c --- /dev/null +++ b/common/src/main/java/org/apache/hertzbeat/common/util/DesensitizedUtil.java @@ -0,0 +1,119 @@ +/* + * 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. + */ + +package org.apache.hertzbeat.common.util; + +import org.apache.commons.lang3.StringUtils; + +/** + * desensitize field utility + */ +public class DesensitizedUtil { + + /** + * desensitize field + */ + public enum DesensitizedType { + MOBILE_PHONE, + EMAIL, + PASSWORD + } + + /** + * desensitize field + * @param type field type + * @param str field value + * @return desensitized value + */ + public static String desensitize(DesensitizedType type, String str) { + if (type == null || StringUtils.isEmpty(str)) { + return str; + } + switch (type) { + case MOBILE_PHONE -> str = mobilePhone(str); + case EMAIL -> str = email(str); + case PASSWORD -> str = password(str); + default -> {} + } + return str; + } + + /** + * desensitize mobile phone + * @param str field value + * @return desensitized value + */ + public static String mobilePhone(String str) { + if (StringUtils.isEmpty(str)) { + return ""; + } + return desensitized(str, 3, str.length() - 4); + } + + /** + * desensitize email + * @param str field value + * @return desensitized value + */ + public static String email(String str) { + if (StringUtils.isEmpty(str)) { + return ""; + } + int index = str.indexOf("@"); + if (index <= 1) { + return str; + } + return desensitized(str, 1, index); + } + + /** + * desensitize password + * @param str field value + * @return desensitized value + */ + public static String password(String str) { + if (StringUtils.isEmpty(str)) { + return ""; + } + return desensitized(str, 0, str.length()); + } + + /** + * replace char to * + * @param str field value + * @param start start index + * @param end end index + * @return desensitized value + */ + private static String desensitized(String str, int start, int end) { + if (StringUtils.isEmpty(str)) { + return ""; + } + if (start < 0 || end < 0 || end > str.length() || start >= end) { + return str; + } + final StringBuilder result = new StringBuilder(); + for (int i = 0; i < str.length(); i++) { + if (i >= start && i < end) { + result.append("*"); + } else { + result.append(str.charAt(i)); + } + } + return result.toString(); + } +} diff --git a/manager/src/main/java/org/apache/hertzbeat/manager/aspect/DesensitizedAspect.java b/manager/src/main/java/org/apache/hertzbeat/manager/aspect/DesensitizedAspect.java new file mode 100644 index 00000000000..3d203367c24 --- /dev/null +++ b/manager/src/main/java/org/apache/hertzbeat/manager/aspect/DesensitizedAspect.java @@ -0,0 +1,113 @@ +/* + * 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. + */ + +package org.apache.hertzbeat.manager.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.DesensitizedField; +import org.apache.hertzbeat.common.entity.manager.NoticeReceiver; +import org.apache.hertzbeat.common.util.DesensitizedUtil; +import org.apache.hertzbeat.manager.dao.NoticeReceiverDao; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.lang.reflect.Field; +import java.util.List; + +@Aspect +@Component +@Slf4j +public class DesensitizedAspect { + @Resource + private NoticeReceiverDao noticeReceiverDao; + + @Around("execution(* org.apache.hertzbeat.manager.service.NoticeConfigService.*(..)))") + public Object around(ProceedingJoinPoint point) { + try { + unDesensitized(point.getArgs()); + Object result = point.proceed(); + return desensitized(result); + } catch (Throwable e) { + log.error(e.getMessage(), e); + } + return null; + } + + private void unDesensitized(Object[] args) throws IllegalAccessException { + if (args == null || args.length == 0) { + return; + } + Long receiverId = null; + for (Object arg : args) { + if (arg instanceof NoticeReceiver argNoticeReceiver) { + receiverId = argNoticeReceiver.getId(); + break; + } + } + if (receiverId == null) { + return; + } + NoticeReceiver noticeReceiver = noticeReceiverDao.findById(receiverId).orElse(null); + if (noticeReceiver == null) { + return; + } + for (Object arg : args) { + if (arg instanceof NoticeReceiver argNoticeReceiver) { + for (Field field : argNoticeReceiver.getClass().getDeclaredFields()) { + DesensitizedField annotation = field.getAnnotation(DesensitizedField.class); + if (annotation != null) { + field.setAccessible(true); + DesensitizedUtil.DesensitizedType desensitizedType = annotation.desensitizedType(); + String desensitizedValue = DesensitizedUtil.desensitize(desensitizedType, field.get(noticeReceiver).toString()); + if (field.get(argNoticeReceiver) != null && field.get(argNoticeReceiver).equals(desensitizedValue)) { + field.set(argNoticeReceiver, field.get(noticeReceiver)); + } + } + } + } + } + } + + private Object desensitized(Object result) throws IllegalAccessException { + if (result == null) { + return null; + } + if (result instanceof List) { + for (Object item : ((List) result)) { + desensitizedField(item); + } + } else { + desensitizedField(result); + } + return result; + } + + private void desensitizedField(Object result) throws IllegalAccessException { + for (Field field : result.getClass().getDeclaredFields()) { + DesensitizedField annotation = field.getAnnotation(DesensitizedField.class); + if (annotation != null) { + DesensitizedUtil.DesensitizedType desensitizedType = annotation.desensitizedType(); + field.setAccessible(true); + field.set(result, DesensitizedUtil.desensitize(desensitizedType, field.get(result).toString())); + } + } + } + +}