Skip to content

Commit

Permalink
Add authn for graphql and http admin endpoints (#5162)
Browse files Browse the repository at this point in the history
Fixes #4758.
This PR adds authentication to following endpoints:

/admin/backup (http & graphql)
/admin/config/lru_mb (http [GET & PUT] & graphql [query & mutation])
/admin/draining (http & graphql)
/admin/export (http & graphql)
/admin/shutdown (http & graphql)
/admin/restore (graphql only)
/admin/listBackups (graphql only)
Now, all the above http endpoints and their corresponding graphql versions have following kinds of auth:

IP White-listing, if --whitelist flag is passed to alpha
Poor-man's auth, if --auth_token flag is passed to alpha
Guardian only access, if ACL is enabled
This PR also adds query for config in graphql admin, as it was missing earlier.

In addition to above points:

All the /admin endpoints apply Poor-man's auth check at http level itself, while other auth checks are routed through graphql resolvers.
GraphQL Resolvers for health/state and the ones related to ACL User/Group have IP whitelisting middleware applied, while dgraph handles Guardian auth for them.
/alter has the existing behaviour of checking only Poor-man's and Guardian auth.
GraphQL Resolvers related to schema don't apply IP whitelisting as to keep them in sync with /alter. They do apply Guardian auth.
Any GraphQL admin introspection queries don't require IP whitelisting or Guardian auth.
  • Loading branch information
abhimanyusinghgaur committed May 19, 2020
1 parent bae2b1b commit 8992238
Show file tree
Hide file tree
Showing 24 changed files with 968 additions and 274 deletions.
186 changes: 115 additions & 71 deletions dgraph/cmd/alpha/admin.go
Expand Up @@ -17,76 +17,100 @@
package alpha

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"strconv"

"github.com/dgraph-io/dgraph/posting"
"github.com/dgraph-io/dgraph/graphql/schema"
"github.com/dgraph-io/dgraph/graphql/web"

"github.com/dgraph-io/dgraph/worker"
"github.com/dgraph-io/dgraph/x"
"github.com/golang/glog"
)

// handlerInit does some standard checks. Returns false if something is wrong.
func handlerInit(w http.ResponseWriter, r *http.Request, allowedMethods map[string]bool) bool {
if _, ok := allowedMethods[r.Method]; !ok {
x.SetStatus(w, x.ErrorInvalidMethod, "Invalid method")
return false
}
type allowedMethods map[string]bool

ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil || (!ipInIPWhitelistRanges(ip) && !net.ParseIP(ip).IsLoopback()) {
x.SetStatus(w, x.ErrorUnauthorized, fmt.Sprintf("Request from IP: %v", ip))
// hasPoormansAuth checks if poorman's auth is required and if so whether the given http request has
// poorman's auth in it or not
func hasPoormansAuth(r *http.Request) bool {
if worker.Config.AuthToken != "" && worker.Config.AuthToken != r.Header.Get(
"X-Dgraph-AuthToken") {
return false
}
return true
}

func drainingHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPut, http.MethodPost:
enableStr := r.URL.Query().Get("enable")

enable, err := strconv.ParseBool(enableStr)
if err != nil {
x.SetStatus(w, x.ErrorInvalidRequest,
"Found invalid value for the enable parameter")
func allowedMethodsHandler(allowedMethods allowedMethods, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, ok := allowedMethods[r.Method]; !ok {
x.SetStatus(w, x.ErrorInvalidMethod, "Invalid method")
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

x.UpdateDrainingMode(enable)
_, err = w.Write([]byte(fmt.Sprintf(`{"code": "Success",`+
`"message": "draining mode has been set to %v"}`, enable)))
if err != nil {
glog.Errorf("Failed to write response: %v", err)
next.ServeHTTP(w, r)
})
}

// adminAuthHandler does some standard checks for admin endpoints.
// It returns if something is wrong. Otherwise, it lets the given handler serve the request.
func adminAuthHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !hasPoormansAuth(r) {
x.SetStatus(w, x.ErrorUnauthorized, "Invalid X-Dgraph-AuthToken")
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}

next.ServeHTTP(w, r)
})
}

func shutDownHandler(w http.ResponseWriter, r *http.Request) {
if !handlerInit(w, r, map[string]bool{
http.MethodGet: true,
}) {
func drainingHandler(w http.ResponseWriter, r *http.Request, adminServer web.IServeGraphQL) {
enableStr := r.URL.Query().Get("enable")

enable, err := strconv.ParseBool(enableStr)
if err != nil {
x.SetStatus(w, x.ErrorInvalidRequest,
"Found invalid value for the enable parameter")
return
}

close(worker.ShutdownCh)
gqlReq := &schema.Request{
Query: `
mutation draining($enable: Boolean) {
draining(enable: $enable) {
response {
code
}
}
}`,
Variables: map[string]interface{}{"enable": enable},
}
_ = resolveWithAdminServer(gqlReq, r, adminServer)
w.Header().Set("Content-Type", "application/json")
x.Check2(w.Write([]byte(`{"code": "Success", "message": "Server is shutting down"}`)))
x.Check2(w.Write([]byte(fmt.Sprintf(`{"code": "Success",`+
`"message": "draining mode has been set to %v"}`, enable))))
}

func exportHandler(w http.ResponseWriter, r *http.Request) {
if !handlerInit(w, r, map[string]bool{
http.MethodGet: true,
}) {
return
func shutDownHandler(w http.ResponseWriter, r *http.Request, adminServer web.IServeGraphQL) {
gqlReq := &schema.Request{
Query: `
mutation {
shutdown {
response {
code
}
}
}`,
}
_ = resolveWithAdminServer(gqlReq, r, adminServer)
w.Header().Set("Content-Type", "application/json")
x.Check2(w.Write([]byte(`{"code": "Success", "message": "Server is shutting down"}`)))
}

func exportHandler(w http.ResponseWriter, r *http.Request, adminServer web.IServeGraphQL) {
if err := r.ParseForm(); err != nil {
x.SetHttpStatus(w, http.StatusBadRequest, "Parse of export request failed.")
return
Expand All @@ -105,26 +129,37 @@ func exportHandler(w http.ResponseWriter, r *http.Request) {
return
}
}
if err := worker.ExportOverNetwork(context.Background(), format); err != nil {
x.SetStatus(w, err.Error(), "Export failed.")

gqlReq := &schema.Request{
Query: `
mutation export($format: String) {
export(input: {format: $format}) {
response {
code
}
}
}`,
Variables: map[string]interface{}{},
}
resp := resolveWithAdminServer(gqlReq, r, adminServer)
if len(resp.Errors) != 0 {
x.SetStatus(w, resp.Errors[0].Message, "Export failed.")
return
}
w.Header().Set("Content-Type", "application/json")
x.Check2(w.Write([]byte(`{"code": "Success", "message": "Export completed."}`)))
}

func memoryLimitHandler(w http.ResponseWriter, r *http.Request) {
func memoryLimitHandler(w http.ResponseWriter, r *http.Request, adminServer web.IServeGraphQL) {
switch r.Method {
case http.MethodGet:
memoryLimitGetHandler(w, r)
memoryLimitGetHandler(w, r, adminServer)
case http.MethodPut:
memoryLimitPutHandler(w, r)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
memoryLimitPutHandler(w, r, adminServer)
}
}

func memoryLimitPutHandler(w http.ResponseWriter, r *http.Request) {
func memoryLimitPutHandler(w http.ResponseWriter, r *http.Request, adminServer web.IServeGraphQL) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
Expand All @@ -135,36 +170,45 @@ func memoryLimitPutHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
gqlReq := &schema.Request{
Query: `
mutation config($lruMb: Float) {
config(input: {lruMb: $lruMb}) {
response {
code
}
}
}`,
Variables: map[string]interface{}{"lruMb": memoryMB},
}
resp := resolveWithAdminServer(gqlReq, r, adminServer)

if err := worker.UpdateLruMb(memoryMB); err != nil {
if len(resp.Errors) != 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err.Error())
x.Check2(fmt.Fprint(w, resp.Errors[0].Message))
return
}
w.WriteHeader(http.StatusOK)
}

func memoryLimitGetHandler(w http.ResponseWriter, r *http.Request) {
posting.Config.Lock()
memoryMB := posting.Config.AllottedMemory
posting.Config.Unlock()

if _, err := fmt.Fprintln(w, memoryMB); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
func memoryLimitGetHandler(w http.ResponseWriter, r *http.Request, adminServer web.IServeGraphQL) {
gqlReq := &schema.Request{
Query: `
query {
config {
lruMb
}
}`,
}
}

func ipInIPWhitelistRanges(ipString string) bool {
ip := net.ParseIP(ipString)

if ip == nil {
return false
resp := resolveWithAdminServer(gqlReq, r, adminServer)
var data struct {
Config struct {
LruMb float64
}
}
x.Check(json.Unmarshal(resp.Data.Bytes(), &data))

for _, ipRange := range x.WorkerConfig.WhiteListedIPRanges {
if bytes.Compare(ip, ipRange.Lower) >= 0 && bytes.Compare(ip, ipRange.Upper) <= 0 {
return true
}
if _, err := fmt.Fprintln(w, data.Config.LruMb); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return false
}
56 changes: 29 additions & 27 deletions dgraph/cmd/alpha/admin_backup.go
Expand Up @@ -19,43 +19,45 @@
package alpha

import (
"context"
"net/http"

"github.com/dgraph-io/dgraph/protos/pb"
"github.com/dgraph-io/dgraph/worker"
"github.com/dgraph-io/dgraph/graphql/schema"

"github.com/dgraph-io/dgraph/graphql/web"

"github.com/dgraph-io/dgraph/x"
)

func init() {
http.HandleFunc("/admin/backup", backupHandler)
http.Handle("/admin/backup", allowedMethodsHandler(allowedMethods{http.MethodPost: true},
adminAuthHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
backupHandler(w, r, adminServer)
}))))
}

// backupHandler handles backup requests coming from the HTTP endpoint.
func backupHandler(w http.ResponseWriter, r *http.Request) {
if !handlerInit(w, r, map[string]bool{
http.MethodPost: true,
}) {
return
func backupHandler(w http.ResponseWriter, r *http.Request, adminServer web.IServeGraphQL) {
gqlReq := &schema.Request{
Query: `
mutation backup($input: BackupInput!) {
backup(input: $input) {
response {
code
}
}
}`,
Variables: map[string]interface{}{"input": map[string]interface{}{
"destination": r.FormValue("destination"),
"accessKey": r.FormValue("access_key"),
"secretKey": r.FormValue("secret_key"),
"sessionToken": r.FormValue("session_token"),
"anonymous": r.FormValue("anonymous") == "true",
"forceFull": r.FormValue("force_full") == "true",
}},
}

destination := r.FormValue("destination")
accessKey := r.FormValue("access_key")
secretKey := r.FormValue("secret_key")
sessionToken := r.FormValue("session_token")
anonymous := r.FormValue("anonymous") == "true"
forceFull := r.FormValue("force_full") == "true"

req := pb.BackupRequest{
Destination: destination,
AccessKey: accessKey,
SecretKey: secretKey,
SessionToken: sessionToken,
Anonymous: anonymous,
}

if err := worker.ProcessBackupRequest(context.Background(), &req, forceFull); err != nil {
x.SetStatus(w, err.Error(), "Backup failed.")
resp := resolveWithAdminServer(gqlReq, r, adminServer)
if resp.Errors != nil {
x.SetStatus(w, resp.Errors.Error(), "Backup failed.")
return
}

Expand Down
25 changes: 15 additions & 10 deletions dgraph/cmd/alpha/http.go
Expand Up @@ -586,12 +586,8 @@ func adminSchemaHandler(w http.ResponseWriter, r *http.Request, adminServer web.
return
}

md := metadata.New(nil)
ctx := metadata.NewIncomingContext(context.Background(), md)
ctx = x.AttachAccessJwt(ctx, r)

gqlReq := &schema.Request{}
gqlReq.Query = `
gqlReq := &schema.Request{
Query: `
mutation updateGqlSchema($sch: String!) {
updateGQLSchema(input: {
set: {
Expand All @@ -602,12 +598,11 @@ func adminSchemaHandler(w http.ResponseWriter, r *http.Request, adminServer web.
id
}
}
}`
gqlReq.Variables = map[string]interface{}{
"sch": string(b),
}`,
Variables: map[string]interface{}{"sch": string(b)},
}

response := adminServer.Resolve(ctx, gqlReq)
response := resolveWithAdminServer(gqlReq, r, adminServer)
if len(response.Errors) > 0 {
x.SetStatus(w, x.Error, response.Errors.Error())
return
Expand All @@ -616,6 +611,16 @@ func adminSchemaHandler(w http.ResponseWriter, r *http.Request, adminServer web.
writeSuccessResponse(w, r)
}

func resolveWithAdminServer(gqlReq *schema.Request, r *http.Request,
adminServer web.IServeGraphQL) *schema.Response {
md := metadata.New(nil)
ctx := metadata.NewIncomingContext(context.Background(), md)
ctx = x.AttachAccessJwt(ctx, r)
ctx = x.AttachRemoteIP(ctx, r)

return adminServer.Resolve(ctx, gqlReq)
}

func writeSuccessResponse(w http.ResponseWriter, r *http.Request) {
res := map[string]interface{}{}
data := map[string]interface{}{}
Expand Down

0 comments on commit 8992238

Please sign in to comment.