Improve database authentication

Improve the database authentication implementation for namespaces, databases, and scopes.
This commit is contained in:
Tobie Morgan Hitchcock 2016-11-21 18:48:25 +00:00
parent d5604f589c
commit 94c9631d91
8 changed files with 462 additions and 60 deletions

View file

@ -16,6 +16,19 @@ package cnf
var Settings *Options
type Auth struct {
Kind int
Data interface{}
Possible struct {
NS string
DB string
}
Selected struct {
NS string
DB string
}
}
// Options defines global configuration options
type Options struct {
DB struct {

View file

@ -103,6 +103,12 @@ func Execute(ctx *fibre.Context, txt interface{}, vars map[string]interface{}) (
return
}
// Ensure that the current authentication data
// is made available as a runtime variable to
// the query layer.
vars["auth"] = ctx.Get("auth").(*cnf.Auth).Data
return Process(ctx, ast, vars)
}

View file

@ -34,26 +34,22 @@ const (
// options represents context runtime config.
type options struct {
kind int
auth map[string]string
conf map[string]string
auth *cnf.Auth
}
func newOptions(c *fibre.Context) *options {
return &options{
kind: c.Get("kind").(int),
auth: c.Get("auth").(map[string]string),
conf: c.Get("conf").(map[string]string),
auth: c.Get("auth").(*cnf.Auth),
}
}
func (o *options) get(kind int) (kv, ns, db string, err error) {
kv = cnf.Settings.DB.Base
ns = o.conf["NS"]
db = o.conf["DB"]
ns = o.auth.Selected.NS
db = o.auth.Selected.DB
if o.kind > kind {
if kind < o.auth.Kind {
err = &QueryError{}
return
}
@ -81,7 +77,7 @@ func (o *options) ns(ns string) (err error) {
// that it is remembered across requests on
// any persistent connections.
o.conf["NS"] = ns
o.auth.Selected.NS = ns
return
@ -101,7 +97,7 @@ func (o *options) db(db string) (err error) {
// that it is remembered across requests on
// any persistent connections.
o.conf["DB"] = db
o.auth.Selected.DB = db
return

View file

@ -18,10 +18,13 @@ import (
"fmt"
"bytes"
"strings"
"encoding/base64"
"github.com/abcum/fibre"
"github.com/abcum/surreal/cnf"
"github.com/abcum/surreal/mem"
"github.com/abcum/surreal/sql"
"github.com/dgrijalva/jwt-go"
@ -29,26 +32,41 @@ import (
func auth() fibre.MiddlewareFunc {
return func(h fibre.HandlerFunc) fibre.HandlerFunc {
return func(c *fibre.Context) error {
return func(c *fibre.Context) (err error) {
auth := map[string]string{"NS": "", "DB": ""}
defer func() {
if r := recover(); r != nil {
err = fibre.NewHTTPError(403)
}
}()
auth := &cnf.Auth{}
c.Set("auth", auth)
conf := map[string]string{"NS": "", "DB": ""}
c.Set("conf", conf)
// Start off with an authentication level
// which prevents running any sql queries,
// and denies access to all data.
c.Set("kind", sql.AuthNO)
auth.Kind = sql.AuthNO
// Check whether there is an Authorization
// header present, and if there is check
// whether it is a Basic Auth header.
// Retrieve the current domain host and
// if we are using a subdomain then set
// the NS and DB to the subdomain bits.
bits := strings.Split(c.Request().URL().Host, ".")
subs := strings.Split(bits[0], "-")
if len(subs) == 2 {
auth.Kind = sql.AuthSC
auth.Possible.NS = subs[0]
auth.Selected.NS = subs[0]
auth.Possible.DB = subs[1]
auth.Selected.DB = subs[1]
}
// Retrieve the HTTP Authorization header
// from the request, and continue.
// from the request, so that we can detect
// whether it is Basic auth or Bearer auth.
head := c.Request().Header().Get("Authorization")
@ -72,11 +90,11 @@ func auth() fibre.MiddlewareFunc {
// Root authentication
// ------------------------------
c.Set("kind", sql.AuthKV)
auth["NS"] = "*" // Anything allowed
conf["NS"] = "" // Must specify
auth["DB"] = "*" // Anything allowed
conf["DB"] = "" // Must specify
auth.Kind = sql.AuthKV
auth.Possible.NS = "*"
auth.Selected.NS = ""
auth.Possible.DB = "*"
auth.Selected.DB = ""
return h(c)
@ -92,44 +110,107 @@ func auth() fibre.MiddlewareFunc {
if head != "" && head[:6] == "Bearer" {
var vars jwt.MapClaims
var nok, dok, sok, tok, uok bool
var nsv, dbv, scv, tkv, usv string
token, err := jwt.Parse(head[7:], func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
vars = token.Claims.(jwt.MapClaims)
if err := vars.Valid(); err != nil {
return nil, err
}
return []byte(cnf.Settings.Auth.Token), nil
nsv, nok = vars["NS"].(string) // Namespace
dbv, dok = vars["DB"].(string) // Database
scv, sok = vars["SC"].(string) // Scope
tkv, tok = vars["TK"].(string) // Token
usv, uok = vars["US"].(string) // Login
if tkv == "default" {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method")
}
}
if nok && dok && sok && tok {
if tkv != "default" {
key := mem.GetNS(nsv).GetDB(dbv).GetSC(scv).GetTK(tkv)
if token.Header["alg"] != key.Type {
return nil, fmt.Errorf("Unexpected signing method")
}
auth.Kind = sql.AuthSC
return []byte(key.Text), nil
} else {
scp := mem.GetNS(nsv).GetDB(dbv).GetSC(scv)
auth.Kind = sql.AuthSC
return []byte(scp.Uniq), nil
}
} else if nok && dok && tok {
if tkv != "default" {
key := mem.GetNS(nsv).GetDB(dbv).GetTK(tkv)
if token.Header["alg"] != key.Type {
return nil, fmt.Errorf("Unexpected signing method")
}
auth.Kind = sql.AuthDB
return []byte(key.Text), nil
} else if uok {
usr := mem.GetNS(nsv).GetDB(dbv).GetAC(usv)
auth.Kind = sql.AuthDB
return []byte(usr.Uniq), nil
}
} else if nok && tok {
if tkv != "default" {
key := mem.GetNS(nsv).GetTK(tkv)
if token.Header["alg"] != key.Type {
return nil, fmt.Errorf("Unexpected signing method")
}
auth.Kind = sql.AuthNS
return []byte(key.Text), nil
} else if uok {
usr := mem.GetNS(nsv).GetAC(usv)
auth.Kind = sql.AuthNS
return []byte(usr.Uniq), nil
}
}
return nil, fmt.Errorf("No available token")
})
if err == nil && token.Valid {
// ------------------------------
// Namespace authentication
// ------------------------------
if auth.Kind == sql.AuthNS {
auth.Possible.NS = nsv
auth.Selected.NS = nsv
auth.Possible.DB = "*"
auth.Selected.DB = ""
}
// c.Set("kind", sql.AuthNS)
// auth["NS"] = "SPECIFIED" // Not allowed to change
// conf["NS"] = "SPECIFIED" // Not allowed to change
// auth["DB"] = "*" // Anything allowed
// conf["DB"] = "" // Must specify
if auth.Kind == sql.AuthDB {
auth.Possible.NS = nsv
auth.Selected.NS = nsv
auth.Possible.DB = dbv
auth.Selected.DB = dbv
}
// ------------------------------
// Database authentication
// ------------------------------
if auth.Kind == sql.AuthSC {
auth.Possible.NS = nsv
auth.Selected.NS = nsv
auth.Possible.DB = dbv
auth.Selected.DB = dbv
}
// c.Set("kind", sql.AuthDB)
// auth["NS"] = "SPECIFIED" // Not allowed to change
// conf["NS"] = "SPECIFIED" // Not allowed to change
// auth["DB"] = "SPECIFIED" // Not allowed to change
// conf["DB"] = "SPECIFIED" // Not allowed to change
// ------------------------------
// Scoped authentication
// ------------------------------
// c.Set("kind", sql.AuthTB)
// auth["NS"] = "SPECIFIED" // Not allowed to change
// conf["NS"] = "SPECIFIED" // Not allowed to change
// auth["DB"] = "SPECIFIED" // Not allowed to change
// conf["DB"] = "SPECIFIED" // Not allowed to change
if val, ok := vars["auth"]; ok {
auth.Data = val
}
return h(c)
@ -137,11 +218,7 @@ func auth() fibre.MiddlewareFunc {
}
if c.Request().Header().Get("Upgrade") == "websocket" {
return h(c)
}
return fibre.NewHTTPError(401)
}
}

View file

@ -36,6 +36,12 @@ func errors(val error, c *fibre.Context) {
code, info = 409, e.Error()
case *kvs.CKError:
code, info = 403, e.Error()
case *sql.NSError:
code, info = 403, e.Error()
case *sql.DBError:
code, info = 403, e.Error()
case *sql.QueryError:
code, info = 401, e.Error()
case *sql.ParseError:
code, info = 400, e.Error()
case *fibre.HTTPError:

View file

@ -65,6 +65,30 @@ func routes(s *fibre.Fibre) {
s.Rpc("/rpc", &rpc{})
// --------------------------------------------------
// Endpoints for authentication signup
// --------------------------------------------------
s.Options("/signup", func(c *fibre.Context) error {
return c.Code(200)
})
s.Post("/signup", func(c *fibre.Context) error {
return signup(c)
})
// --------------------------------------------------
// Endpoints for authentication signin
// --------------------------------------------------
s.Options("/signin", func(c *fibre.Context) error {
return c.Code(200)
})
s.Post("/signin", func(c *fibre.Context) error {
return signin(c)
})
// --------------------------------------------------
// Endpoints for submitting sql queries
// --------------------------------------------------

208
web/signin.go Normal file
View file

@ -0,0 +1,208 @@
// Copyright © 2016 Abcum Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package web
import (
"time"
"github.com/abcum/fibre"
"github.com/abcum/surreal/db"
"github.com/abcum/surreal/mem"
"github.com/abcum/surreal/sql"
"github.com/dgrijalva/jwt-go"
"golang.org/x/crypto/bcrypt"
)
func signin(c *fibre.Context) (err error) {
defer func() {
if r := recover(); r != nil {
err = fibre.NewHTTPError(403)
}
}()
var vars map[string]interface{}
c.Bind(&vars)
n, nok := vars["NS"].(string)
d, dok := vars["DB"].(string)
s, sok := vars["SC"].(string)
// If we have a namespace, database, and
// scope defined, then we are logging in
// to the scope level.
if nok && len(n) > 0 && dok && len(d) > 0 && sok && len(s) > 0 {
var str string
var scp *mem.SC
var res []*db.Response
// Get the specified signin scope.
if scp = mem.GetNS(n).GetDB(d).GetSC(s); scp == nil {
return fibre.NewHTTPError(403)
}
// Process the scope signin statement.
res, err = db.Process(c, &sql.Query{[]sql.Statement{scp.Signin}}, vars)
if err != nil {
return fibre.NewHTTPError(403)
}
if len(res) != 1 && len(res[0].Result) != 1 {
return fibre.NewHTTPError(403)
}
// Create a new token signer with the default claims.
signr := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
"NS": n,
"DB": d,
"SC": s,
"TK": "default",
"iss": "Surreal",
"iat": time.Now().Unix(),
"nbf": time.Now().Unix(),
"exp": time.Now().Add(scp.Time).Unix(),
"auth": res[0].Result[0],
})
// Try to create the final signed token as a string.
str, err = signr.SignedString([]byte(scp.Uniq))
if err != nil {
return fibre.NewHTTPError(403)
}
return c.Text(200, str)
}
// If we have a namespace, database, but
// no scope defined, then we are logging
// in to the database level.
if nok && len(n) > 0 && dok && len(d) > 0 {
var str string
var usr *mem.AC
// Get the specified user and password.
u, uok := vars["user"].(string)
p, pok := vars["pass"].(string)
if !uok || len(u) == 0 || !pok || len(p) == 0 {
return fibre.NewHTTPError(403)
}
// Get the specified database login.
if usr = mem.GetNS(n).GetDB(d).GetAC(u); usr == nil {
return fibre.NewHTTPError(403)
}
// Compare the hashed and stored passwords.
err = bcrypt.CompareHashAndPassword([]byte(usr.Pass), []byte(p))
if err != nil {
return fibre.NewHTTPError(403)
}
// Create a new token signer with the default claims.
signr := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
"US": u,
"NS": n,
"DB": d,
"TK": "default",
"iss": "Surreal",
"iat": time.Now().Unix(),
"nbf": time.Now().Unix(),
"exp": time.Now().Add(10 * time.Minute).Unix(),
})
// Try to create the final signed token as a string.
str, err = signr.SignedString([]byte(usr.Uniq))
if err != nil {
return fibre.NewHTTPError(403)
}
return c.Text(200, str)
}
// If we have a namespace, but no database,
// or scope defined, then we are logging
// in to the namespace level.
if nok && len(n) > 0 {
var str string
var usr *mem.AC
// Get the specified user and password.
u, uok := vars["user"].(string)
p, pok := vars["pass"].(string)
if !uok || len(u) == 0 || !pok || len(p) == 0 {
return fibre.NewHTTPError(403)
}
// Get the specified namespace login.
if usr = mem.GetNS(n).GetAC(u); usr == nil {
return fibre.NewHTTPError(403)
}
// Compare the hashed and stored passwords.
err = bcrypt.CompareHashAndPassword([]byte(usr.Pass), []byte(p))
if err != nil {
return fibre.NewHTTPError(403)
}
// Create a new token signer with the default claims.
signr := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
"US": u,
"NS": n,
"TK": "default",
"iss": "Surreal",
"iat": time.Now().Unix(),
"nbf": time.Now().Unix(),
"exp": time.Now().Add(10 * time.Minute).Unix(),
})
// Try to create the final signed token as a string.
str, err = signr.SignedString([]byte(usr.Uniq))
if err != nil {
return fibre.NewHTTPError(403)
}
return c.Text(200, str)
}
return fibre.NewHTTPError(403)
}

72
web/signup.go Normal file
View file

@ -0,0 +1,72 @@
// Copyright © 2016 Abcum Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package web
import (
"github.com/abcum/fibre"
"github.com/abcum/surreal/db"
"github.com/abcum/surreal/mem"
"github.com/abcum/surreal/sql"
)
func signup(c *fibre.Context) (err error) {
defer func() {
if r := recover(); r != nil {
err = fibre.NewHTTPError(403)
}
}()
var vars map[string]interface{}
c.Bind(&vars)
n, nok := vars["NS"].(string)
d, dok := vars["DB"].(string)
s, sok := vars["SC"].(string)
// If we have a namespace, database, and
// scope defined, then we are logging in
// to the scope level.
if nok && len(n) > 0 && dok && len(d) > 0 && sok && len(s) > 0 {
var scp *mem.SC
var res []*db.Response
// Get the specified signin scope.
if scp = mem.GetNS(n).GetDB(d).GetSC(s); scp == nil {
return fibre.NewHTTPError(403)
}
// Process the scope signup statement.
res, err = db.Process(c, &sql.Query{[]sql.Statement{scp.Signup}}, vars)
if err != nil {
return fibre.NewHTTPError(403)
}
if len(res) != 1 && len(res[0].Result) != 1 {
return fibre.NewHTTPError(403)
}
return c.Code(200)
}
return fibre.NewHTTPError(401)
}