-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Access logging module that allows keeping a record of incoming operations. --------- Co-authored-by: ldebruijn <[email protected]>
- Loading branch information
Showing
6 changed files
with
243 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Access Logging | ||
|
||
In some cases you want to keep a record of what operations were performed against your landscape. The access logging protection can provide that for you. | ||
Access logging is done to STDOUT. | ||
|
||
<!-- TOC --> | ||
|
||
## Configuration | ||
|
||
You can configure `graphql-protect` to enable access logging for incoming operaitons. | ||
|
||
```yaml | ||
access_logging: | ||
# Enable the feature, | ||
enable: true | ||
included_headers: | ||
# Include any headers of interest here | ||
- Authorization | ||
# Include the operation name in the access log record | ||
include_operation_name: true | ||
# Include the variables in the access log record | ||
include_variables: true | ||
# Include the payload in the access log record | ||
include_payload: true | ||
``` | ||
## How does it work? | ||
For each operation we'll produce an access log record according to your provided configuration. | ||
If used in conjunction with persisted operations the access log will be produced after the operation is swapped for the payload, meaning you have full access to the operation name and payload. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package accesslogging | ||
|
||
import ( | ||
"github.com/ldebruijn/graphql-protect/internal/business/gql" | ||
"log/slog" | ||
"net/http" | ||
"slices" | ||
) | ||
|
||
type Config struct { | ||
Enable bool `config:"default:true" yaml:"enabled"` | ||
IncludedHeaders []string `yaml:"included_headers"` | ||
IncludeOperationName bool `config:"default:true" yaml:"include_operation_name"` | ||
IncludeVariables bool `config:"default:true" yaml:"include_variables"` | ||
IncludePayload bool `config:"default:false" yaml:"include_payload"` | ||
} | ||
|
||
type AccessLogging struct { | ||
log *slog.Logger | ||
cfg Config | ||
} | ||
|
||
func NewAccessLogging(cfg Config, log *slog.Logger) *AccessLogging { | ||
return &AccessLogging{ | ||
log: log.WithGroup("access-logging"), | ||
cfg: cfg, | ||
} | ||
} | ||
|
||
func (a *AccessLogging) Log(payloads []gql.RequestData, headers http.Header) { | ||
if !a.cfg.Enable { | ||
return | ||
} | ||
|
||
toLog := map[string]interface{}{} | ||
|
||
for _, req := range payloads { | ||
if a.cfg.IncludeOperationName { | ||
toLog["operationName"] = req.OperationName | ||
} | ||
if a.cfg.IncludeVariables { | ||
toLog["variables"] = req.Variables | ||
} | ||
if a.cfg.IncludePayload { | ||
toLog["payload"] = req.Query | ||
} | ||
|
||
logHeaders := map[string]interface{}{} | ||
for name, values := range headers { | ||
if slices.Contains(a.cfg.IncludedHeaders, name) { | ||
logHeaders[name] = values | ||
} | ||
} | ||
|
||
toLog["headers"] = logHeaders | ||
|
||
a.log.Info("access-logging", "payload", toLog) | ||
} | ||
} |
133 changes: 133 additions & 0 deletions
133
internal/business/rules/accesslogging/accesslogging_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package accesslogging | ||
|
||
import ( | ||
"context" | ||
"github.com/ldebruijn/graphql-protect/internal/business/gql" | ||
"github.com/stretchr/testify/assert" | ||
"log/slog" | ||
"net/http" | ||
"testing" | ||
) | ||
|
||
type testLogHandler struct { | ||
assert func(ctx context.Context, record slog.Record) error | ||
count int | ||
} | ||
|
||
func (t *testLogHandler) Enabled(context.Context, slog.Level) bool { | ||
return true | ||
} | ||
func (t *testLogHandler) Handle(ctx context.Context, record slog.Record) error { | ||
t.count++ | ||
return t.assert(ctx, record) | ||
} | ||
func (t *testLogHandler) WithAttrs(_ []slog.Attr) slog.Handler { | ||
return t | ||
} | ||
func (t *testLogHandler) WithGroup(_ string) slog.Handler { | ||
return t | ||
} | ||
|
||
func TestAccessLogging_Log(t *testing.T) { | ||
type args struct { | ||
cfg Config | ||
payloads []gql.RequestData | ||
headers http.Header | ||
count int | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
want func(ctx context.Context, record slog.Record) error | ||
}{ | ||
{ | ||
name: "logs expected fields when enabled", | ||
args: args{ | ||
cfg: Config{ | ||
Enable: true, | ||
IncludedHeaders: []string{"Authorization"}, | ||
IncludeOperationName: true, | ||
IncludeVariables: true, | ||
IncludePayload: true, | ||
}, | ||
payloads: []gql.RequestData{ | ||
{ | ||
OperationName: "Foobar", | ||
Variables: map[string]interface{}{ | ||
"foo": "bar", | ||
}, | ||
Query: "query Foo { id name }", | ||
}, | ||
}, | ||
headers: map[string][]string{ | ||
"Authorization": {"bearer hello"}, | ||
"Content-Type": {"application/json"}, | ||
}, | ||
count: 1, | ||
}, | ||
want: func(_ context.Context, record slog.Record) error { | ||
assert.Equal(t, 1, record.NumAttrs()) | ||
record.Attrs(func(a slog.Attr) bool { | ||
assert.Equal(t, "payload", a.Key) | ||
val := a.Value.Any().(map[string]interface{}) | ||
assert.Equal(t, "Foobar", val["operationName"]) | ||
assert.Equal(t, "query Foo { id name }", val["payload"]) | ||
assert.Equal(t, map[string]interface{}{ | ||
"foo": "bar", | ||
}, val["variables"]) | ||
assert.Equal(t, map[string]interface{}{ | ||
"Authorization": []string{"bearer hello"}, | ||
}, val["headers"]) | ||
|
||
return true | ||
}) | ||
|
||
return nil | ||
}, | ||
}, | ||
{ | ||
name: "logs nothing when disabled", | ||
args: args{ | ||
cfg: Config{ | ||
Enable: false, | ||
IncludedHeaders: []string{"Authorization"}, | ||
IncludeOperationName: true, | ||
IncludeVariables: true, | ||
IncludePayload: true, | ||
}, | ||
payloads: []gql.RequestData{ | ||
{ | ||
OperationName: "Foobar", | ||
Variables: map[string]interface{}{ | ||
"foo": "bar", | ||
}, | ||
Query: "query Foo { id name }", | ||
}, | ||
}, | ||
headers: map[string][]string{ | ||
"Authorization": {"bearer hello"}, | ||
"Content-Type": {"application/json"}, | ||
}, | ||
count: 0, | ||
}, | ||
want: func(_ context.Context, _ slog.Record) error { | ||
assert.Fail(t, "should never reach here") | ||
return nil | ||
}, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
handler := &testLogHandler{assert: tt.want} | ||
log := slog.New(handler) | ||
|
||
a := &AccessLogging{ | ||
log: log, | ||
cfg: tt.args.cfg, | ||
} | ||
a.Log(tt.args.payloads, tt.args.headers) | ||
|
||
assert.Equal(t, tt.args.count, a.log.Handler().(*testLogHandler).count) | ||
}) | ||
} | ||
} |