github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/core/auditlog/auditlog.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  package auditlog
     4  
     5  import (
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"math/rand"
    11  	"os"
    12  	"path/filepath"
    13  	"time"
    14  
    15  	"github.com/juju/clock"
    16  	"github.com/juju/errors"
    17  	"github.com/juju/loggo"
    18  	"github.com/juju/utils"
    19  	"gopkg.in/natefinch/lumberjack.v2"
    20  )
    21  
    22  var logger = loggo.GetLogger("core.auditlog")
    23  
    24  // Conversation represents a high-level juju command from the juju
    25  // client (or other client). There'll be one Conversation per API
    26  // connection from the client, with zero or more associated
    27  // Request/ResponseErrors pairs.
    28  type Conversation struct {
    29  	Who            string `json:"who"`        // username@idm
    30  	What           string `json:"what"`       // "juju deploy ./foo/bar"
    31  	When           string `json:"when"`       // ISO 8601 to second precision
    32  	ModelName      string `json:"model-name"` // full representation "user/name"
    33  	ModelUUID      string `json:"model-uuid"`
    34  	ConversationID string `json:"conversation-id"` // uint64 in hex
    35  	ConnectionID   string `json:"connection-id"`   // uint64 in hex (using %X to match the value in log files)
    36  }
    37  
    38  // ConversationArgs is the information needed to create a method recorder.
    39  type ConversationArgs struct {
    40  	Who          string
    41  	What         string
    42  	ModelName    string
    43  	ModelUUID    string
    44  	ConnectionID uint64
    45  }
    46  
    47  // Request represents a call to an API facade made as part of
    48  // a specific conversation.
    49  type Request struct {
    50  	ConversationID string `json:"conversation-id"`
    51  	ConnectionID   string `json:"connection-id"`
    52  	RequestID      uint64 `json:"request-id"`
    53  	When           string `json:"when"`
    54  	Facade         string `json:"facade"`
    55  	Method         string `json:"method"`
    56  	Version        int    `json:"version"`
    57  	Args           string `json:"args,omitempty"`
    58  }
    59  
    60  // RequestArgs is the information about an API call that we want to
    61  // record.
    62  type RequestArgs struct {
    63  	Facade    string
    64  	Method    string
    65  	Version   int
    66  	Args      string
    67  	RequestID uint64
    68  }
    69  
    70  // ResponseErrors captures any errors coming back from the API in
    71  // response to a request.
    72  type ResponseErrors struct {
    73  	ConversationID string   `json:"conversation-id"`
    74  	ConnectionID   string   `json:"connection-id"`
    75  	RequestID      uint64   `json:"request-id"`
    76  	When           string   `json:"when"`
    77  	Errors         []*Error `json:"errors"`
    78  }
    79  
    80  // ResponseErrorsArgs has errors from an API response to record in the
    81  // audit log.
    82  type ResponseErrorsArgs struct {
    83  	RequestID uint64
    84  	Errors    []*Error
    85  }
    86  
    87  // Error holds the details of an error sent back from the API.
    88  type Error struct {
    89  	Message string `json:"message"`
    90  	Code    string `json:"code"`
    91  }
    92  
    93  // Record is the top-level entry type in an audit log, which serves as
    94  // a type discriminator. Only one of Conversation/Request/Errors should be set.
    95  type Record struct {
    96  	Conversation *Conversation   `json:"conversation,omitempty"`
    97  	Request      *Request        `json:"request,omitempty"`
    98  	Errors       *ResponseErrors `json:"errors,omitempty"`
    99  }
   100  
   101  // AuditLog represents something that can store calls, requests and
   102  // responses somewhere.
   103  type AuditLog interface {
   104  	AddConversation(c Conversation) error
   105  	AddRequest(r Request) error
   106  	AddResponse(r ResponseErrors) error
   107  	Close() error
   108  }
   109  
   110  // Recorder records method calls for a specific API connection.
   111  type Recorder struct {
   112  	log          AuditLog
   113  	clock        clock.Clock
   114  	connectionID string
   115  	callID       string
   116  }
   117  
   118  // NewRecorder creates a Recorder for the connection described (and
   119  // stores details of the initial call in the log).
   120  func NewRecorder(log AuditLog, clock clock.Clock, c ConversationArgs) (*Recorder, error) {
   121  	callID := newConversationID()
   122  	connectionID := idString(c.ConnectionID)
   123  	err := log.AddConversation(Conversation{
   124  		ConversationID: callID,
   125  		ConnectionID:   connectionID,
   126  		Who:            c.Who,
   127  		What:           c.What,
   128  		When:           clock.Now().Format(time.RFC3339),
   129  		ModelName:      c.ModelName,
   130  		ModelUUID:      c.ModelUUID,
   131  	})
   132  	if err != nil {
   133  		return nil, errors.Trace(err)
   134  	}
   135  	return &Recorder{
   136  		log:          log,
   137  		clock:        clock,
   138  		callID:       callID,
   139  		connectionID: connectionID,
   140  	}, nil
   141  }
   142  
   143  // AddRequest records a method call to the API.
   144  func (r *Recorder) AddRequest(m RequestArgs) error {
   145  	return errors.Trace(r.log.AddRequest(Request{
   146  		ConversationID: r.callID,
   147  		ConnectionID:   r.connectionID,
   148  		RequestID:      m.RequestID,
   149  		When:           r.clock.Now().Format(time.RFC3339),
   150  		Facade:         m.Facade,
   151  		Method:         m.Method,
   152  		Version:        m.Version,
   153  		Args:           m.Args,
   154  	}))
   155  }
   156  
   157  // AddResponse records the result of a method call to the API.
   158  func (r *Recorder) AddResponse(m ResponseErrorsArgs) error {
   159  	return errors.Trace(r.log.AddResponse(ResponseErrors{
   160  		ConversationID: r.callID,
   161  		ConnectionID:   r.connectionID,
   162  		RequestID:      m.RequestID,
   163  		When:           r.clock.Now().Format(time.RFC3339),
   164  		Errors:         m.Errors,
   165  	}))
   166  }
   167  
   168  // newConversationID generates a random 64bit integer as hex - this
   169  // will be used to link the requests and responses with the command
   170  // the user issued. We don't use the API server's connection ID here
   171  // because that starts from 0 and increments, so it resets when the
   172  // API server is restarted. The conversation ID needs to be unique
   173  // across restarts, otherwise we'd attribute requests to the wrong
   174  // conversation.
   175  func newConversationID() string {
   176  	buf := make([]byte, 8)
   177  	rand.Read(buf) // Can't fail
   178  	return hex.EncodeToString(buf)
   179  }
   180  
   181  type auditLogFile struct {
   182  	fileLogger io.WriteCloser
   183  }
   184  
   185  // NewLogFile returns an audit entry sink which writes to an audit.log
   186  // file in the specified directory. maxSize is the maximum size (in
   187  // megabytes) of the log file before it gets rotated. maxBackups is
   188  // the maximum number of old compressed log files to keep (or 0 to
   189  // keep all of them).
   190  func NewLogFile(logDir string, maxSize, maxBackups int) AuditLog {
   191  	logPath := filepath.Join(logDir, "audit.log")
   192  	if err := primeLogFile(logPath); err != nil {
   193  		// This isn't a fatal error so log and continue if priming
   194  		// fails.
   195  		logger.Errorf("Unable to prime %s (proceeding anyway): %v", logPath, err)
   196  	}
   197  
   198  	return &auditLogFile{
   199  		fileLogger: &lumberjack.Logger{
   200  			Filename:   logPath,
   201  			MaxSize:    maxSize,
   202  			MaxBackups: maxBackups,
   203  			Compress:   true,
   204  		},
   205  	}
   206  }
   207  
   208  // AddConversation implements AuditLog.
   209  func (a *auditLogFile) AddConversation(c Conversation) error {
   210  	return errors.Trace(a.addRecord(Record{Conversation: &c}))
   211  }
   212  
   213  // AddRequest implements AuditLog.
   214  func (a *auditLogFile) AddRequest(m Request) error {
   215  	return errors.Trace(a.addRecord(Record{Request: &m}))
   216  
   217  }
   218  
   219  // AddResponse implements AuditLog.
   220  func (a *auditLogFile) AddResponse(m ResponseErrors) error {
   221  	return errors.Trace(a.addRecord(Record{Errors: &m}))
   222  }
   223  
   224  // Close implements AuditLog.
   225  func (a *auditLogFile) Close() error {
   226  	return errors.Trace(a.fileLogger.Close())
   227  }
   228  
   229  func (a *auditLogFile) addRecord(r Record) error {
   230  	bytes, err := json.Marshal(r)
   231  	if err != nil {
   232  		return errors.Trace(err)
   233  	}
   234  	// Add a linebreak to bytes rather than doing two calls to write
   235  	// just in case lumberjack rolls the file between them.
   236  	bytes = append(bytes, byte('\n'))
   237  	_, err = a.fileLogger.Write(bytes)
   238  	return errors.Trace(err)
   239  }
   240  
   241  func idString(id uint64) string {
   242  	return fmt.Sprintf("%X", id)
   243  }
   244  
   245  // primeLogFile ensures the logsink log file is created with the
   246  // correct mode and ownership.
   247  func primeLogFile(path string) error {
   248  	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600)
   249  	if err != nil {
   250  		return errors.Trace(err)
   251  	}
   252  	if err := f.Close(); err != nil {
   253  		return errors.Trace(err)
   254  	}
   255  	err = utils.ChownPath(path, "syslog")
   256  	return errors.Trace(err)
   257  }