-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add audit filter that will be able to catch authn failures
- Loading branch information
Showing
2 changed files
with
186 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
package origin | ||
|
||
import ( | ||
"bufio" | ||
"encoding/base64" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
"github.com/golang/glog" | ||
"github.com/pborman/uuid" | ||
|
||
utilnet "k8s.io/apimachinery/pkg/util/net" | ||
apiresponsewriters "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" | ||
apirequest "k8s.io/apiserver/pkg/endpoints/request" | ||
) | ||
|
||
const AuditTriggered = "audit-triggered" | ||
|
||
var _ http.ResponseWriter = &auditResponseWriter{} | ||
|
||
// auditResponseWriter is responsible for serving as a fallback audit, responsible | ||
// for logging failed authn events. | ||
type auditResponseWriter struct { | ||
http.ResponseWriter | ||
contextMapper apirequest.RequestContextMapper | ||
req *http.Request | ||
out io.Writer | ||
} | ||
|
||
func (a *auditResponseWriter) WriteHeader(code int) { | ||
ctx, ok := a.contextMapper.Get(a.req) | ||
if !ok { | ||
apiresponsewriters.InternalError(a.ResponseWriter, a.req, errors.New("no context found for request")) | ||
return | ||
} | ||
// if the original audit handler triggered there's no need to do anything | ||
triggeredValue := ctx.Value(AuditTriggered) | ||
if triggered, ok := triggeredValue.(bool); ok && triggered { | ||
a.ResponseWriter.WriteHeader(code) | ||
return | ||
} | ||
id := uuid.NewRandom().String() | ||
line := fmt.Sprintf("%s AUDIT: id=%q ip=%q method=%q user=%q uri=%q\n", | ||
time.Now().Format(time.RFC3339Nano), id, utilnet.GetClientIP(a.req), a.req.Method, getUsername(a.req), a.req.URL) | ||
if _, err := fmt.Fprint(a.out, line); err != nil { | ||
glog.Errorf("Unable to write audit log: %s, the error is: %v", line, err) | ||
} | ||
line = fmt.Sprintf("%s AUDIT: id=%q response=\"%d\"\n", time.Now().Format(time.RFC3339Nano), id, code) | ||
if _, err := fmt.Fprint(a.out, line); err != nil { | ||
glog.Errorf("Unable to write audit log: %s, the error is: %v", line, err) | ||
} | ||
|
||
a.ResponseWriter.WriteHeader(code) | ||
} | ||
|
||
// fancyResponseWriterDelegator implements http.CloseNotifier, http.Flusher and | ||
// http.Hijacker which are needed to make certain http operation (e.g. watch, rsh, etc) | ||
// working. | ||
type fancyResponseWriterDelegator struct { | ||
*auditResponseWriter | ||
} | ||
|
||
func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool { | ||
return f.ResponseWriter.(http.CloseNotifier).CloseNotify() | ||
} | ||
|
||
func (f *fancyResponseWriterDelegator) Flush() { | ||
f.ResponseWriter.(http.Flusher).Flush() | ||
} | ||
|
||
func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) { | ||
return f.ResponseWriter.(http.Hijacker).Hijack() | ||
} | ||
|
||
var _ http.CloseNotifier = &fancyResponseWriterDelegator{} | ||
var _ http.Flusher = &fancyResponseWriterDelegator{} | ||
var _ http.Hijacker = &fancyResponseWriterDelegator{} | ||
|
||
// WithAuditTriggeredMarker is responsible for marking that the audit did actually | ||
// took place and fallback audit should not trigger. | ||
func WithAuditTriggeredMarker(handler http.Handler, contextMapper apirequest.RequestContextMapper, out io.Writer) http.Handler { | ||
if out == nil { | ||
return handler | ||
} | ||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||
ctx, ok := contextMapper.Get(req) | ||
if !ok { | ||
apiresponsewriters.InternalError(w, req, errors.New("no context found for request")) | ||
return | ||
} | ||
contextMapper.Update(req, apirequest.WithValue(ctx, AuditTriggered, true)) | ||
handler.ServeHTTP(w, req) | ||
}) | ||
} | ||
|
||
// WithAuthFallbackAudit decorates a http.Handler with a fallback audit, logging | ||
// information only when the original one did was not triggered. | ||
// This needs to be used with WithAuditTriggeredMarker, which wraps the original | ||
// audit filter. | ||
func WithAuthFallbackAudit(handler http.Handler, contextMapper apirequest.RequestContextMapper, out io.Writer) http.Handler { | ||
if out == nil { | ||
return handler | ||
} | ||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | ||
respWriter := decorateResponseWriter(w, req, contextMapper, out) | ||
handler.ServeHTTP(respWriter, req) | ||
}) | ||
} | ||
|
||
// decorateResponseWriter is a copy method from the upstream audit, adapted | ||
// to work with the fallback audit mechanism. | ||
func decorateResponseWriter(responseWriter http.ResponseWriter, req *http.Request, | ||
contextMapper apirequest.RequestContextMapper, out io.Writer) http.ResponseWriter { | ||
delegate := &auditResponseWriter{ | ||
ResponseWriter: responseWriter, | ||
req: req, | ||
contextMapper: contextMapper, | ||
out: out, | ||
} | ||
// check if the ResponseWriter we're wrapping is the fancy one we need | ||
// or if the basic is sufficient | ||
_, cn := responseWriter.(http.CloseNotifier) | ||
_, fl := responseWriter.(http.Flusher) | ||
_, hj := responseWriter.(http.Hijacker) | ||
if cn && fl && hj { | ||
return &fancyResponseWriterDelegator{delegate} | ||
} | ||
return delegate | ||
} | ||
|
||
// getUsername returns username or information on the authn method being used. | ||
func getUsername(req *http.Request) string { | ||
auth := strings.TrimSpace(req.Header.Get("Authorization")) | ||
if auth == "" { | ||
return "<none>" | ||
} | ||
|
||
// check basic auth | ||
const basicScheme string = "Basic " | ||
if strings.HasPrefix(auth, basicScheme) { | ||
const basicInvalid = "<basic_invalid>" | ||
str, err := base64.StdEncoding.DecodeString(auth[len(basicScheme):]) | ||
if err != nil { | ||
return basicInvalid | ||
} | ||
|
||
cred := strings.SplitN(string(str), ":", 2) | ||
if len(cred) < 2 { | ||
return basicInvalid | ||
} | ||
|
||
return cred[0] | ||
} | ||
|
||
// check bearer token | ||
parts := strings.Split(auth, " ") | ||
if len(parts) > 1 && strings.ToLower(parts[0]) == "bearer" { | ||
token := parts[1] | ||
// Empty bearer tokens aren't valid | ||
if len(token) == 0 { | ||
return "<bearer_invalid>" | ||
} | ||
|
||
return "<bearer>" | ||
} | ||
|
||
// other tokens | ||
token := strings.TrimSpace(req.URL.Query().Get("access_token")) | ||
if len(token) > 0 { | ||
return "<token>" | ||
} | ||
|
||
return "<unknown>" | ||
} |
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