← Back to team overview

maria-developers team mailing list archive

Re: MDEV-4472 (audit plugin)

 

Hi, Alexey!

On Jun 21, Alexey Botchkov wrote:
> Hi, Sergei.
> 
> Here is the plugin code for you to check out.
> It constists of a single new file, so i send you the link to it.
> http://myoffice.izhnet.ru/~alex/server_audit/server_audit.c

See my review attached. Comments are marked by // at the start of the
line.

> What's missing in it as i'm not sure how it can be done:
>      - No server host/ip in the log. How get that information?

What server host/ip?

>      - SYSLOG logging.
>            The SysLog i knew was declared in the syslog.h. That's the 
> interface talking to the
>            local syslogd instance.
>            If it's what we need to use, then i have no idea how to 
> specify the Syslog server IP for that
>            daemon (which is required by the spec). I see nothing in the 
> API about it and i thought
>            it should be configured with the daemon's cfg file or 
> something like that.

Right. So don't implement that, just use openlog/syslog/closelog.

Regards,
Sergei
/* Copyright (C) 2012 Monty Program Ab.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; version 2 of the License.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA */

#include <stdio.h>
#include <time.h>
#include <string.h>

#include <my_global.h>
// try to avoid including my_global.h
// if possible
#include <typelib.h>
#include <mysql/plugin.h>
#include <mysql/plugin_audit.h>
#include "../sql_errlog/sql_logger.cc"
// make logger a real service, remove it from both audit plugins
// and in here make, like
// #ifdef COMPILE_FOR_MYSQL
// #include "../../mysys/logger.c"
// #define MYSQL_SERVICE_LOGGER_INCLUDED
// #endif

/*
 Disable __attribute__() on non-gcc compilers.
*/
#if !defined(__attribute__) && !defined(__GNUC__)
#define __attribute__(A)
#endif
// let's move it to plugin.h, in a separate changeset

#ifdef _WIN32
#define localtime_r(a, b) localtime_s(b, a)
#endif /*WIN32*/

/*
  rate 0 means the logging was disabled.
*/


static char *incl_dml_users, *excl_dml_users,
            *incl_ddl_users, *excl_ddl_users,
            *syslog_server, *file_path;
static ulong output_type= 0;
static unsigned long long file_rotate_size;
static unsigned int rotations;
static char rotate;
static char logging;
static char ddl_user_buffer[1024];
static char dml_user_buffer[1024];

static char servhost[16];
static size_t servhost_len;

static unsigned int count;

static void update_incl_dml_users(MYSQL_THD thd, struct st_mysql_sys_var *var,
                                  void *var_ptr, const void *save);
static void update_excl_dml_users(MYSQL_THD thd, struct st_mysql_sys_var *var,
                                  void *var_ptr, const void *save);
static void update_incl_ddl_users(MYSQL_THD thd, struct st_mysql_sys_var *var,
                                  void *var_ptr, const void *save);
static void update_excl_ddl_users(MYSQL_THD thd, struct st_mysql_sys_var *var,
                                  void *var_ptr, const void *save);
static void update_output_type(MYSQL_THD thd, struct st_mysql_sys_var *var,
                               void *var_ptr, const void *save);
static int check_syslog_server(MYSQL_THD thd, struct st_mysql_sys_var *var,
                               void *save, struct st_mysql_value *value);
static void update_syslog_server(MYSQL_THD thd, struct st_mysql_sys_var *var,
                                 void *var_ptr, const void *save);
#ifdef TO_ENABLE_LATER
static int check_file_path(MYSQL_THD thd, struct st_mysql_sys_var *var,
                           void *save, struct st_mysql_value *value);
static void update_file_path(MYSQL_THD thd, struct st_mysql_sys_var *var,
                             void *var_ptr, const void *save);
static void switch_logging(MYSQL_THD thd, struct st_mysql_sys_var *var,
                           void *var_ptr, const void *save);
#endif /*TO_ENABLE_LATER*/
// I wouldn't bother, and would make this setting (logging destination)
// read-only
static void rotate_log(MYSQL_THD thd, struct st_mysql_sys_var *var,
                       void *var_ptr, const void *save);

static MYSQL_SYSVAR_STR(incl_dml_users, incl_dml_users, PLUGIN_VAR_RQCMDARG,
       "Comma separated list of users to monitor.",
       NULL, update_incl_dml_users, NULL);
static MYSQL_SYSVAR_STR(excl_dml_users, excl_dml_users, PLUGIN_VAR_RQCMDARG,
       "Comma separated list of users to exclude from auditing.",
       NULL, update_excl_dml_users, NULL);
static MYSQL_SYSVAR_STR(incl_ddl_users, incl_ddl_users, PLUGIN_VAR_RQCMDARG,
       "Comma separated list of users to monitor.",
       NULL, update_incl_ddl_users, "");
static MYSQL_SYSVAR_STR(excl_ddl_users, excl_ddl_users, PLUGIN_VAR_RQCMDARG,
       "Comma separated list of users to exclude from auditing.",
       NULL, update_excl_ddl_users, "");
#define OUTPUT_NULL 0
#define OUTPUT_SYSLOG 1
#define OUTPUT_FILE 2
const char *output_type_names[]= { "null", "syslog", "file", 0 };
TYPELIB output_typelib=
{
    array_elements(output_type_names) - 1, "output_typelib",
    output_type_names, NULL
};
static MYSQL_SYSVAR_ENUM(output_type, output_type, PLUGIN_VAR_RQCMDARG,
       "Desired output type. Possible values - 'syslog', 'file'"
       " or 'null' as no output.", 0, update_output_type, 0,
       &output_typelib);
// why did you do a 'null' output type?
static MYSQL_SYSVAR_STR(syslog_server, syslog_server,
       PLUGIN_VAR_RQCMDARG,
       "IP address of the syslog server.",
       check_syslog_server, update_syslog_server, "null");
static MYSQL_SYSVAR_STR(file_path, file_path,
       PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_MEMALLOC,
       "Path to the log file.", NULL, NULL, "null");
static MYSQL_SYSVAR_ULONGLONG(file_rotate_size, file_rotate_size,
       PLUGIN_VAR_RQCMDARG, "Maximum size of the log to start the rotation.",
       NULL, NULL,
       1000000, 100, ((long long) 0x7FFFFFFFFFFFFFFFLL), 1);
static MYSQL_SYSVAR_UINT(rotations, rotations,
       PLUGIN_VAR_READONLY, "Number of rotations before log is removed.",
       NULL, NULL, 9, 1, 999, 1);
static MYSQL_SYSVAR_BOOL(rotate, rotate,
       PLUGIN_VAR_OPCMDARG, "Force log rotation", NULL, rotate_log,
       0);
static MYSQL_SYSVAR_BOOL(logging, logging,
       PLUGIN_VAR_OPCMDARG, "Turn on/off the logging.", NULL, NULL, 0);

static struct st_mysql_sys_var* vars[] = {
    MYSQL_SYSVAR(incl_dml_users),
    MYSQL_SYSVAR(excl_dml_users),
    MYSQL_SYSVAR(incl_ddl_users),
    MYSQL_SYSVAR(excl_ddl_users),
    MYSQL_SYSVAR(output_type),
    MYSQL_SYSVAR(syslog_server),
    MYSQL_SYSVAR(file_path),
    MYSQL_SYSVAR(file_rotate_size),
    MYSQL_SYSVAR(rotations),
    MYSQL_SYSVAR(rotate),
    MYSQL_SYSVAR(logging),
    NULL
};


/* Here we set the limitation of 255 on the number of users  */
/* we can watch simultaneously. The size is convenient as it */
/* fits one byte. And seems to be enough for the user.       */
// please follow our coding (in particular, commenting) style
#define USERLIST_SIZE_LIMIT 0xFF
typedef struct server_audit_user_rec
{
  int hash_same;
  const char *user_name;
  int username_len;
} SERVER_AUDIT_USER_REC;


typedef struct server_audit_user_hash
{
  unsigned char table[0x10000];
  struct server_audit_user_rec userlist[USERLIST_SIZE_LIMIT];
  int n_users;
} SERVER_AUDIT_USER_HASH;
// why? why did you invent your own hash, while we have a perfectly
// usable hash in the server already?


static unsigned int user_hash_key(const char *s, int len)
{
  unsigned int key= 0;
  const char *end= s+len-1;
  while (s<end)
  {
    key^= (s[0]<<8) + s[1];
    s+= 2;
  }
  if (s == end)
    key^= s[0]<<8;
  return key;
}


static void init_user_hash(struct server_audit_user_hash *h)
{
  bzero(h, sizeof(struct server_audit_user_hash));
}


static int user_hash_add(struct server_audit_user_hash *h,
                   const char *uname, int uname_len)
{
  struct server_audit_user_rec *cur_rec= h->userlist + (h->n_users++);
  unsigned int hkey= user_hash_key(uname, uname_len);

  if (h->n_users > USERLIST_SIZE_LIMIT)
    return 1;
  cur_rec->hash_same= h->table[hkey];
  h->table[hkey]= h->n_users;
  cur_rec->user_name= uname;
  cur_rec->username_len= uname_len;
  return 0;
}


static int user_hash_lookup(struct server_audit_user_hash *h,
                            const char *uname, int uname_len)
{
  unsigned int hkey= user_hash_key(uname, uname_len);
  int cur_n_user;
  struct server_audit_user_rec *cur_rec;

  if (!h->table[hkey])
    return 0;
  cur_n_user= h->table[hkey];
  do
  {
    cur_rec= h->userlist + (cur_n_user-1);
    if (uname_len == cur_rec->username_len &&
        strncmp(uname, cur_rec->user_name, uname_len) == 0)
      return 1;
  } while ((cur_n_user= cur_rec->hash_same));

  return 0;
}


static int user_hash_fill(struct server_audit_user_hash *h,
                          const char *users)
{
  const char *cur_uname;

  init_user_hash(h);
  while (*users)
  {
    while (*users == ' ')
      users++;
    if (!*users)
      return 0;

    cur_uname= users;
    while (*users && *users != ' ' && *users != ',')
      users++;
    if (user_hash_add(h, cur_uname, users - cur_uname))
      return 1;
    while (*users && *users != ',')
      users++;
    if (!*users)
      break;
    users++;
  }
  return 0;
}


static void error_header()
{
  struct tm tm_time;
  time_t curtime;

  (void) time(&curtime);
  (void) localtime_r(&curtime, &tm_time);

  (void) fprintf(stderr,"%02d%02d%02d %2d:%02d:%02d server_audit: ",
    tm_time.tm_year % 100, tm_time.tm_mon + 1,
    tm_time.tm_mday, tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec);
}


static LOGGER_HANDLE *logfile;
static struct server_audit_user_hash ddl_user_hash;
static struct server_audit_user_hash dml_user_hash;

static int write_log(const char *message, size_t len)
{
  if (output_type == OUTPUT_FILE)
    return logger_write(logfile, message, len);
  return 0;
}
// better extend the logger service to support syslog


static size_t log_header(char *message, size_t message_len,
                      time_t *ts,
                      const char *serverhost, unsigned int serverhost_len,
                      const char *username, unsigned int username_len,
                      const char *host, unsigned int hostlen,
                      unsigned int connection_id, const char *operation)
{
  struct tm tm_time;
  (void) localtime_r(ts, &tm_time);

  return my_snprintf(message, message_len,
      "%04d%02d%02d %02d:%02d:%02d,%.*s,%.*s,%.*s,%d,%s",
      tm_time.tm_year+1900, tm_time.tm_mon+1, tm_time.tm_mday,
      tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec,
      serverhost_len, serverhost,
      username_len, username,
      hostlen, host,
      connection_id, operation);
}


static int log_connection(const struct mysql_event_connection *event,
                          const char *type)
{
  time_t ctime;
  size_t csize;
  char message[1024];

  (void) time(&ctime);
  csize= log_header(message, sizeof(message)-1, &ctime,
                    servhost, servhost_len,
                    event->user, event->user_length,
                    event->host, event->host_length,
                    event->thread_id, type);
  message[csize]= '\n';
  return write_log(message, csize + 1);
}


static int log_statement(const struct mysql_event_general *event,
                         const char *type)
{
  size_t csize;
  char message[1024];
  time_t ev_time= (time_t) event->general_time;

  csize= log_header(message, sizeof(message)-1, &ev_time,
                    servhost, servhost_len,
                    event->general_user, event->general_user_length,
                    "", 0, event->general_thread_id, type);
  csize+= my_snprintf(message+csize, sizeof(message) - 1 - csize,
           ",%.*s", event->general_query_length, event->general_query);
  message[csize]= '\n';
  return write_log(message, csize + 1);
}


static int log_table(const struct mysql_event_table *event, const char *type)
{
  size_t csize;
  char message[1024];
  time_t ctime;

  (void) time(&ctime);
  csize= log_header(message, sizeof(message)-1, &ctime,
                    servhost, servhost_len,
                    event->user, strlen(event->user),
                    event->host, strlen(event->host),
                    event->thread_id, type);
  csize+= my_snprintf(message+csize, sizeof(message) - 1 - csize,
            ",%.*s,%.*s",event->database_length, event->database,
                         event->table_length, event->table);
// note that for RENAME you need to log both old and new table names
  message[csize]= '\n';
  return write_log(message, csize + 1);
}


static int ddl_log_user(const char *name, size_t len)
{
  if (incl_ddl_users)
    return user_hash_lookup(&ddl_user_hash, name, len);

  if (excl_ddl_users)
    return user_hash_lookup(&ddl_user_hash, name, len) == 0;

  return 1;
}


static int dml_log_user(const char *name, size_t len)
{
  if (incl_dml_users)
    return user_hash_lookup(&dml_user_hash, name, len);

  if (excl_dml_users)
    return user_hash_lookup(&dml_user_hash, name, len) == 0;

  return 1;
}


static void auditing(MYSQL_THD thd __attribute__((unused)),
                     unsigned int event_class,
                     const void *ev)
{
  if (!logging)
    return;

  if (event_class == MYSQL_AUDIT_GENERAL_CLASS)
  {
    const struct mysql_event_general *event =
      (const struct mysql_event_general *) ev;
    switch (event->event_subclass)
    {
      case MYSQL_AUDIT_GENERAL_LOG:
        log_statement(event, "QUERY");
        break;
      case MYSQL_AUDIT_GENERAL_ERROR:
        break;
      case MYSQL_AUDIT_GENERAL_RESULT:
        break;
      case MYSQL_AUDIT_GENERAL_STATUS:
        break;
      default:
        count++;
    }
  }
  else if (event_class == MYSQL_AUDIT_TABLE_CLASS)
  {
    const struct mysql_event_table *event =
      (const struct mysql_event_table *) ev;
    switch (event->event_subclass)
    {
      case MYSQL_AUDIT_TABLE_LOCK:
        if (dml_log_user(event->user, strlen(event->user)))
          log_table(event, "LOCK");
        break;
      case MYSQL_AUDIT_TABLE_CREATE:
        if (ddl_log_user(event->user, strlen(event->user)))
          log_table(event, "CREATE");
        break;
      case MYSQL_AUDIT_TABLE_DROP:
        if (ddl_log_user(event->user, strlen(event->user)))
          log_table(event, "DROP");
        break;
      case MYSQL_AUDIT_TABLE_RENAME:
        if (ddl_log_user(event->user, strlen(event->user)))
          log_table(event, "RENAME");
        break;
      case MYSQL_AUDIT_TABLE_ALTER:
        if (ddl_log_user(event->user, strlen(event->user)))
          log_table(event, "ALTER");
        break;
      default:
        count++;
    }
  }
  else if (event_class == MYSQL_AUDIT_CONNECTION_CLASS)
  {
    const struct mysql_event_connection *event =
      (const struct mysql_event_connection *) ev;
    switch (event->event_subclass)
    {
      case MYSQL_AUDIT_CONNECTION_CONNECT:
        log_connection(event, "CONNECT");
        break;
      case MYSQL_AUDIT_CONNECTION_DISCONNECT:
        log_connection(event, "DISCONNECT");
        break;
      case MYSQL_AUDIT_CONNECTION_CHANGE_USER:
        log_connection(event, "CHANGEUSER");
        break;
      default:
        count++;
    }
  }
}


static int server_audit_init(void *p __attribute__((unused)))
{
#ifdef DUMMY
  struct hostent* localHost;
  char* localIP;
  localHost= gethostbyname("localhost");
  localIP= inet_ntoa (*(struct in_addr *)*localHost->h_addr_list);
#endif /*DUMMY*/
// what?

  incl_dml_users= excl_dml_users= NULL;
  incl_ddl_users= excl_ddl_users= NULL;
  syslog_server= file_path= NULL;

  init_logger_mutexes();
  strcpy(servhost, "127.0.0.1");
  servhost_len= strlen(servhost);
  error_header();
  fprintf(stderr, "STARTED\n");
// really? I thought you need to log it in the log
// preferrably with the plugin version, and may be the server version too
  count= 0;
  return 0;
}


static int server_audit_deinit(void *p __attribute__((unused)))
{
  if (output_type == OUTPUT_FILE)
    logger_close(logfile);
    /* syslog implementation */
    /* else if (output_type == OUTPUT_SYSLOG) */
  error_header();
  fprintf(stderr, "STOPPED\n");
  return 0;
}


static void rotate_log(MYSQL_THD thd  __attribute__((unused)),
                       struct st_mysql_sys_var *var  __attribute__((unused)),
                       void *var_ptr  __attribute__((unused)),
                       const void *save  __attribute__((unused)))
{
  if (output_type == OUTPUT_FILE)
    (void) logger_rotate(logfile);
}


static struct st_mysql_audit mysql_descriptor =
{
  MYSQL_AUDIT_INTERFACE_VERSION,
  NULL,
  auditing,
  { MYSQL_AUDIT_GENERAL_CLASSMASK | MYSQL_AUDIT_CONNECTION_CLASSMASK }
};

mysql_declare_plugin(server_auditing)
{
  MYSQL_AUDIT_PLUGIN,
  &mysql_descriptor,
  "SERVER_AUDIT",
  "Alexey Botchkov",
  "Audit the server activity.",
  PLUGIN_LICENSE_GPL,
  server_audit_init,
  server_audit_deinit,
  0x0100,
  NULL,
  vars,
  NULL,
  0
}
mysql_declare_plugin_end;

static struct st_mysql_audit maria_descriptor =
{
  MYSQL_AUDIT_INTERFACE_VERSION,
  NULL,
  auditing,
  { MYSQL_AUDIT_TABLE_CLASSMASK | MYSQL_AUDIT_CONNECTION_CLASSMASK }
// no MYSQL_AUDIT_GENERAL_CLASSMASK here?  
};
maria_declare_plugin(server_auditing)
{
  MYSQL_AUDIT_PLUGIN,
  &maria_descriptor,
  "SERVER_AUDIT",
  "Alexey Botchkov",
  "Audit the server activity.",
  PLUGIN_LICENSE_GPL,
  server_audit_init,
  server_audit_deinit,
  0x0100,
  NULL,
  vars,
  "1.0",
  MariaDB_PLUGIN_MATURITY_ALPHA
}
maria_declare_plugin_end;


static void update_incl_dml_users(MYSQL_THD thd  __attribute__((unused)),
              struct st_mysql_sys_var *var  __attribute__((unused)),
              void *var_ptr  __attribute__((unused)), const void *save)
{
  strncpy(dml_user_buffer, *(const char **) save, sizeof(dml_user_buffer));
// these callbacks aren't called when an initial value of the option
// is set on the command line. please, make sure you have this use case covered
  incl_dml_users= dml_user_buffer;
  excl_dml_users= NULL;
  user_hash_fill(&dml_user_hash, incl_dml_users);
}


static void update_excl_dml_users(MYSQL_THD thd  __attribute__((unused)),
              struct st_mysql_sys_var *var  __attribute__((unused)),
              void *var_ptr  __attribute__((unused)), const void *save)
{
  strncpy(dml_user_buffer, *(const char **) save, sizeof(dml_user_buffer));
  excl_dml_users= dml_user_buffer;
  incl_dml_users= NULL;
  user_hash_fill(&dml_user_hash, excl_dml_users);
}


static void update_incl_ddl_users(MYSQL_THD thd  __attribute__((unused)),
              struct st_mysql_sys_var *var  __attribute__((unused)),
              void *var_ptr  __attribute__((unused)), const void *save)
{
  strncpy(ddl_user_buffer, *(const char **) save, sizeof(ddl_user_buffer));
  incl_ddl_users= ddl_user_buffer;
  excl_ddl_users= NULL;
  user_hash_fill(&ddl_user_hash, incl_ddl_users);
}


static void update_excl_ddl_users(MYSQL_THD thd  __attribute__((unused)),
              struct st_mysql_sys_var *var  __attribute__((unused)),
              void *var_ptr  __attribute__((unused)), const void *save)
{
  strncpy(ddl_user_buffer, *(const char **) save, sizeof(ddl_user_buffer));
  excl_ddl_users= ddl_user_buffer;
  incl_ddl_users= NULL;
  user_hash_fill(&ddl_user_hash, excl_ddl_users);
}


static void update_output_type(MYSQL_THD thd  __attribute__((unused)),
              struct st_mysql_sys_var *var  __attribute__((unused)),
              void *var_ptr  __attribute__((unused)), const void *save)
{
  ulong new_output_type= *((ulong *) save);
  if (output_type == new_output_type)
    return;

  if (output_type == OUTPUT_FILE)
  {
    logger_close(logfile);
  }
  else if (output_type == OUTPUT_SYSLOG)
  {
    /* syslog implementation */
  }

  output_type= OUTPUT_NULL;

  if (new_output_type == OUTPUT_FILE)
  {
    logfile= logger_open(file_path, file_rotate_size, rotations);
    if (logfile == NULL)
    {
      error_header();
      fprintf(stderr, "Could not create file '%s'\n", file_path);
      return;
    }
  }
  else if (new_output_type == OUTPUT_SYSLOG)
  {
    /* syslog implementation */
    return;
  }

  output_type= new_output_type;
}


static int check_syslog_server(MYSQL_THD thd  __attribute__((unused)), struct st_mysql_sys_var *var  __attribute__((unused)),
                               void *save  __attribute__((unused)), struct st_mysql_value *value  __attribute__((unused)))
{
  /* syslog implementation */
  return output_type == OUTPUT_SYSLOG;
}


static void update_syslog_server(MYSQL_THD thd  __attribute__((unused)), struct st_mysql_sys_var *var  __attribute__((unused)),
                                 void *var_ptr  __attribute__((unused)), const void *save  __attribute__((unused)))
{
  /* syslog implementation */
}


#ifdef TO_ENABLE_LATER
static int check_file_path(MYSQL_THD thd  __attribute__((unused)), struct st_mysql_sys_var *var  __attribute__((unused)),
                           void *save  __attribute__((unused)), struct st_mysql_value *value  __attribute__((unused)))
{
  return output_type == OUTPUT_FILE;
}


static void update_file_path(MYSQL_THD thd  __attribute__((unused)), struct st_mysql_sys_var *var  __attribute__((unused)),
                             void *var_ptr  __attribute__((unused)), const void *save  __attribute__((unused)))
{
}


static void switch_logging(MYSQL_THD thd  __attribute__((unused)), struct st_mysql_sys_var *var  __attribute__((unused)),
                           void *var_ptr  __attribute__((unused)), const void *save  __attribute__((unused)))
{
}

#endif /*TO_ENABLE_LATER*/


Follow ups

References