-
Notifications
You must be signed in to change notification settings - Fork 350
/
log.go
249 lines (209 loc) · 6.04 KB
/
log.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/*
Package log provides a request logging filter, usable also for
audit logging. Audit logging is showing who did a request in case of
OAuth2 provider returns a "uid" key and value.
*/
package log
import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"os"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
"github.com/zalando/skipper/filters"
)
const (
// AuditLogName is the filter name seen by the user
AuditLogName = "auditLog"
// AuthUserKey is used by the auth package to set the user
// information into the state bag to pass the information to
// the auditLog filter.
AuthUserKey = "auth-user"
// AuthRejectReasonKey is used by the auth package to set the
// reject reason information into the state bag to pass the
// information to the auditLog filter.
AuthRejectReasonKey = "auth-reject-reason"
// UnverifiedAuditLogName is the filtername seen by the user
UnverifiedAuditLogName = "unverifiedAuditLog"
// UnverifiedAuditHeader is the name of the header added to the request which contains the unverified audit details
UnverifiedAuditHeader = "X-Unverified-Audit"
authHeaderName = "Authorization"
authHeaderPrefix = "Bearer "
defaultSub = "<invalid-sub>"
defaultUnverifiedAuditLogKey = "sub"
)
var (
re = regexp.MustCompile("^[a-zA-z0-9_/:?=&%@.#-]*$")
)
type auditLog struct {
writer io.Writer
maxBodyLog int
}
type teeBody struct {
body io.ReadCloser
buffer *bytes.Buffer
teeReader io.Reader
maxTee int
}
type auditDoc struct {
Method string `json:"method"`
Path string `json:"path"`
Status int `json:"status"`
AuthStatus *authStatusDoc `json:"authStatus,omitempty"`
RequestBody string `json:"requestBody,omitempty"`
}
type authStatusDoc struct {
User string `json:"user,omitempty"`
Rejected bool `json:"rejected"`
Reason string `json:"reason,omitempty"`
}
func newTeeBody(rc io.ReadCloser, maxTee int) io.ReadCloser {
b := bytes.NewBuffer(nil)
tb := &teeBody{
body: rc,
buffer: b,
maxTee: maxTee}
tb.teeReader = io.TeeReader(rc, tb)
return tb
}
func (tb *teeBody) Read(b []byte) (int, error) { return tb.teeReader.Read(b) }
func (tb *teeBody) Close() error { return tb.body.Close() }
func (tb *teeBody) Write(b []byte) (int, error) {
if tb.maxTee < 0 {
return tb.buffer.Write(b)
}
wl := len(b)
if wl >= tb.maxTee {
wl = tb.maxTee
}
n, err := tb.buffer.Write(b[:wl])
if err != nil {
return n, err
}
tb.maxTee -= n
// lie to avoid short write
return len(b), nil
}
// NewAuditLog creates an auditLog filter specification. It expects a
// maxAuditBody attribute to limit the size of the log. It will use
// os.Stderr as writer for the output of the log entries.
//
// spec := NewAuditLog(1024)
func NewAuditLog(maxAuditBody int) filters.Spec {
return &auditLog{
writer: os.Stderr,
maxBodyLog: maxAuditBody,
}
}
func (al *auditLog) Name() string { return AuditLogName }
// CreateFilter has no arguments. It creates the filter if the user
// specifies auditLog() in their route.
func (al *auditLog) CreateFilter(args []interface{}) (filters.Filter, error) {
if len(args) != 0 {
return nil, filters.ErrInvalidFilterParameters
}
return &auditLog{writer: al.writer, maxBodyLog: al.maxBodyLog}, nil
}
func (al *auditLog) Request(ctx filters.FilterContext) {
if al.maxBodyLog != 0 {
ctx.Request().Body = newTeeBody(ctx.Request().Body, al.maxBodyLog)
}
}
func (al *auditLog) Response(ctx filters.FilterContext) {
req := ctx.Request()
rsp := ctx.Response()
doc := auditDoc{
Method: req.Method,
Path: req.URL.Path,
Status: rsp.StatusCode}
sb := ctx.StateBag()
au, _ := sb[AuthUserKey].(string)
rr, _ := sb[AuthRejectReasonKey].(string)
if au != "" || rr != "" {
doc.AuthStatus = &authStatusDoc{User: au}
if rr != "" {
doc.AuthStatus.Rejected = true
doc.AuthStatus.Reason = rr
}
}
if tb, ok := req.Body.(*teeBody); ok {
if tb.maxTee < 0 {
io.Copy(tb.buffer, tb.body)
} else {
io.CopyN(tb.buffer, tb.body, int64(tb.maxTee))
}
if tb.buffer.Len() > 0 {
doc.RequestBody = tb.buffer.String()
}
}
enc := json.NewEncoder(al.writer)
err := enc.Encode(&doc)
if err != nil {
log.Errorf("Failed to json encode auditDoc: %v", err)
}
}
type (
unverifiedAuditLogSpec struct{}
unverifiedAuditLogFilter struct {
TokenKeys []string
}
)
// NewUnverifiedAuditLog logs "Sub" of the middle part of a JWT Token. Or else, logs the requested JSON key if present
func NewUnverifiedAuditLog() filters.Spec { return &unverifiedAuditLogSpec{} }
func (ual *unverifiedAuditLogSpec) Name() string { return UnverifiedAuditLogName }
// CreateFilter has no arguments. It creates the filter if the user
// specifies unverifiedAuditLog() in their route.
func (ual *unverifiedAuditLogSpec) CreateFilter(args []interface{}) (filters.Filter, error) {
var len = len(args)
if len == 0 {
return &unverifiedAuditLogFilter{TokenKeys: []string{defaultUnverifiedAuditLogKey}}, nil
}
keys := make([]string, len)
for i := 0; i < len; i++ {
keyName, ok := args[i].(string)
if !ok {
return nil, filters.ErrInvalidFilterParameters
}
keys[i] = keyName
}
return &unverifiedAuditLogFilter{TokenKeys: keys}, nil
}
func (ual *unverifiedAuditLogFilter) Request(ctx filters.FilterContext) {
req := ctx.Request()
ahead := req.Header.Get(authHeaderName)
if !strings.HasPrefix(ahead, authHeaderPrefix) {
return
}
fields := strings.FieldsFunc(ahead, func(r rune) bool {
return r == []rune(".")[0]
})
if len(fields) == 3 {
sDec, err := base64.RawURLEncoding.DecodeString(fields[1])
if err != nil {
return
}
var j map[string]interface{}
err = json.Unmarshal(sDec, &j)
if err != nil {
return
}
for i := 0; i < len(ual.TokenKeys); i++ {
if k, ok := j[ual.TokenKeys[i]]; ok {
if v, ok2 := k.(string); ok2 {
req.Header.Add(UnverifiedAuditHeader, cleanSub(v))
return
}
}
}
}
}
func (*unverifiedAuditLogFilter) Response(filters.FilterContext) {}
func cleanSub(s string) string {
if re.MatchString(s) {
return s
}
return defaultSub
}