Merge pull request #266 from sosedoff/connect-backend

Add ability to connect with settings from third-party backend
This commit is contained in:
Dan Sosedoff
2017-09-21 20:37:46 -05:00
committed by GitHub
17 changed files with 275 additions and 1316 deletions

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/tuvistavie/securerandom"
"github.com/sosedoff/pgweb/pkg/bookmarks"
"github.com/sosedoff/pgweb/pkg/client"
@@ -69,6 +70,51 @@ func GetSessions(c *gin.Context) {
c.JSON(200, map[string]int{"sessions": len(DbSessions)})
}
func ConnectWithBackend(c *gin.Context) {
// Setup a new backend client
backend := Backend{
Endpoint: command.Opts.ConnectBackend,
Token: command.Opts.ConnectToken,
PassHeaders: command.Opts.ConnectHeaders,
}
// Fetch connection credentials
cred, err := backend.FetchCredential(c.Param("resource"), c)
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
// Make the new session
sessionId, err := securerandom.Uuid()
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
c.Request.Header.Add("x-session-id", sessionId)
// Connect to the database
cl, err := client.NewFromUrl(cred.DatabaseUrl, nil)
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
cl.External = true
// Finalize session seetup
_, err = cl.Info()
if err == nil {
err = setClient(c, cl)
}
if err != nil {
cl.Close()
c.JSON(400, Error{err.Error()})
return
}
c.Redirect(301, fmt.Sprintf("/%s?session=%s", command.Opts.Prefix, sessionId))
}
func Connect(c *gin.Context) {
if command.Opts.LockSession {
c.JSON(400, Error{"Session is locked"})
@@ -141,6 +187,12 @@ func SwitchDb(c *gin.Context) {
return
}
// Do not allow switching databases for connections from third-party backends
if conn.External {
c.JSON(400, Error{"Session is locked"})
return
}
currentUrl, err := neturl.Parse(conn.ConnectionString)
if err != nil {
c.JSON(400, Error{"Unable to parse current connection string"})
@@ -199,6 +251,12 @@ func Disconnect(c *gin.Context) {
}
func GetDatabases(c *gin.Context) {
conn := DB(c)
if conn.External {
c.JSON(403, Error{"Not permitted"})
return
}
names, err := DB(c).Databases()
serveResult(names, err, c)
}

75
pkg/api/backend.go Normal file
View File

@@ -0,0 +1,75 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
type Backend struct {
Endpoint string
Token string
PassHeaders string
}
type BackendRequest struct {
Resource string `json:"resource"`
Token string `json:"token"`
Headers map[string]string `json:"headers"`
}
type BackendCredential struct {
DatabaseUrl string `json:"database_url"`
}
func (be Backend) FetchCredential(resource string, c *gin.Context) (*BackendCredential, error) {
request := BackendRequest{
Resource: resource,
Token: be.Token,
Headers: map[string]string{},
}
for _, name := range strings.Split(be.PassHeaders, ",") {
request.Headers[strings.ToLower(name)] = c.Request.Header.Get(name)
}
body, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := http.Post(be.Endpoint, "application/json", bytes.NewReader(body))
if err != nil {
// Any connection-related issues will show up in the server log
log.Println("Unable to fetch backend credential:", err)
// We dont want to expose the url of the backend here, so reply with generic error
return nil, fmt.Errorf("Unable to connect to the auth backend")
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("Got HTTP error %v from backend", resp.StatusCode)
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
cred := &BackendCredential{}
if err := json.Unmarshal(respBody, cred); err != nil {
return nil, err
}
if cred.DatabaseUrl == "" {
return nil, fmt.Errorf("Database url was not provided")
}
return cred, nil
}

View File

@@ -49,4 +49,6 @@ func SetupRoutes(router *gin.Engine) {
api.GET("/bookmarks", GetBookmarks)
api.GET("/export", DataExport)
}
group.GET("/connect/:resource", ConnectWithBackend)
}

View File

@@ -22,6 +22,7 @@ type Client struct {
tunnel *Tunnel
serverVersion string
lastQueryTime time.Time
External bool
History []history.Record `json:"history"`
ConnectionString string `json:"connection_string"`
}

View File

@@ -1,6 +1,7 @@
package command
import (
"errors"
"os"
"strings"
@@ -29,6 +30,9 @@ type Options struct {
Bookmark string `short:"b" long:"bookmark" description:"Bookmark to use for connection. Bookmark files are stored under $HOME/.pgweb/bookmarks/*.toml" default:""`
BookmarksDir string `long:"bookmarks-dir" description:"Overrides default directory for bookmark files to search" default:""`
DisablePrettyJson bool `long:"no-pretty-json" description:"Disable JSON formatting feature for result export" default:"false"`
ConnectBackend string `long:"connect-backend" description:"Enable database authentication through a third party backend"`
ConnectToken string `long:"connect-token" description:"Authentication token for the third-party connect backend"`
ConnectHeaders string `long:"connect-headers" description:"List of headers to pass to the connect backend"`
}
var Opts Options
@@ -64,5 +68,18 @@ func ParseOptions() error {
Opts.AuthPass = os.Getenv("AUTH_PASS")
}
if Opts.ConnectBackend != "" {
if !Opts.Sessions {
return errors.New("--sessions flag must be set")
}
if Opts.ConnectToken == "" {
return errors.New("--connect-token flag must be set")
}
} else {
if Opts.ConnectToken != "" || Opts.ConnectHeaders != "" {
return errors.New("--connect-backend flag must be set")
}
}
return nil
}

File diff suppressed because one or more lines are too long