From 94c9631d9184fc298b57de1ccd09bfda7077909c Mon Sep 17 00:00:00 2001 From: Tobie Morgan Hitchcock Date: Mon, 21 Nov 2016 18:48:25 +0000 Subject: [PATCH] Improve database authentication Improve the database authentication implementation for namespaces, databases, and scopes. --- cnf/cnf.go | 13 ++++ db/db.go | 6 ++ sql/options.go | 18 ++--- web/auth.go | 175 +++++++++++++++++++++++++++++------------ web/err.go | 6 ++ web/routes.go | 24 ++++++ web/signin.go | 208 +++++++++++++++++++++++++++++++++++++++++++++++++ web/signup.go | 72 +++++++++++++++++ 8 files changed, 462 insertions(+), 60 deletions(-) create mode 100644 web/signin.go create mode 100644 web/signup.go diff --git a/cnf/cnf.go b/cnf/cnf.go index 2c60d344..5d83e134 100644 --- a/cnf/cnf.go +++ b/cnf/cnf.go @@ -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 { diff --git a/db/db.go b/db/db.go index 85e6cce4..b1b4bc6e 100644 --- a/db/db.go +++ b/db/db.go @@ -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) } diff --git a/sql/options.go b/sql/options.go index 63e06ee1..d87fe141 100644 --- a/sql/options.go +++ b/sql/options.go @@ -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 diff --git a/web/auth.go b/web/auth.go index c05c3b7f..31fec651 100644 --- a/web/auth.go +++ b/web/auth.go @@ -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) + return h(c) } } diff --git a/web/err.go b/web/err.go index 8bad3fa4..96e6d621 100644 --- a/web/err.go +++ b/web/err.go @@ -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: diff --git a/web/routes.go b/web/routes.go index 2b21a6dc..ec26b99e 100644 --- a/web/routes.go +++ b/web/routes.go @@ -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 // -------------------------------------------------- diff --git a/web/signin.go b/web/signin.go new file mode 100644 index 00000000..9b7ec977 --- /dev/null +++ b/web/signin.go @@ -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) + +} diff --git a/web/signup.go b/web/signup.go new file mode 100644 index 00000000..14c59915 --- /dev/null +++ b/web/signup.go @@ -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) + +}