content: /*
   * 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.cassandra.audit;
  
  import java.nio.file.Path;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Optional;
  import java.util.stream.Collectors;
  
  import com.google.common.base.Strings;
  import org.apache.commons.lang3.StringUtils;
  
  import org.apache.cassandra.config.CassandraRelevantProperties;
  import org.apache.cassandra.config.ParameterizedClass;
  import org.apache.cassandra.exceptions.ConfigurationException;
  import org.apache.cassandra.io.util.File;
  import org.apache.cassandra.utils.binlog.BinLogOptions;
  
  import static org.apache.cassandra.utils.LocalizeString.toUpperCaseLocalized;
  
  public class AuditLogOptions extends BinLogOptions
  {
      public volatile boolean enabled = false;
      public ParameterizedClass logger = new ParameterizedClass(BinAuditLogger.class.getSimpleName(), Collections.emptyMap());
      public String included_keyspaces = StringUtils.EMPTY;
      // CASSANDRA-14498: By default, system, system_schema and system_virtual_schema are excluded, but these can be included via cassandra.yaml
      public String excluded_keyspaces = "system,system_schema,system_virtual_schema";
      public String included_categories = StringUtils.EMPTY;
      public String excluded_categories = StringUtils.EMPTY;
      public String included_users = StringUtils.EMPTY;
      public String excluded_users = StringUtils.EMPTY;
  
      public String audit_logs_dir;
  
      public AuditLogOptions()
      {
          String auditLogDir = CassandraRelevantProperties.LOG_DIR_AUDIT.getString();
          String logDir = CassandraRelevantProperties.LOG_DIR.getString() + "/audit";
          Path path = auditLogDir == null ? File.getPath(logDir) : File.getPath(auditLogDir);
          audit_logs_dir = path.normalize().toString();
      }
  
      public static AuditLogOptions validate(final AuditLogOptions options) throws ConfigurationException
      {
          // not validating keyspaces nor users on purpose,
          // logging might be enabled on these entities before they exist
          // so they are picked up automatically
  
          validateCategories(options.included_categories);
          validateCategories(options.excluded_categories);
  
          // other fields in BinLogOptions are validated upon BinAuditLogger initialisation
  
          return options;
      }
  
      public static class Builder
      {
          private boolean enabled;
          private ParameterizedClass logger;
          private String includedKeyspaces;
          private String excludedKeyspaces;
          private String includedCategories;
          private String excludedCategories;
          private String includedUsers;
          private String excludedUsers;
          private String auditLogDir;
          private int maxQueueWeight;
          private int maxArchiveRetries;
          private String rollCycle;
          private String archiveCommand;
          private boolean block;
          private long maxLogSize;
  
          public Builder()
          {
              this(new AuditLogOptions());
          }
  
          public Builder(final AuditLogOptions opts)
          {
              this.enabled = opts.enabled;
              this.logger = opts.logger;
              this.includedKeyspaces = opts.included_keyspaces;
              this.excludedKeyspaces = opts.excluded_keyspaces;
              this.includedCategories = opts.included_categories;
              this.excludedCategories = opts.excluded_categories;
              this.includedUsers = opts.included_users;
              this.excludedUsers = opts.excluded_users;
              this.auditLogDir = opts.audit_logs_dir;
              this.maxQueueWeight = opts.max_queue_weight;
              this.maxArchiveRetries = opts.max_archive_retries;
              this.rollCycle = opts.roll_cycle;
              this.archiveCommand = opts.archive_command;
              this.block = opts.block;
              this.maxLogSize = opts.max_log_size;
          }
  
          public Builder withEnabled(boolean enabled)
          {
              this.enabled = enabled;
              return this;
          }
  
          public Builder withLogger(final String loggerName, Map<String, String> parameters)
          {
  
              if (loggerName != null && !loggerName.trim().isEmpty())
              {
                  this.logger = new ParameterizedClass(loggerName.trim(), parameters);
              }
  
              return this;
          }
  
          public Builder withIncludedKeyspaces(final String includedKeyspaces)
          {
              sanitise(includedKeyspaces).map(v -> this.includedKeyspaces = v);
              return this;
          }
  
          public Builder withExcludedKeyspaces(final String excludedKeyspaces)
          {
              sanitise(excludedKeyspaces).map(v -> this.excludedKeyspaces = v);
              return this;
          }
  
          public Builder withIncludedCategories(final String includedCategories)
          {
              sanitise(includedCategories).map(v -> this.includedCategories = toUpperCaseLocalized(v));
              return this;
          }
  
          public Builder withExcludedCategories(final String excludedCategories)
          {
              sanitise(excludedCategories).map(v -> this.excludedCategories = toUpperCaseLocalized(v));
              return this;
          }
  
          public Builder withIncludedUsers(final String includedUsers)
          {
              sanitise(includedUsers).map(v -> this.includedUsers = v);
              return this;
          }
  
          public Builder withExcludedUsers(final String excludedUsers)
          {
              sanitise(excludedUsers).map(v -> this.excludedUsers = v);
              return this;
          }
  
          public Builder withAuditLogDir(final String auditLogDir)
          {
              this.auditLogDir = auditLogDir;
              return this;
          }
  
          public Builder withRollCycle(final String rollCycle)
          {
              sanitise(rollCycle).map(v -> this.rollCycle = toUpperCaseLocalized(v));
              return this;
          }
  
          public Builder withArchiveCommand(final String archiveCommand)
          {
              if (archiveCommand != null)
              {
                  this.archiveCommand = archiveCommand;
              }
              return this;
          }
  
          public Builder withBlock(final Boolean block)
          {
              if (block != null)
              {
                  this.block = block;
              }
              return this;
          }
  
          public Builder withMaxLogSize(final long maxLogSize)
          {
              if (maxLogSize != Long.MIN_VALUE)
              {
                  this.maxLogSize = maxLogSize;
              }
              return this;
          }
  
          public Builder withMaxArchiveRetries(final int maxArchiveRetries)
          {
              if (maxArchiveRetries != Integer.MIN_VALUE)
              {
                  this.maxArchiveRetries = maxArchiveRetries;
              }
              return this;
          }
  
          public Builder withMaxQueueWeight(final int maxQueueWeight)
          {
              if (maxQueueWeight != Integer.MIN_VALUE)
              {
                  this.maxQueueWeight = maxQueueWeight;
              }
              return this;
          }
  
          public AuditLogOptions build()
          {
              final AuditLogOptions opts = new AuditLogOptions();
  
              opts.enabled = this.enabled;
              opts.logger = this.logger;
              sanitise(this.includedKeyspaces).map(v -> opts.included_keyspaces = v);
              sanitise(this.excludedKeyspaces).map(v -> opts.excluded_keyspaces = v);
              sanitise(this.includedCategories).map(v -> opts.included_categories = toUpperCaseLocalized(v));
              sanitise(this.excludedCategories).map(v -> opts.excluded_categories = toUpperCaseLocalized(v));
              sanitise(this.includedUsers).map(v -> opts.included_users = v);
              sanitise(this.excludedUsers).map(v -> opts.excluded_users = v);
              opts.roll_cycle = this.rollCycle;
              opts.audit_logs_dir = this.auditLogDir;
              opts.max_queue_weight = this.maxQueueWeight;
              opts.max_archive_retries = this.maxArchiveRetries;
              opts.archive_command = this.archiveCommand;
              opts.block = this.block;
              opts.max_log_size = this.maxLogSize;
  
              AuditLogOptions.validate(opts);
  
              return opts;
          }
  
          private static Optional<String> sanitise(final String input)
          {
              if (input == null || input.trim().isEmpty())
                  return Optional.empty();
  
              return Optional.of(Arrays.stream(input.split(","))
                                       .map(String::trim)
                                       .map(Strings::emptyToNull)
                                       .filter(Objects::nonNull)
                                       .collect(Collectors.joining(",")));
          }
      }
  
      private static void validateCategories(final String categories)
      {
          assert categories != null;
  
          if (categories.isEmpty())
              return;
  
          for (final String includedCategory : categories.split(","))
          {
              try
              {
                  AuditLogEntryCategory.valueOf(includedCategory);
              }
              catch (final IllegalArgumentException ex)
              {
                  throw new ConfigurationException(String.format("category %s not found in %s",
                                                                 includedCategory,
                                                                 AuditLogEntryCategory.class.getName()),
                                                   ex);
              }
          }
      }
  
      public String toString()
      {
          return "AuditLogOptions{" +
                 "enabled=" + enabled +
                 ", logger='" + logger + '\'' +
                 ", included_keyspaces='" + included_keyspaces + '\'' +
                 ", excluded_keyspaces='" + excluded_keyspaces + '\'' +
                 ", included_categories='" + included_categories + '\'' +
                 ", excluded_categories='" + excluded_categories + '\'' +
                 ", included_users='" + included_users + '\'' +
                 ", excluded_users='" + excluded_users + '\'' +
                 ", audit_logs_dir='" + audit_logs_dir + '\'' +
                 ", archive_command='" + archive_command + '\'' +
                 ", roll_cycle='" + roll_cycle + '\'' +
                 ", block=" + block +
                 ", max_queue_weight=" + max_queue_weight +
                 ", max_log_size=" + max_log_size +
                 ", max_archive_retries=" + max_archive_retries +
                 '}';
      }
  }
  