From 1d85a0afaff1486fc0287a5b482b27f14107df27 Mon Sep 17 00:00:00 2001 From: Mostyn Bramley-Moore Date: Wed, 26 Feb 2020 08:25:44 +0100 Subject: [PATCH] grpc: add basic htpasswd authentication support Add basic htpasswd auth to the gRPC endpoint, for feature parity with the HTTP endpoint. Until now, gRPC has been unauthenticated. To test, add the user/pass to bazel like so: --remote_cache=grpc://user:pass@address:port/ --- WORKSPACE | 4 +- go.mod | 2 +- go.sum | 4 +- main.go | 15 ++++-- server/BUILD.bazel | 3 ++ server/grpc_basic_auth.go | 101 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 server/grpc_basic_auth.go 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) +}