diff --git a/WORKSPACE b/WORKSPACE index a26898a44..30bf2da12 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -92,8 +92,8 @@ _go_image_repos() go_repository( name = "com_github_abbot_go_http_auth", importpath = "github.com/abbot/go-http-auth", - sum = "h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=", - version = "v0.4.0", + sum = "h1:9ZqcMQ0fB+ywKACVjGfZM4C7Uq9D5rq0iSmwIjX187k=", + version = "v0.4.1-0.20181019201920-860ed7f246ff", ) go_repository( diff --git a/go.mod b/go.mod index 2fee104c0..cace213e6 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/buchgr/bazel-remote require ( cloud.google.com/go v0.50.0 // indirect - github.com/abbot/go-http-auth v0.4.0 + github.com/abbot/go-http-auth v0.4.1-0.20181019201920-860ed7f246ff github.com/bazelbuild/remote-apis v0.0.0-20191119143007-b5123b1bb285 github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/djherbis/atime v1.0.0 diff --git a/go.sum b/go.sum index 1a1c8f2a4..45e4df702 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0= -github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= +github.com/abbot/go-http-auth v0.4.1-0.20181019201920-860ed7f246ff h1:9ZqcMQ0fB+ywKACVjGfZM4C7Uq9D5rq0iSmwIjX187k= +github.com/abbot/go-http-auth v0.4.1-0.20181019201920-860ed7f246ff/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= diff --git a/main.go b/main.go index 5b8ee699e..cb23537b1 100644 --- a/main.go +++ b/main.go @@ -268,9 +268,11 @@ func main() { mux.Handle("/metrics", promhttp.Handler()) mux.HandleFunc("/status", h.StatusPageHandler) + var htpasswdSecrets auth.SecretProvider cacheHandler := h.CacheHandler if c.HtpasswdFile != "" { - cacheHandler = wrapAuthHandler(cacheHandler, c.HtpasswdFile, c.Host) + htpasswdSecrets = auth.HtpasswdFileProvider(c.HtpasswdFile) + cacheHandler = wrapAuthHandler(cacheHandler, htpasswdSecrets, c.Host) } if c.IdleTimeout > 0 { cacheHandler = wrapIdleHandler(cacheHandler, c.IdleTimeout, accessLogger, httpServer) @@ -297,6 +299,14 @@ func main() { opts = append(opts, grpc.Creds(creds)) } + if htpasswdSecrets != nil { + gba := server.NewGrpcBasicAuth(htpasswdSecrets) + opts = append(opts, + grpc.StreamInterceptor(gba.StreamServerInterceptor), + grpc.UnaryInterceptor(gba.UnaryServerInterceptor), + ) + } + log.Printf("Starting gRPC server on address %s", addr) err3 := server.ListenAndServeGRPC(addr, opts, @@ -361,8 +371,7 @@ func wrapIdleHandler(handler http.HandlerFunc, idleTimeout time.Duration, access }) } -func wrapAuthHandler(handler http.HandlerFunc, htpasswdFile string, host string) http.HandlerFunc { - secrets := auth.HtpasswdFileProvider(htpasswdFile) +func wrapAuthHandler(handler http.HandlerFunc, secrets auth.SecretProvider, host string) http.HandlerFunc { authenticator := auth.NewBasicAuthenticator(host, secrets) return auth.JustCheck(authenticator, handler) } diff --git a/server/BUILD.bazel b/server/BUILD.bazel index a636994e6..2b1f9aac1 100644 --- a/server/BUILD.bazel +++ b/server/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "grpc.go", "grpc_ac.go", + "grpc_basic_auth.go", "grpc_bytestream.go", "grpc_cas.go", "http.go", @@ -14,6 +15,7 @@ go_library( deps = [ "//cache:go_default_library", "//cache/disk:go_default_library", + "@com_github_abbot_go_http_auth//:go_default_library", "@com_github_bazelbuild_remote_apis//build/bazel/remote/execution/v2:go_default_library", "@com_github_bazelbuild_remote_apis//build/bazel/semver:go_default_library", "@com_github_golang_protobuf//jsonpb:go_default_library_gen", @@ -24,6 +26,7 @@ go_library( "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//codes:go_default_library", "@org_golang_google_grpc//encoding/gzip:go_default_library", + "@org_golang_google_grpc//metadata:go_default_library", "@org_golang_google_grpc//peer:go_default_library", "@org_golang_google_grpc//status:go_default_library", ], diff --git a/server/grpc_basic_auth.go b/server/grpc_basic_auth.go new file mode 100644 index 000000000..e76c29f37 --- /dev/null +++ b/server/grpc_basic_auth.go @@ -0,0 +1,101 @@ +package server + +import ( + "context" + "strings" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + grpc_status "google.golang.org/grpc/status" + + auth "github.com/abbot/go-http-auth" +) + +var ( + errNoMetadata = grpc_status.Error(codes.Unauthenticated, + "no metadata found") + errNoAuthMetadata = grpc_status.Error(codes.Unauthenticated, + "no authentication metadata found") + errAccessDenied = grpc_status.Error(codes.Unauthenticated, + "access denied") +) + +type GrpcBasicAuth struct { + secrets auth.SecretProvider +} + +func NewGrpcBasicAuth(secrets auth.SecretProvider) *GrpcBasicAuth { + return &GrpcBasicAuth{secrets: secrets} +} + +func (b *GrpcBasicAuth) StreamServerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + username, password, err := getLogin(ss.Context()) + if err != nil { + return err + } + if username == "" || password == "" { + return errAccessDenied + } + + if !b.allowed(username, password) { + return errAccessDenied + } + + return handler(srv, ss) +} + +func (b *GrpcBasicAuth) UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + username, password, err := getLogin(ctx) + if err != nil { + return nil, err + } + if username == "" || password == "" { + return nil, errAccessDenied + } + + if !b.allowed(username, password) { + return nil, errAccessDenied + } + + return handler(ctx, req) +} + +func getLogin(ctx context.Context) (username, password string, err error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "", "", errNoMetadata + } + + for k, v := range md { + if k == ":authority" && len(v) > 0 { + // When bazel is run with --remote_cache=grpc://user:pass@address/" + // the value looks like "user:pass@address". + fields := strings.SplitN(v[0], ":", 2) + if len(fields) < 2 { + continue + } + username = fields[0] + + fields = strings.SplitN(fields[1], "@", 2) + if len(fields) < 2 { + continue + } + password = fields[0] + + return username, password, nil + } + } + + return "", "", errNoAuthMetadata +} + +func (b *GrpcBasicAuth) allowed(username, password string) bool { + ignoredRealm := "" + requiredSecret := b.secrets(username, ignoredRealm) + if requiredSecret == "" { + return false // User does not exist. + } + + return auth.CheckSecret(password, requiredSecret) +}