diff --git a/docs/middleware.md b/docs/middleware.md index f753a521..30b32d71 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -25,7 +25,7 @@ It is often the case that one must not start to process events until the audit l capabilities are set up. For this, the following pattern is suggested: ```golang -fd, err := ginaudit.OpenAuditLogFileUntilSuccess(auditLogPath) +fd, err := helpers.OpenAuditLogFileUntilSuccess(auditLogPath) if err != nil { panic(err) } @@ -36,7 +36,7 @@ defer fd.Close() mdw := ginaudit.NewJSONMiddleware("my-test-component", fd) ``` -The function `ginaudit.OpenAuditLogFileUntilSuccess` attempts to open the audit log +The function `helpers.OpenAuditLogFileUntilSuccess` attempts to open the audit log file, and will block until it's available. This file may be created beforehand or it may be created by another process e.g. a sidecar container. It opens the file with `O_APPEND` which enables atomic writes as long as the audit events are less than 4096 bytes. diff --git a/ginaudit/mdw.go b/ginaudit/mdw.go index 369124fb..bfa3f331 100644 --- a/ginaudit/mdw.go +++ b/ginaudit/mdw.go @@ -19,9 +19,7 @@ import ( "fmt" "io" "net/http" - "os" "sync" - "time" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus" @@ -35,34 +33,6 @@ type Middleware struct { eventTypeMap sync.Map } -const ( - ownerGroupAccess = 0o640 - retryInterval = 100 * time.Millisecond -) - -// OpenAuditLogFileUntilSuccess attempts to open a file for writing audit events until -// it succeeds. -// It assumes that audit events are less than 4096 bytes to ensure atomicity. -// it takes a writer for the audit log. -func OpenAuditLogFileUntilSuccess(path string) (*os.File, error) { - for { - // This is opened with the O_APPEND option to ensure - // atomicity of writes. This is important to ensure - // we can concurrently write to the file and not block - // the server's main loop. - fd, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, ownerGroupAccess) - if err != nil { - if os.IsNotExist(err) { - time.Sleep(retryInterval) - continue - } - // Not being able to write audit log events is a fatal error - return nil, err - } - return fd, nil - } -} - // NewMiddleware returns a new instance of audit Middleware. func NewMiddleware(component string, aew *auditevent.EventWriter) *Middleware { return &Middleware{ diff --git a/ginaudit/mdw_test.go b/ginaudit/mdw_test.go index 1e27e1d2..ab15ad44 100644 --- a/ginaudit/mdw_test.go +++ b/ginaudit/mdw_test.go @@ -29,7 +29,6 @@ import ( "net/http" "net/http/httptest" "os" - "path/filepath" "regexp" "strings" "sync" @@ -319,68 +318,6 @@ func TestParallelCallsToMiddleware(t *testing.T) { } } -func TestOpenAuditLogFileUntilSuccess(t *testing.T) { - t.Parallel() - - var wg sync.WaitGroup - wg.Add(1) - - tmpdir := t.TempDir() - tmpfile := filepath.Join(tmpdir, "audit.log") - - go func() { - defer wg.Done() - time.Sleep(time.Second) - fd, err := os.OpenFile(tmpfile, os.O_RDONLY|os.O_CREATE, 0o600) - require.NoError(t, err) - err = fd.Close() - require.NoError(t, err) - }() - - fd, err := ginaudit.OpenAuditLogFileUntilSuccess(tmpfile) - require.NoError(t, err) - require.NotNil(t, fd) - - err = fd.Close() - require.NoError(t, err) - - // We wait so we don't leak file descriptors - wg.Wait() - - err = os.Remove(tmpfile) - require.NoError(t, err) -} - -func TestOpenAuditLogFileError(t *testing.T) { - t.Parallel() - - var wg sync.WaitGroup - wg.Add(1) - - tmpdir := t.TempDir() - tmpfile := filepath.Join(tmpdir, "audit.log") - - go func() { - defer wg.Done() - time.Sleep(time.Second) - // This file is read only - fd, err := os.OpenFile(tmpfile, os.O_RDONLY|os.O_CREATE, 0o500) - require.NoError(t, err) - err = fd.Close() - require.NoError(t, err) - }() - - fd, err := ginaudit.OpenAuditLogFileUntilSuccess(tmpfile) - require.Error(t, err) - require.Nil(t, fd) - - // We wait so we don't leak file descriptors - wg.Wait() - - err = os.Remove(tmpfile) - require.NoError(t, err) -} - func TestCantRegisterMultipleTimesToSamePrometheus(t *testing.T) { t.Parallel() diff --git a/helpers/helpers.go b/helpers/helpers.go new file mode 100644 index 00000000..1064b81b --- /dev/null +++ b/helpers/helpers.go @@ -0,0 +1,49 @@ +/* +Copyright 2022 Equinix, Inc. + +Licensed 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 helpers + +import ( + "os" + "time" +) + +const ( + ownerGroupAccess = 0o640 + retryInterval = 100 * time.Millisecond +) + +// OpenAuditLogFileUntilSuccess attempts to open a file for writing audit events until +// it succeeds. +// It assumes that audit events are less than 4096 bytes to ensure atomicity. +// it takes a writer for the audit log. +func OpenAuditLogFileUntilSuccess(path string) (*os.File, error) { + for { + // This is opened with the O_APPEND option to ensure + // atomicity of writes. This is important to ensure + // we can concurrently write to the file and not block + // the server's main loop. + fd, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, ownerGroupAccess) + if err != nil { + if os.IsNotExist(err) { + time.Sleep(retryInterval) + continue + } + // Not being able to write audit log events is a fatal error + return nil, err + } + return fd, nil + } +} diff --git a/helpers/helpers_test.go b/helpers/helpers_test.go new file mode 100644 index 00000000..1e73f339 --- /dev/null +++ b/helpers/helpers_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2022 Equinix, Inc. + +Licensed 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 helpers_test + +import ( + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/metal-toolbox/auditevent/helpers" +) + +func TestOpenAuditLogFileUntilSuccess(t *testing.T) { + t.Parallel() + + var wg sync.WaitGroup + wg.Add(1) + + tmpdir := t.TempDir() + tmpfile := filepath.Join(tmpdir, "audit.log") + + go func() { + defer wg.Done() + time.Sleep(time.Second) + fd, err := os.OpenFile(tmpfile, os.O_RDONLY|os.O_CREATE, 0o600) + require.NoError(t, err) + err = fd.Close() + require.NoError(t, err) + }() + + fd, err := helpers.OpenAuditLogFileUntilSuccess(tmpfile) + require.NoError(t, err) + require.NotNil(t, fd) + + err = fd.Close() + require.NoError(t, err) + + // We wait so we don't leak file descriptors + wg.Wait() + + err = os.Remove(tmpfile) + require.NoError(t, err) +} + +func TestOpenAuditLogFileError(t *testing.T) { + t.Parallel() + + var wg sync.WaitGroup + wg.Add(1) + + tmpdir := t.TempDir() + tmpfile := filepath.Join(tmpdir, "audit.log") + + go func() { + defer wg.Done() + time.Sleep(time.Second) + // This file is read only + fd, err := os.OpenFile(tmpfile, os.O_RDONLY|os.O_CREATE, 0o500) + require.NoError(t, err) + err = fd.Close() + require.NoError(t, err) + }() + + fd, err := helpers.OpenAuditLogFileUntilSuccess(tmpfile) + require.Error(t, err) + require.Nil(t, fd) + + // We wait so we don't leak file descriptors + wg.Wait() + + err = os.Remove(tmpfile) + require.NoError(t, err) +} diff --git a/internal/testtools/testtools.go b/internal/testtools/testtools.go index 5437bc5c..934041f3 100644 --- a/internal/testtools/testtools.go +++ b/internal/testtools/testtools.go @@ -30,7 +30,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/metal-toolbox/auditevent/ginaudit" + "github.com/metal-toolbox/auditevent/helpers" ) const ( @@ -63,7 +63,7 @@ func SetPipeReader(t *testing.T, namedPipe string) <-chan io.WriteCloser { t.Helper() rchan := make(chan io.WriteCloser) go func(c chan<- io.WriteCloser) { - fd, err := ginaudit.OpenAuditLogFileUntilSuccess(namedPipe) + fd, err := helpers.OpenAuditLogFileUntilSuccess(namedPipe) require.NoError(t, err) c <- fd }(rchan)