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

Add authn for graphql and http admin endpoints #5162

Merged
merged 30 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
91fc1ca
add auth for admin endpoints
abhimanyusinghgaur Apr 8, 2020
c78becc
add poor man's auth
abhimanyusinghgaur Apr 10, 2020
563ecde
add tests
abhimanyusinghgaur Apr 15, 2020
1dc12d1
update auth for /alter and /admin/schema
abhimanyusinghgaur Apr 15, 2020
7006028
remove whitelisting check for /alter
abhimanyusinghgaur Apr 15, 2020
603fd25
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur Apr 17, 2020
5629249
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur Apr 17, 2020
deeb2bb
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur Apr 17, 2020
08772df
add middlewares
abhimanyusinghgaur Apr 19, 2020
4009e29
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur Apr 19, 2020
9ea03fd
fix test
abhimanyusinghgaur Apr 19, 2020
262b944
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur Apr 22, 2020
9af6def
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur Apr 27, 2020
1b64682
incomplete changes
abhimanyusinghgaur Apr 28, 2020
46330f9
final changes
abhimanyusinghgaur Apr 28, 2020
3497cbb
add test
abhimanyusinghgaur Apr 28, 2020
fa2b651
fix tests
abhimanyusinghgaur Apr 29, 2020
1a471ba
add more tests
abhimanyusinghgaur Apr 29, 2020
0b92b7f
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur Apr 29, 2020
076e68b
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur May 13, 2020
636999f
manage middlewares as configuration
abhimanyusinghgaur May 14, 2020
0d75284
route admin http requests through graphql admin server
abhimanyusinghgaur May 15, 2020
75a4d8b
add test
abhimanyusinghgaur May 15, 2020
b6e6863
restructure tests
abhimanyusinghgaur May 15, 2020
4f513fb
final touches
abhimanyusinghgaur May 15, 2020
ed81244
fix tests
abhimanyusinghgaur May 15, 2020
1d80239
fix test
abhimanyusinghgaur May 15, 2020
3677c15
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur May 15, 2020
aa4e3cc
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur May 18, 2020
8fe2324
Merge branch 'master' of github.com:dgraph-io/dgraph into abhimanyu/a…
abhimanyusinghgaur May 19, 2020
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
186 changes: 115 additions & 71 deletions dgraph/cmd/alpha/admin.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading