Skip to content

Commit

Permalink
grpc: add basic htpasswd authentication support
Browse files Browse the repository at this point in the history
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/
  • Loading branch information
mostynb committed Feb 26, 2020
1 parent b34f49c commit 1d85a0a
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 8 deletions.
4 changes: 2 additions & 2 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
15 changes: 12 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
3 changes: 3 additions & 0 deletions server/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go_library(
srcs = [
"grpc.go",
"grpc_ac.go",
"grpc_basic_auth.go",
"grpc_bytestream.go",
"grpc_cas.go",
"http.go",
Expand All @@ -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",
Expand All @@ -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",
],
Expand Down
101 changes: 101 additions & 0 deletions server/grpc_basic_auth.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 1d85a0a

Please sign in to comment.