-
Notifications
You must be signed in to change notification settings - Fork 7.3k
ZOOKEEPER-1260:Audit logging in ZooKeeper servers. #338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,3 +63,20 @@ log4j.appender.TRACEFILE.File=${zookeeper.tracelog.dir}/${zookeeper.tracelog.fil | |
| log4j.appender.TRACEFILE.layout=org.apache.log4j.PatternLayout | ||
| ### Notice we are including log4j's NDC here (%x) | ||
| log4j.appender.TRACEFILE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L][%x] - %m%n | ||
| # | ||
| # zk audit logging | ||
| # | ||
| zookeeper.auditlog.file=zookeeper_audit.log | ||
| zookeeper.auditlog.threshold=INFO | ||
| audit.logger=INFO, RFAAUDIT | ||
| log4j.logger.org.apache.zookeeper.audit.ZKAuditLogger=${audit.logger} | ||
| log4j.additivity.org.apache.zookeeper.audit.ZKAuditLogger=false | ||
| log4j.appender.RFAAUDIT=org.apache.log4j.RollingFileAppender | ||
| log4j.appender.RFAAUDIT.File=${zookeeper.log.dir}/${zookeeper.auditlog.file} | ||
| log4j.appender.RFAAUDIT.layout=org.apache.log4j.PatternLayout | ||
| log4j.appender.RFAAUDIT.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why don't we use the same conversion pattern as the other log types?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logging is done only from one class, which thread is logging is not important. so only required parameters are logged. |
||
| log4j.appender.RFAAUDIT.Threshold=${zookeeper.auditlog.threshold} | ||
|
|
||
| # Max log file size of 10MB | ||
| log4j.appender.RFAAUDIT.MaxFileSize=10MB | ||
| log4j.appender.RFAAUDIT.MaxBackupIndex=10 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,18 +20,22 @@ | |
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.Deque; | ||
| import java.util.HashMap; | ||
| import java.util.LinkedList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| import org.apache.zookeeper.AsyncCallback.StringCallback; | ||
| import org.apache.zookeeper.AsyncCallback.VoidCallback; | ||
| import org.apache.zookeeper.KeeperException.Code; | ||
| import org.apache.zookeeper.common.PathUtils; | ||
| import org.apache.zookeeper.data.ACL; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| public class ZKUtil { | ||
| private static final Logger LOG = LoggerFactory.getLogger(ZKUtil.class); | ||
| private static final Map<Integer, String> permCache = new HashMap<Integer, String>(); | ||
| /** | ||
| * Recursively delete the node with the given path. | ||
| * <p> | ||
|
|
@@ -168,4 +172,53 @@ private static void visitSubTreeDFSHelper(ZooKeeper zk, final String path, | |
| return; // ignore | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @param perms | ||
| * ACL permissions | ||
| * @return string representation of permissions | ||
| */ | ||
| public static String getPermString(int perms) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered caching these values in some way?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good suggestion |
||
| String permString = permCache.get(perms); | ||
| if (permString != null) { | ||
| return permString; | ||
| } | ||
| permString = constructPermString(perms); | ||
| if (!permString.isEmpty()) { | ||
| permCache.put(perms, permString); | ||
| } | ||
| return permString; | ||
| } | ||
|
|
||
| private static String constructPermString(int perms) { | ||
| StringBuilder p = new StringBuilder(); | ||
| if ((perms & ZooDefs.Perms.CREATE) != 0) { | ||
| p.append('c'); | ||
| } | ||
| if ((perms & ZooDefs.Perms.DELETE) != 0) { | ||
| p.append('d'); | ||
| } | ||
| if ((perms & ZooDefs.Perms.READ) != 0) { | ||
| p.append('r'); | ||
| } | ||
| if ((perms & ZooDefs.Perms.WRITE) != 0) { | ||
| p.append('w'); | ||
| } | ||
| if ((perms & ZooDefs.Perms.ADMIN) != 0) { | ||
| p.append('a'); | ||
| } | ||
| return p.toString(); | ||
| } | ||
|
|
||
| public static String aclToString(List<ACL> acls) { | ||
| StringBuilder sb = new StringBuilder(); | ||
| for (ACL acl : acls) { | ||
| sb.append(acl.getId().getScheme()); | ||
| sb.append(":"); | ||
| sb.append(acl.getId().getId()); | ||
| sb.append(":"); | ||
| sb.append(getPermString(acl.getPerms())); | ||
| } | ||
| return sb.toString(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /** | ||
| * 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.zookeeper.audit; | ||
|
|
||
| public class AuditConstants { | ||
| public static final String SUCCESS = "success"; | ||
| public static final String FAILURE = "failure"; | ||
| // operation is performed, result is not known yet | ||
| public static final String INVOKED = "invoked"; | ||
| public static final String KEY_VAL_SEPARATOR = "="; | ||
| public static final char PAIR_SEPARATOR = '\t'; | ||
|
|
||
| public static final String OP_START = "serverStart"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm concerned about having another listing of ZooKeeper operations. Can we possibly do something in ZooDefs that keeps these string versions of the operations closer to the Opcodes?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should not expose the internal op codes, etc to end users, through API or through the public constant definition included in this patch. We can get rid of the constants definitions here and instead, whenever we want to audit log something we create the operation string on site and directly pass the string to the audit event function call. Something like: I also checked HDFS code base and it took a similar approach.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Operation names are used multiple places through constants. So keeping the AuditConstants as it is. Also not exposing operation name through ZooDefs constants. |
||
| public static final String OP_STOP = "serverStop"; | ||
| public static final String OP_CREATE = "create"; | ||
| public static final String OP_DELETE = "delete"; | ||
| public static final String OP_SETDATA = "setData"; | ||
| public static final String OP_SETACL = "setAcl"; | ||
| public static final String OP_MULTI_OP = "multiOperation"; | ||
| public static final String OP_RECONFIG = "reconfig"; | ||
| public static final String OP_DEL_EZNODE_EXP = "ephemeralZNodeDeletionOnSessionCloseOrExpire"; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /** | ||
| * 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.zookeeper.audit; | ||
|
|
||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
| import java.util.Map.Entry; | ||
|
|
||
| public class ZKAuditLogFormatter { | ||
| private Map<String, String> fieldVsValue = new LinkedHashMap<String, String>(); | ||
|
|
||
| /** | ||
| * Add fields to be logged | ||
| */ | ||
| public void addField(String key, String value) { | ||
| fieldVsValue.put(key, value); | ||
| } | ||
|
|
||
| /** | ||
| * Gives the string to be logged, ignores fields with null values | ||
| */ | ||
| public String format() { | ||
| StringBuilder buffer = new StringBuilder(); | ||
| boolean first = true; | ||
| for (Entry<String, String> entry : fieldVsValue.entrySet()) { | ||
| String key = entry.getKey(); | ||
| String value = entry.getValue(); | ||
| if (null != value) { | ||
| // if first field then no need to add the tabs | ||
| if (first) { | ||
| first = false; | ||
| } else { | ||
| buffer.append(AuditConstants.PAIR_SEPARATOR); | ||
| } | ||
| buffer.append(key.toLowerCase()).append(AuditConstants.KEY_VAL_SEPARATOR) | ||
| .append(value); | ||
| } | ||
| } | ||
| return buffer.toString(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| /** | ||
| * 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.zookeeper.audit; | ||
|
|
||
| import org.apache.zookeeper.server.ServerCnxnFactory; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| public class ZKAuditLogger { | ||
| public static final String SYSPROP_AUDIT_ENABLE = "zookeeper.audit.enable"; | ||
| private static final Logger LOG = LoggerFactory.getLogger(ZKAuditLogger.class); | ||
| // By default audit logging is disabled | ||
| private static boolean auditEnabled = Boolean.getBoolean(SYSPROP_AUDIT_ENABLE); | ||
|
|
||
| /** | ||
| * @return true if audit log is enabled | ||
| */ | ||
| public static boolean isAuditEnabled() { | ||
| return auditEnabled; | ||
| } | ||
|
|
||
| // @VisisbleForTesting | ||
| public static void setAuditEnabled(boolean auditEnabled) { | ||
| ZKAuditLogger.auditEnabled = auditEnabled; | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * Prints audit log based on log level specified | ||
| * | ||
| */ | ||
| public static enum LogLevel { | ||
| ERROR { | ||
| @Override | ||
| public void printLog(String logMsg) { | ||
| LOG.error(logMsg); | ||
| } | ||
| }, | ||
| INFO { | ||
| @Override | ||
| public void printLog(String logMsg) { | ||
| LOG.info(logMsg); | ||
| } | ||
| }; | ||
| public abstract void printLog(String logMsg); | ||
| } | ||
|
|
||
| public static enum Keys { | ||
| USER, OPERATION, RESULT, IP, ACL, ZNODE, SESSION; | ||
| } | ||
|
|
||
| public static void logInvoked(String user, String operation) { | ||
| log(LogLevel.INFO, user, operation, AuditConstants.INVOKED); | ||
| } | ||
|
|
||
| public static void logSuccess(String user, String operation) { | ||
| log(LogLevel.INFO, user, operation, AuditConstants.SUCCESS); | ||
| } | ||
|
|
||
| public static void logFailure(String user, String operation) { | ||
| log(LogLevel.ERROR, user, operation, AuditConstants.FAILURE); | ||
| } | ||
|
|
||
| private static void log(LogLevel level, String user, String operation, String logType) { | ||
| level.printLog(createLog(user, operation, null, null, null, null, logType)); | ||
| } | ||
|
|
||
| public static void logSuccess(String user, String operation, String znode, String acl, | ||
| String session, String ip) { | ||
| LogLevel.INFO.printLog( | ||
| createLog(user, operation, znode, acl, session, ip, AuditConstants.SUCCESS)); | ||
| } | ||
|
|
||
| public static void logFailure(String user, String operation, String znode, String acl, | ||
| String session, String ip) { | ||
| LogLevel.ERROR.printLog( | ||
| createLog(user, operation, znode, acl, session, ip, AuditConstants.FAILURE)); | ||
| } | ||
|
|
||
| /** | ||
| * A helper api for creating an audit log string. | ||
| */ | ||
| public static String createLog(String user, String operation, String znode, String acl, | ||
| String session, String ip, String status) { | ||
| ZKAuditLogFormatter fmt = new ZKAuditLogFormatter(); | ||
| fmt.addField(Keys.SESSION.name(), session); | ||
| fmt.addField(Keys.USER.name(), user); | ||
| fmt.addField(Keys.IP.name(), ip); | ||
| fmt.addField(Keys.OPERATION.name(), operation); | ||
| fmt.addField(Keys.ZNODE.name(), znode); | ||
| fmt.addField(Keys.ACL.name(), acl); | ||
| fmt.addField(Keys.RESULT.name(), status); | ||
| return fmt.format(); | ||
| } | ||
|
|
||
| /** | ||
| * add audit log for server start and register server stop log | ||
| */ | ||
| public static void addZKStartStopAuditLog() { | ||
| if (isAuditEnabled()) { | ||
| ZKAuditLogger.logSuccess(getZKUser(), AuditConstants.OP_START); | ||
| Runtime.getRuntime().addShutdownHook(new Thread() { | ||
| @Override | ||
| public void run() { | ||
| ZKAuditLogger.logInvoked(getZKUser(), AuditConstants.OP_STOP); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * User who has started the ZooKeeper server user, it will be the logged-in | ||
| * user. If no user logged-in then system user | ||
| */ | ||
| public static String getZKUser() { | ||
| return ServerCnxnFactory.getUserName(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sure this is a very dumb question, why is this called RFAAUDIT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is very valid question. By RFAAUDIT i meant Rolling File Audit. Also one A is extra by mistake. I think it is better to change the name to AUDITFILE.