Skip to content

Commit

Permalink
server: add /api/v2/ tree with auth/pagination, port some status endp…
Browse files Browse the repository at this point in the history
…oints

This change adds the skeleton for a new API tree that lives in
`/api/v2/` in the http listener, and currently reimplements a few endpoints
that are also implemented in `/_status/`. The new v2 API tree avoids the
need to use GRPC Gateway, as well as cookie-based authentication which is
less intuitive and idiomatic for REST APIs. Instead, for authentication,
it uses a new session header that needs to be set on every request.

As many RPC fan-out APIs use statusServer.iterateNodes, this change
implements a pagination-aware method, paginatedIterateNodes, that
works on a sorted set of node IDs and arranges results in such a way
to be able to return the next `limit` results of an arbitary slice
after the `next` cursor passed in. An example of how this works in practice
is the new `/api/v2/sessions/` endpoint.

A dependency on gorilla/mux is added to be able to pattern-match
arguments in the URL. This was already an indirect dependency; now it's
a direct dependency of cockroach.

TODO that are likely to fall over into future PRs:
 - API Documentation, need to explore using swagger here.
 - Porting over remaining /_admin/ and /_status/ APIs, incl. SQL based ones

Part of #55947.

Release note (api change): Adds a new API tree, in /api/v2/*, currently
undocumented, that avoids the use of and cookie-based
authentication in favour of sessions in headers, and support for pagination.
  • Loading branch information
itsbilal committed Jan 15, 2021
1 parent af2a567 commit d618d16
Show file tree
Hide file tree
Showing 16 changed files with 1,361 additions and 79 deletions.
4 changes: 2 additions & 2 deletions DEPS.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -1169,8 +1169,8 @@ def go_deps():
name = "com_github_gorilla_mux",
build_file_proto_mode = "disable_global",
importpath = "github.com/gorilla/mux",
sum = "h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=",
version = "v1.7.4",
sum = "h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=",
version = "v1.8.0",
)
go_repository(
name = "com_github_gorilla_securecookie",
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ require (
github.com/google/pprof v0.0.0-20190109223431-e84dfd68c163
github.com/googleapis/gax-go v2.0.2+incompatible // indirect
github.com/gorhill/cronexpr v0.0.0-20140423231348-a557574d6c02
github.com/gorilla/mux v1.7.4 // indirect
github.com/gorilla/mux v1.8.0
github.com/goware/modvendor v0.3.0
github.com/grpc-ecosystem/grpc-gateway v1.13.0
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,8 @@ github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorhill/cronexpr v0.0.0-20140423231348-a557574d6c02 h1:Spo+4PFAGDqULAsZ7J69MOxq4/fwgZ0zvmDTBqpq7yU=
github.com/gorhill/cronexpr v0.0.0-20140423231348-a557574d6c02/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
Expand Down
2 changes: 2 additions & 0 deletions pkg/server/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go_library(
name = "server",
srcs = [
"admin.go",
"api.go",
"api_error.go",
"authentication.go",
"auto_upgrade.go",
Expand Down Expand Up @@ -166,6 +167,7 @@ go_library(
"@com_github_cockroachdb_sentry_go//:sentry-go",
"@com_github_elastic_gosigar//:gosigar",
"@com_github_gogo_protobuf//proto",
"@com_github_gorilla_mux///mux",
"@com_github_grpc_ecosystem_grpc_gateway//runtime:go_default_library",
"@com_github_grpc_ecosystem_grpc_gateway//utilities:go_default_library",
"@com_github_marusama_semaphore//:semaphore",
Expand Down
198 changes: 198 additions & 0 deletions pkg/server/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package server

import (
"context"
"encoding/json"
"net/http"
"strconv"

"github.com/cockroachdb/cockroach/pkg/server/serverpb"
"github.com/gorilla/mux"
)

const (
apiV2Path = "/api/v2/"
apiV2AuthHeader = "X-Cockroach-API-Session"
)

func writeJsonResponse(w http.ResponseWriter, code int, payload interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)

res, err := json.Marshal(payload)
if err != nil {
panic(err)
}
if _, err := w.Write(res); err != nil {
panic(err)
}
}

// apiV2Server implements endpoints under apiV2Path.
type apiV2Server struct {
admin *adminServer
status *statusServer
mux *mux.Router
}

func newApiServer(ctx context.Context, s *Server) *apiV2Server {
authServer := newAuthenticationV2Server(ctx, s, apiV2Path)
innerMux := mux.NewRouter()

authMux := newAuthenticationV2Mux(authServer, innerMux)
outerMux := mux.NewRouter()
a := &apiV2Server{
admin: s.admin,
status: s.status,
mux: outerMux,
}
a.registerRoutes(innerMux, authMux)
a.mux.Handle(apiV2Path + "login/", authServer)
a.mux.Handle(apiV2Path + "logout/", authServer)
return a
}

func (a *apiV2Server) registerRoutes(innerMux *mux.Router, authMux http.Handler) {
routeDefinitions := []struct{
endpoint string
handler http.HandlerFunc
requiresAuth bool
requiresAdmin bool
}{
{"sessions/", a.listSessions, true /* requiresAuth */ , true /* requiresAdmin */},
{"hotranges/", a.hotRanges, true /* requiresAuth */ , true /* requiresAdmin */},
{"ranges/{range_id}/", a.rangeHandler, true /* requiresAuth */ , true /* requiresAdmin */},
{"nodes/", a.nodes, true /* requiresAuth */ , false /* requiresAdmin */},
}

// For all routes requiring authentication, have the outer mux (a.mux)
// send requests through to the authMux, and also register the relevant route
// in innerMux. Routes not requiring login can directly be handled in a.mux.
for _, route := range routeDefinitions {
if route.requiresAuth {
a.mux.Handle(apiV2Path + route.endpoint, authMux)
handler := http.Handler(route.handler)
if route.requiresAdmin {
handler = &requireAdminWrapper{
a: a.admin.adminPrivilegeChecker,
inner: route.handler,
}
}
innerMux.Handle(apiV2Path + route.endpoint, handler)
} else {
a.mux.HandleFunc(apiV2Path + route.endpoint, route.handler)
}
}
}

type listSessionsResponse struct {
serverpb.ListSessionsResponse

Next string `json:"next"`
}

func (a *apiV2Server) listSessions(w http.ResponseWriter, r *http.Request) {
limit, start := getRPCPaginationValues(r)
req := &serverpb.ListSessionsRequest{Username: r.Context().Value(webSessionUserKey{}).(string)}
response := &listSessionsResponse{}

responseProto, pagState, err := a.status.listSessionsHelper(r.Context(), req, limit, start)
var nextBytes []byte
if nextBytes, err = pagState.MarshalText(); err != nil {
err := serverpb.ListSessionsError{Message: err.Error()}
response.Errors = append(response.Errors, err)
} else {
response.Next = string(nextBytes)
}
response.ListSessionsResponse = *responseProto
writeJsonResponse(w, http.StatusOK, response)
}

type rangeResponse struct {
serverpb.RangeResponse

Next string `json:"next"`
}

func (a *apiV2Server) rangeHandler(w http.ResponseWriter, r *http.Request) {
limit, start := getRPCPaginationValues(r)
var err error
var rangeID int64
vars := mux.Vars(r)
if rangeID, err = strconv.ParseInt(vars["range_id"], 10, 64); err != nil {
http.Error(w, "invalid range id", http.StatusBadRequest)
return
}

req := &serverpb.RangeRequest{RangeId: rangeID}
response := &rangeResponse{}
responseProto, next, err := a.status.rangeHelper(r.Context(), req, limit, start)
if err != nil {
apiV2InternalError(r.Context(), err, w)
return
}
response.RangeResponse = *responseProto
if nextBytes, err := next.MarshalText(); err == nil {
response.Next = string(nextBytes)
}
writeJsonResponse(w, http.StatusOK, response)
}

type hotRangesResponse struct {
serverpb.HotRangesResponse

Next string `json:"next"`
}

func (a *apiV2Server) hotRanges(w http.ResponseWriter, r *http.Request) {
limit, start := getRPCPaginationValues(r)
req := &serverpb.HotRangesRequest{NodeID: r.URL.Query().Get("node_id")}
response := &hotRangesResponse{}

responseProto, next, err := a.status.hotRangesHelper(r.Context(), req, limit, start)
if err != nil {
apiV2InternalError(r.Context(), err, w)
return
}
response.HotRangesResponse = *responseProto
if nextBytes, err := next.MarshalText(); err == nil {
response.Next = string(nextBytes)
}
writeJsonResponse(w, http.StatusOK, response)
}

type nodesResponse struct {
serverpb.NodesResponse

Next int `json:"next"`
}

func (a *apiV2Server) nodes(w http.ResponseWriter, r *http.Request) {
limit, offset := getSimplePaginationValues(r)
req := &serverpb.NodesRequest{}
response := &nodesResponse{}

responseProto, next, err := a.status.nodesHelper(r.Context(), req, limit, offset)
if err != nil {
apiV2InternalError(r.Context(), err, w)
return
}
response.NodesResponse = *responseProto
response.Next = next
writeJsonResponse(w, http.StatusOK, response)
}

func (a *apiV2Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.mux.ServeHTTP(w, r)
}

Loading

0 comments on commit d618d16

Please sign in to comment.