1package file 2 3import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "strconv" 11 "strings" 12 "sync" 13 "sync/atomic" 14 15 "github.com/hashicorp/errwrap" 16 "github.com/hashicorp/vault/audit" 17 "github.com/hashicorp/vault/sdk/helper/salt" 18 "github.com/hashicorp/vault/sdk/logical" 19) 20 21func Factory(ctx context.Context, conf *audit.BackendConfig) (audit.Backend, error) { 22 if conf.SaltConfig == nil { 23 return nil, fmt.Errorf("nil salt config") 24 } 25 if conf.SaltView == nil { 26 return nil, fmt.Errorf("nil salt view") 27 } 28 29 path, ok := conf.Config["file_path"] 30 if !ok { 31 path, ok = conf.Config["path"] 32 if !ok { 33 return nil, fmt.Errorf("file_path is required") 34 } 35 } 36 37 // normalize path if configured for stdout 38 if strings.EqualFold(path, "stdout") { 39 path = "stdout" 40 } 41 if strings.EqualFold(path, "discard") { 42 path = "discard" 43 } 44 45 format, ok := conf.Config["format"] 46 if !ok { 47 format = "json" 48 } 49 switch format { 50 case "json", "jsonx": 51 default: 52 return nil, fmt.Errorf("unknown format type %q", format) 53 } 54 55 // Check if hashing of accessor is disabled 56 hmacAccessor := true 57 if hmacAccessorRaw, ok := conf.Config["hmac_accessor"]; ok { 58 value, err := strconv.ParseBool(hmacAccessorRaw) 59 if err != nil { 60 return nil, err 61 } 62 hmacAccessor = value 63 } 64 65 // Check if raw logging is enabled 66 logRaw := false 67 if raw, ok := conf.Config["log_raw"]; ok { 68 b, err := strconv.ParseBool(raw) 69 if err != nil { 70 return nil, err 71 } 72 logRaw = b 73 } 74 75 // Check if mode is provided 76 mode := os.FileMode(0600) 77 if modeRaw, ok := conf.Config["mode"]; ok { 78 m, err := strconv.ParseUint(modeRaw, 8, 32) 79 if err != nil { 80 return nil, err 81 } 82 if m != 0 { 83 mode = os.FileMode(m) 84 } 85 } 86 87 b := &Backend{ 88 path: path, 89 mode: mode, 90 saltConfig: conf.SaltConfig, 91 saltView: conf.SaltView, 92 salt: new(atomic.Value), 93 formatConfig: audit.FormatterConfig{ 94 Raw: logRaw, 95 HMACAccessor: hmacAccessor, 96 }, 97 } 98 99 // Ensure we are working with the right type by explicitly storing a nil of 100 // the right type 101 b.salt.Store((*salt.Salt)(nil)) 102 103 switch format { 104 case "json": 105 b.formatter.AuditFormatWriter = &audit.JSONFormatWriter{ 106 Prefix: conf.Config["prefix"], 107 SaltFunc: b.Salt, 108 } 109 case "jsonx": 110 b.formatter.AuditFormatWriter = &audit.JSONxFormatWriter{ 111 Prefix: conf.Config["prefix"], 112 SaltFunc: b.Salt, 113 } 114 } 115 116 switch path { 117 case "stdout", "discard": 118 // no need to test opening file if outputting to stdout or discarding 119 default: 120 // Ensure that the file can be successfully opened for writing; 121 // otherwise it will be too late to catch later without problems 122 // (ref: https://github.com/hashicorp/vault/issues/550) 123 if err := b.open(); err != nil { 124 return nil, errwrap.Wrapf(fmt.Sprintf("sanity check failed; unable to open %q for writing: {{err}}", path), err) 125 } 126 } 127 128 return b, nil 129} 130 131// Backend is the audit backend for the file-based audit store. 132// 133// NOTE: This audit backend is currently very simple: it appends to a file. 134// It doesn't do anything more at the moment to assist with rotation 135// or reset the write cursor, this should be done in the future. 136type Backend struct { 137 path string 138 139 formatter audit.AuditFormatter 140 formatConfig audit.FormatterConfig 141 142 fileLock sync.RWMutex 143 f *os.File 144 mode os.FileMode 145 146 saltMutex sync.RWMutex 147 salt *atomic.Value 148 saltConfig *salt.Config 149 saltView logical.Storage 150} 151 152var _ audit.Backend = (*Backend)(nil) 153 154func (b *Backend) Salt(ctx context.Context) (*salt.Salt, error) { 155 s := b.salt.Load().(*salt.Salt) 156 if s != nil { 157 return s, nil 158 } 159 160 b.saltMutex.Lock() 161 defer b.saltMutex.Unlock() 162 163 s = b.salt.Load().(*salt.Salt) 164 if s != nil { 165 return s, nil 166 } 167 168 newSalt, err := salt.NewSalt(ctx, b.saltView, b.saltConfig) 169 if err != nil { 170 b.salt.Store((*salt.Salt)(nil)) 171 return nil, err 172 } 173 174 b.salt.Store(newSalt) 175 return newSalt, nil 176} 177 178func (b *Backend) GetHash(ctx context.Context, data string) (string, error) { 179 salt, err := b.Salt(ctx) 180 if err != nil { 181 return "", err 182 } 183 184 return audit.HashString(salt, data), nil 185} 186 187func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error { 188 var writer io.Writer 189 switch b.path { 190 case "stdout": 191 writer = os.Stdout 192 case "discard": 193 return nil 194 } 195 196 buf := bytes.NewBuffer(make([]byte, 0, 2000)) 197 err := b.formatter.FormatRequest(ctx, buf, b.formatConfig, in) 198 if err != nil { 199 return err 200 } 201 202 return b.log(ctx, buf, writer) 203} 204 205func (b *Backend) log(ctx context.Context, buf *bytes.Buffer, writer io.Writer) error { 206 reader := bytes.NewReader(buf.Bytes()) 207 208 b.fileLock.Lock() 209 210 if writer == nil { 211 if err := b.open(); err != nil { 212 b.fileLock.Unlock() 213 return err 214 } 215 writer = b.f 216 } 217 218 if _, err := reader.WriteTo(writer); err == nil { 219 b.fileLock.Unlock() 220 return nil 221 } else if b.path == "stdout" { 222 b.fileLock.Unlock() 223 return err 224 } 225 226 // If writing to stdout there's no real reason to think anything would have 227 // changed so return above. Otherwise, opportunistically try to re-open the 228 // FD, once per call. 229 b.f.Close() 230 b.f = nil 231 232 if err := b.open(); err != nil { 233 b.fileLock.Unlock() 234 return err 235 } 236 237 reader.Seek(0, io.SeekStart) 238 _, err := reader.WriteTo(writer) 239 b.fileLock.Unlock() 240 return err 241} 242 243func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error { 244 var writer io.Writer 245 switch b.path { 246 case "stdout": 247 writer = os.Stdout 248 case "discard": 249 return nil 250 } 251 252 buf := bytes.NewBuffer(make([]byte, 0, 6000)) 253 err := b.formatter.FormatResponse(ctx, buf, b.formatConfig, in) 254 if err != nil { 255 return err 256 } 257 258 return b.log(ctx, buf, writer) 259} 260 261// The file lock must be held before calling this 262func (b *Backend) open() error { 263 if b.f != nil { 264 return nil 265 } 266 if err := os.MkdirAll(filepath.Dir(b.path), b.mode); err != nil { 267 return err 268 } 269 270 var err error 271 b.f, err = os.OpenFile(b.path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, b.mode) 272 if err != nil { 273 return err 274 } 275 276 // Change the file mode in case the log file already existed. We special 277 // case /dev/null since we can't chmod it and bypass if the mode is zero 278 switch b.path { 279 case "/dev/null": 280 default: 281 if b.mode != 0 { 282 err = os.Chmod(b.path, b.mode) 283 if err != nil { 284 return err 285 } 286 } 287 } 288 289 return nil 290} 291 292func (b *Backend) Reload(_ context.Context) error { 293 switch b.path { 294 case "stdout", "discard": 295 return nil 296 } 297 298 b.fileLock.Lock() 299 defer b.fileLock.Unlock() 300 301 if b.f == nil { 302 return b.open() 303 } 304 305 err := b.f.Close() 306 // Set to nil here so that even if we error out, on the next access open() 307 // will be tried 308 b.f = nil 309 if err != nil { 310 return err 311 } 312 313 return b.open() 314} 315 316func (b *Backend) Invalidate(_ context.Context) { 317 b.saltMutex.Lock() 318 defer b.saltMutex.Unlock() 319 b.salt.Store((*salt.Salt)(nil)) 320} 321