Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: protect HTTP API using access token #328

Merged
merged 4 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions cmd/lassie/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ var daemonFlags = []cli.Flag{
FlagBitswapConcurrency,
FlagGlobalTimeout,
FlagProviderTimeout,
&cli.StringFlag{
Name: "access-token",
Usage: "require HTTP clients to authorize using Bearer scheme and given access token",
Value: "",
},
}

var daemonCmd = &cli.Command{
Expand Down Expand Up @@ -119,7 +124,8 @@ func daemonAction(cctx *cli.Context) error {
port := cctx.Uint("port")
tempDir := cctx.String("tempdir")
maxBlocks := cctx.Uint64("maxblocks")
httpServerCfg := getHttpServerConfigForDaemon(address, port, tempDir, maxBlocks)
accessToken := cctx.String("access-token")
httpServerCfg := getHttpServerConfigForDaemon(address, port, tempDir, maxBlocks, accessToken)

// event recorder config
eventRecorderURL := cctx.String("event-recorder-url")
Expand Down Expand Up @@ -200,11 +206,12 @@ func defaultDaemonRun(
}

// getHttpServerConfigForDaemon returns a HttpServerConfig for the daemon command.
func getHttpServerConfigForDaemon(address string, port uint, tempDir string, maxBlocks uint64) httpserver.HttpServerConfig {
func getHttpServerConfigForDaemon(address string, port uint, tempDir string, maxBlocks uint64, accessToken string) httpserver.HttpServerConfig {
return httpserver.HttpServerConfig{
Address: address,
Port: port,
TempDir: tempDir,
MaxBlocksPerRequest: maxBlocks,
AccessToken: accessToken,
}
}
9 changes: 9 additions & 0 deletions cmd/lassie/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func TestDaemonCommandFlags(t *testing.T) {
require.Equal(t, "127.0.0.1", hCfg.Address)
require.Equal(t, uint(0), hCfg.Port)
require.Equal(t, uint64(0), hCfg.MaxBlocksPerRequest)
require.Equal(t, "", hCfg.AccessToken)

// event recorder config
require.Equal(t, "", erCfg.EndpointURL)
Expand Down Expand Up @@ -172,6 +173,14 @@ func TestDaemonCommandFlags(t *testing.T) {
return nil
},
},
{
name: "with access token",
args: []string{"daemon", "--access-token", "super-secret"},
assert: func(ctx context.Context, lCfg *l.LassieConfig, hCfg h.HttpServerConfig, erCfg *a.EventRecorderConfig) error {
require.Equal(t, "super-secret", hCfg.AccessToken)
return nil
},
},
}

for _, test := range tests {
Expand Down
59 changes: 45 additions & 14 deletions pkg/internal/itest/http_fetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,21 @@ func TestHttpFetch(t *testing.T) {
wrapPath := "/want2/want1/want0"

testCases := []struct {
name string
graphsyncRemotes int
bitswapRemotes int
httpRemotes int
disableGraphsync bool
expectFail bool
expectUncleanEnd bool
modifyHttpConfig func(httpserver.HttpServerConfig) httpserver.HttpServerConfig
generate func(*testing.T, io.Reader, []testpeer.TestPeer) []unixfs.DirEntry
paths []string
setHeader headerSetter
modifyQueries []queryModifier
validateBodies []bodyValidator
lassieOpts lassieOptsGen
name string
graphsyncRemotes int
bitswapRemotes int
httpRemotes int
disableGraphsync bool
expectFail bool
expectUncleanEnd bool
expectUnauthorized bool
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new test-case config - expectUnauthorized.

Alternatively, we can also change the type of expectFail to accept the expected HTTP status code. It would be cleaner (IMO), but it would also require more changes in the existing test cases. Let me know which option you prefer!

modifyHttpConfig func(httpserver.HttpServerConfig) httpserver.HttpServerConfig
generate func(*testing.T, io.Reader, []testpeer.TestPeer) []unixfs.DirEntry
paths []string
setHeader headerSetter
modifyQueries []queryModifier
validateBodies []bodyValidator
lassieOpts lassieOptsGen
}{
{
name: "graphsync large sharded file",
Expand Down Expand Up @@ -918,6 +919,34 @@ func TestHttpFetch(t *testing.T) {
validateCarBody(t, body, srcData.Root, wantCids, true)
}},
},
{
name: "with access token - rejects anonymous requests",
httpRemotes: 1,
generate: func(t *testing.T, rndReader io.Reader, remotes []testpeer.TestPeer) []unixfs.DirEntry {
return []unixfs.DirEntry{unixfs.GenerateFile(t, remotes[0].LinkSystem, rndReader, 1024)}
},
modifyHttpConfig: func(cfg httpserver.HttpServerConfig) httpserver.HttpServerConfig {
cfg.AccessToken = "super-secret"
return cfg
},
expectUnauthorized: true,
},
{
name: "with access token - allows requests with authorization header",
httpRemotes: 1,
generate: func(t *testing.T, rndReader io.Reader, remotes []testpeer.TestPeer) []unixfs.DirEntry {
return []unixfs.DirEntry{unixfs.GenerateFile(t, remotes[0].LinkSystem, rndReader, 1024)}
},
modifyHttpConfig: func(cfg httpserver.HttpServerConfig) httpserver.HttpServerConfig {
cfg.AccessToken = "super-secret"
return cfg
},
setHeader: func(header http.Header) {
header.Set("Authorization", "Bearer super-secret")
header.Add("Accept", "application/vnd.ipld.car")
},
expectUnauthorized: false,
},
}

for _, testCase := range testCases {
Expand Down Expand Up @@ -1031,6 +1060,8 @@ func TestHttpFetch(t *testing.T) {
for i, resp := range responses {
if testCase.expectFail {
req.Equal(http.StatusGatewayTimeout, resp.StatusCode)
} else if testCase.expectUnauthorized {
req.Equal(http.StatusUnauthorized, resp.StatusCode)
} else {
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
Expand Down
20 changes: 20 additions & 0 deletions pkg/server/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type HttpServerConfig struct {
Port uint
TempDir string
MaxBlocksPerRequest uint64
AccessToken string
}

type contextKey struct {
Expand All @@ -52,6 +53,11 @@ func NewHttpServer(ctx context.Context, lassie *lassie.Lassie, cfg HttpServerCon
// create server
mux := http.NewServeMux()
handler := servertiming.Middleware(mux, nil)

if cfg.AccessToken != "" {
handler = authorizationMiddleware(handler, cfg.AccessToken)
}

server := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
BaseContext: func(listener net.Listener) context.Context { return ctx },
Expand Down Expand Up @@ -102,3 +108,17 @@ func (s *HttpServer) Close() error {
s.cancel()
return s.server.Shutdown(context.Background())
}

func authorizationMiddleware(next http.Handler, accessToken string) http.Handler {
requiredHeaderValue := fmt.Sprintf("Bearer %s", accessToken)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == requiredHeaderValue {
next.ServeHTTP(w, r)
return
}

// Unauthorized
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, "Unauthorized")
})
}