@@ -2,26 +2,40 @@ package fetch
22
33import (
44 "context"
5+ "encoding/json"
6+ "fmt"
57 "log"
8+ "strings"
69
710 remoteasset "github.com/bazelbuild/remote-apis/build/bazel/remote/asset/v1"
811 "github.com/buildbarn/bb-remote-asset/pkg/qualifier"
912 "google.golang.org/grpc/status"
1013)
1114
15+ const redactedValue = "<redacted>"
16+
1217type loggingFetcher struct {
13- fetcher Fetcher
18+ fetcher Fetcher
19+ loggedHeaderNames map [string ]struct {}
1420}
1521
16- // NewLoggingFetcher creates a fetcher which logs requests and results
17- func NewLoggingFetcher (fetcher Fetcher ) Fetcher {
22+ // NewLoggingFetcher creates a fetcher which logs requests and results.
23+ //
24+ // Qualifier values that carry HTTP header contents which may contain secrets such as "Authorization" tokens.
25+ // Their values are redacted from the log unless the corresponding header name is present in loggedHeaderNames.
26+ func NewLoggingFetcher (fetcher Fetcher , loggedHeaderNames []string ) Fetcher {
27+ names := make (map [string ]struct {}, len (loggedHeaderNames ))
28+ for _ , name := range loggedHeaderNames {
29+ names [strings .ToLower (name )] = struct {}{}
30+ }
1831 return & loggingFetcher {
19- fetcher : fetcher ,
32+ fetcher : fetcher ,
33+ loggedHeaderNames : names ,
2034 }
2135}
2236
2337func (lf * loggingFetcher ) FetchBlob (ctx context.Context , req * remoteasset.FetchBlobRequest ) (* remoteasset.FetchBlobResponse , error ) {
24- log .Printf ("Fetching Blob %s with qualifiers %s" , req .Uris , req .Qualifiers )
38+ log .Printf ("Fetching Blob %s with qualifiers %s" , req .Uris , lf . redactQualifiers ( req .Qualifiers ) )
2539 resp , err := lf .fetcher .FetchBlob (ctx , req )
2640 if err == nil {
2741 log .Printf ("FetchBlob completed for %s with status code %d" , req .Uris , resp .Status .GetCode ())
@@ -32,7 +46,7 @@ func (lf *loggingFetcher) FetchBlob(ctx context.Context, req *remoteasset.FetchB
3246}
3347
3448func (lf * loggingFetcher ) FetchDirectory (ctx context.Context , req * remoteasset.FetchDirectoryRequest ) (* remoteasset.FetchDirectoryResponse , error ) {
35- log .Printf ("Fetching Directory %s with qualifiers %s" , req .Uris , req .Qualifiers )
49+ log .Printf ("Fetching Directory %s with qualifiers %s" , req .Uris , lf . redactQualifiers ( req .Qualifiers ) )
3650 resp , err := lf .fetcher .FetchDirectory (ctx , req )
3751 if err == nil {
3852 log .Printf ("FetchBlob completed for %s with status code %d" , req .Uris , resp .Status .GetCode ())
@@ -45,3 +59,63 @@ func (lf *loggingFetcher) FetchDirectory(ctx context.Context, req *remoteasset.F
4559func (lf * loggingFetcher ) CheckQualifiers (qualifiers qualifier.Set ) qualifier.Set {
4660 return lf .fetcher .CheckQualifiers (qualifiers )
4761}
62+
63+ func (lf * loggingFetcher ) isHeaderNameLogged (headerName string ) bool {
64+ _ , ok := lf .loggedHeaderNames [strings .ToLower (headerName )]
65+ return ok
66+ }
67+
68+ // redactQualifiers formats qualifiers for logging,
69+ // redacting the values of any HTTP header qualifiers whose header name isn't whitelisted.
70+ func (lf * loggingFetcher ) redactQualifiers (qualifiers []* remoteasset.Qualifier ) string {
71+ parts := make ([]string , 0 , len (qualifiers ))
72+ for _ , q := range qualifiers {
73+ parts = append (parts , fmt .Sprintf ("name:%q value:%q" , q .Name , lf .redactQualifierValue (q )))
74+ }
75+ return "[" + strings .Join (parts , " " ) + "]"
76+ }
77+
78+ func (lf * loggingFetcher ) redactQualifierValue (q * remoteasset.Qualifier ) string {
79+ switch {
80+ case q .Name == QualifierLegacyBazelHTTPHeaders :
81+ return lf .redactLegacyAuthHeaders (q .Value )
82+ case strings .HasPrefix (q .Name , QualifierHTTPHeaderURLPrefix ):
83+ _ , header , err := parseHTTPHeaderURLQualifierName (q .Name )
84+ if err != nil || ! lf .isHeaderNameLogged (header ) {
85+ return redactedValue
86+ }
87+ return q .Value
88+ case strings .HasPrefix (q .Name , QualifierHTTPHeaderPrefix ):
89+ header := strings .TrimPrefix (q .Name , QualifierHTTPHeaderPrefix )
90+ if ! lf .isHeaderNameLogged (header ) {
91+ return redactedValue
92+ }
93+ return q .Value
94+ default :
95+ return q .Value
96+ }
97+ }
98+
99+ // redactLegacyAuthHeaders redacts the header values carried by a legacy
100+ // "bazel.auth_headers" qualifier, keeping only whitelisted header names.
101+ func (lf * loggingFetcher ) redactLegacyAuthHeaders (value string ) string {
102+ ah , err := NewAuthHeadersFromQualifier (value )
103+ if err != nil {
104+ return redactedValue
105+ }
106+ redacted := NewAuthHeaders ()
107+ for uri , headers := range * ah {
108+ for header , v := range headers {
109+ if lf .isHeaderNameLogged (header ) {
110+ redacted .AddHeader (uri , header , v )
111+ } else {
112+ redacted .AddHeader (uri , header , redactedValue )
113+ }
114+ }
115+ }
116+ b , err := json .Marshal (redacted )
117+ if err != nil {
118+ return redactedValue
119+ }
120+ return string (b )
121+ }
0 commit comments