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 }