diff --git a/Makefile b/Makefile index e4f51d9..d1f338b 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ format: go fix ./... test: - go test -v $(GONODE_PLUGINS) ./test/api ./core ./core/config ./commands/server + go test $(GONODE_PLUGINS) ./test/api ./core ./core/config ./commands/server go vet ./... #cd explorer && npm test diff --git a/README.md b/README.md index 797d7b1..27f6637 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ A prototype to store dynamic node inside a PostgreSQL database with the JSONb st Documentation ------------- - - - [Contributing](docs/contributing.md) - - [Node](docs/node.md) - - [Vault](docs/vault.md) \ No newline at end of file + + * [Node](docs/node.md) + * [Plugins](docs/plugins) + * [Vault](docs/plugins/vault.md) + * [Guard](docs/plugins/guard.md) + * [Contributing](docs/contributing.md) \ No newline at end of file diff --git a/commands/server/cli.go b/commands/server/cli.go index f8992eb..29e81c4 100644 --- a/commands/server/cli.go +++ b/commands/server/cli.go @@ -14,6 +14,7 @@ import ( "github.com/rande/gonode/core/config" "github.com/rande/gonode/plugins/api" + "github.com/rande/gonode/plugins/guard" "github.com/rande/gonode/plugins/setup" "github.com/zenazn/goji/bind" "github.com/zenazn/goji/graceful" @@ -54,9 +55,11 @@ func (c *ServerCommand) Run(args []string) int { l := goapp.NewLifecycle() ConfigureServer(l, conf) + // add plugins setup.ConfigureServer(l, conf) api.ConfigureServer(l, conf) + guard.ConfigureServer(l, conf) l.Run(func(app *goapp.App, state *goapp.GoroutineState) error { mux := app.Get("goji.mux").(*web.Mux) diff --git a/core/config/config_server.go b/core/config/config_server.go index 8922629..604b47c 100644 --- a/core/config/config_server.go +++ b/core/config/config_server.go @@ -5,8 +5,17 @@ package config -type ServerAuth struct { +type ServerGuard struct { Key string `toml:"key"` + Jwt struct { + Validity int64 `toml:"validity"` + Login struct { + Path string `toml:"path"` + } `toml:"login"` + Token struct { + Path string `toml:"path"` + } `toml:"token"` + } `toml:"jwt"` } type ServerDatabase struct { @@ -33,7 +42,7 @@ type ServerConfig struct { Filesystem ServerFilesystem `toml:"filesystem"` Test bool `toml:"test"` Bind string `toml:"bind"` - Auth ServerAuth `toml:"auth"` + Guard ServerGuard `toml:"guard"` } func NewServerConfig() *ServerConfig { diff --git a/core/config/config_test.go b/core/config/config_test.go index 557e03e..bf5e37d 100644 --- a/core/config/config_test.go +++ b/core/config/config_test.go @@ -6,6 +6,8 @@ package config import ( + "bytes" + "github.com/BurntSushi/toml" "github.com/stretchr/testify/assert" "os" "testing" @@ -33,4 +35,15 @@ func Test_Server_LoadConfiguration(t *testing.T) { assert.Equal(t, config.Databases["master"].Prefix, "test") assert.Equal(t, config.Filesystem.Type, "") // not used for now assert.Equal(t, config.Filesystem.Path, "/tmp/gnode") + + assert.Equal(t, config.Guard.Jwt.Login.Path, "/login") + assert.Equal(t, config.Guard.Jwt.Token.Path, `^\/nodes\/(.*)$`) + + config.Guard.Jwt.Login.Path = `^\/nodes\/(.*)$` + + w := bytes.NewBufferString("") + e := toml.NewEncoder(w) + + e.Encode(config) + } diff --git a/docs/plugins/guard.md b/docs/plugins/guard.md new file mode 100644 index 0000000..69321c6 --- /dev/null +++ b/docs/plugins/guard.md @@ -0,0 +1,91 @@ +Guard +===== + +Introduction +------------ + +Guard plugin handle request authentification. The plugins comes with a dedicated middleware and request authenticators. +For now there is only 2 authenticators implemented: + - ``JwtLoginGuardAuthenticator``: create a valid Json Web Token from the posted ``username`` and ``password``. + - ``JwtTokenGuardAuthenticator``: validate a Json Web Token. + +Configuration +------------- + + + ```toml + [guard] + key = "ZeSecretKey0oo" + + [guard.jwt] + [guard.jwt.login] + path = "/login" + + [guard.jwt.token] + path = "^\\/nodes\\/(.*)$" + + +- ``key`` is private and it is used to sign the JWT with a symetric algorythm. +- ``guard.jwt.login.path`` is used to configure the login entry point, ie where the ``JwtLoginGuardAuthenticator`` will accept the request. +- ``guard.jwt.token.path`` is used to configure paths requiring to have authentification handled by the ``JwtTokenGuardAuthenticator`` service. + + +Authenticators +-------------- + +### JwtLoginGuardAuthenticator + + +The service will use the ``core.user`` node type to find the user by her/his username. The query looks like: ``type = 'core.user' AND data->>'username' = ?`` + +The authentification request should be a POST + +```HTTP +POST /login HTTP/1.1 +Content-Type: application/x-www-form-urlencoded + +username=admin&password=secret +``` + +If the response is valid, the response will be: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "status": "OK", + "message": "Request is authenticated", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NTA0Nzg1NzQsInJscyI6bnVsbCwidXNyIjoicmFuZGUifQ.E_BMRg2UWO7jVw1CGgn7WhhwbATCHjYYcausZZ7LSZA", +} + +``` + +If the response is not valid, the response will be + +```HTTP +HTTP/1.1 403 Forbidden +Content-Type: application/json + +{ + "status": "KO", + "message": "Unable to authenticate request" +} +``` + +### JwtTokenGuardAuthenticator + +The service will use the ``core.user`` node type to find the user by her/his username. The query looks like: ``type = 'core.user' AND data->>'username' = ?`` + +The authentification request should be on any http method, either using the ``Authorization`` header or the ``access_token`` parameter. + +```HTTP +GET /nodes HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NTA0Nzg1NzQsInJscyI6bnVsbCwidXNyIjoicmFuZGUifQ.E_BMRg2UWO7jVw1CGgn7WhhwbATCHjYYcausZZ7LSZA +``` + +or + +```HTTP +GET /nodes?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NTA0Nzg1NzQsInJscyI6bnVsbCwidXNyIjoicmFuZGUifQ.E_BMRg2UWO7jVw1CGgn7WhhwbATCHjYYcausZZ7LSZA HTTP/1.1 +``` diff --git a/docs/vault.md b/docs/plugins/vault.md similarity index 100% rename from docs/vault.md rename to docs/plugins/vault.md diff --git a/plugins/api/api_http.go b/plugins/api/api_http.go index e8d537f..a90ccce 100644 --- a/plugins/api/api_http.go +++ b/plugins/api/api_http.go @@ -186,7 +186,7 @@ func ConfigureServer(l *goapp.Lifecycle, conf *config.ServerConfig) { // Set some claims token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix() // Sign and get the complete encoded token as a string - tokenString, err := token.SignedString([]byte(conf.Auth.Key)) + tokenString, err := token.SignedString([]byte(conf.Guard.Key)) if err != nil { helper.SendWithHttpCode(res, http.StatusInternalServerError, "Unable to sign the token") diff --git a/plugins/guard/guard.go b/plugins/guard/guard.go new file mode 100644 index 0000000..24bdf57 --- /dev/null +++ b/plugins/guard/guard.go @@ -0,0 +1,96 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package guard + +import ( + "errors" + "net/http" +) + +var ( + InvalidCredentialsFormat = errors.New("Invalid credentials format") + InvalidCredentials = errors.New("Invalid credentials") + UnableRetrieveUser = errors.New("Unable to retrieve the user") + CredentialMismatch = errors.New("Credential mismatch") + AuthenticatedTokenCreationError = errors.New("Unable to create authentication token") +) + +// Bare interface with the default requirement to check username and password +type GuardUser interface { + GetUsername() string + GetPassword() string + GetRoles() []string +} + +type DefaultGuardUser struct { + Username string + Password string + Roles []string +} + +func (u *DefaultGuardUser) GetUsername() string { + return u.Username +} + +func (u *DefaultGuardUser) GetPassword() string { + return u.Password +} + +func (u *DefaultGuardUser) GetRoles() []string { + return u.Roles +} + +// Bare interface to used inside a request lifecycle +type GuardToken interface { + // return the current username for the current token + GetUsername() string + + // return the related roles linked to the current token + GetRoles() []string +} + +// Default implementation to the GuardToken +type DefaultGuardToken struct { + Username string + Roles []string +} + +func (t *DefaultGuardToken) GetUsername() string { + return t.Username +} + +func (t *DefaultGuardToken) GetRoles() []string { + return t.Roles +} + +type GuardAuthenticator interface { + // This method is call on each request. + // If the method return nil as interface{} value, it means the authenticator + // cannot handle the request + getCredentials(req *http.Request) (interface{}, error) + + // Return the user from the credentials + getUser(credentials interface{}) (GuardUser, error) + + // Check if the provided credentials are valid for the current user + checkCredentials(credentials interface{}, user GuardUser) error + + // Return a security token related to the user + createAuthenticatedToken(u GuardUser) (GuardToken, error) + + // Action when the authentication fail. + // On a default form login, it can be used to redirect the user to login page + // return true if the workflows must be stopped (ie, the authenticator was written + // bytes on the response. false if not. + onAuthenticationFailure(req *http.Request, res http.ResponseWriter, err error) bool + + // Action when the authentication success + // On a default form login, it can be used to redirect the user to protected page + // or the homepage + // return true if the workflows must be stopped (ie, the authenticator was written + // bytes on the response. false if not. + onAuthenticationSuccess(req *http.Request, res http.ResponseWriter, token GuardToken) bool +} diff --git a/plugins/guard/guard_app.go b/plugins/guard/guard_app.go new file mode 100644 index 0000000..79e426e --- /dev/null +++ b/plugins/guard/guard_app.go @@ -0,0 +1,42 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package guard + +import ( + "github.com/rande/goapp" + "github.com/rande/gonode/core" + "github.com/rande/gonode/core/config" + "github.com/zenazn/goji/web" + "regexp" +) + +func ConfigureServer(l *goapp.Lifecycle, conf *config.ServerConfig) { + + l.Prepare(func(app *goapp.App) error { + mux := app.Get("goji.mux").(*web.Mux) + conf := app.Get("gonode.configuration").(*config.ServerConfig) + manager := app.Get("gonode.manager").(*core.PgNodeManager) + + auths := []GuardAuthenticator{ + &JwtTokenGuardAuthenticator{ + Path: regexp.MustCompile(conf.Guard.Jwt.Token.Path), + Key: []byte(conf.Guard.Key), + Validity: conf.Guard.Jwt.Validity, + NodeManager: manager, + }, + &JwtLoginGuardAuthenticator{ + LoginPath: conf.Guard.Jwt.Login.Path, + Key: []byte(conf.Guard.Key), + Validity: conf.Guard.Jwt.Validity, + NodeManager: manager, + }, + } + + mux.Use(GetGuardMiddleware(auths)) + + return nil + }) +} diff --git a/plugins/guard/guard_jwt_login.go b/plugins/guard/guard_jwt_login.go new file mode 100644 index 0000000..365bb05 --- /dev/null +++ b/plugins/guard/guard_jwt_login.go @@ -0,0 +1,122 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package guard + +import ( + "encoding/json" + "github.com/dgrijalva/jwt-go" + "github.com/gorilla/schema" + "github.com/rande/gonode/core" + "github.com/rande/gonode/plugins/user" + "golang.org/x/crypto/bcrypt" + "net/http" + "time" +) + +// this authenticator will create a JWT Token from a standard form +type JwtLoginGuardAuthenticator struct { + LoginPath string + NodeManager core.NodeManager + Validity int64 + Key []byte +} + +func (a *JwtLoginGuardAuthenticator) getCredentials(req *http.Request) (interface{}, error) { + if !(req.Method == "POST" && req.URL.Path == a.LoginPath) { + return nil, nil + } + + req.ParseForm() + + loginForm := &struct { + Username string `schema:"username"` + Password string `schema:"password"` + }{} + + decoder := schema.NewDecoder() + if err := decoder.Decode(loginForm, req.Form); err != nil { + return nil, err + } + + return &struct{ Username, Password string }{loginForm.Username, loginForm.Password}, nil +} + +func (a *JwtLoginGuardAuthenticator) getUser(credentials interface{}) (GuardUser, error) { + c := credentials.(*struct{ Username, Password string }) + + query := a.NodeManager. + SelectBuilder(). + Where("type = 'core.user' AND data->>'username' = ?", c.Username) + + if node := a.NodeManager.FindOneBy(query); node != nil { + return node.Data.(*user.User), nil + } + + return nil, UnableRetrieveUser +} + +func (a *JwtLoginGuardAuthenticator) checkCredentials(credentials interface{}, user GuardUser) error { + c := credentials.(*struct{ Username, Password string }) + + if err := bcrypt.CompareHashAndPassword([]byte(user.GetPassword()), []byte(c.Password)); err != nil { // equal + return InvalidCredentials + } + + return nil +} + +func (a *JwtLoginGuardAuthenticator) createAuthenticatedToken(user GuardUser) (GuardToken, error) { + return &DefaultGuardToken{ + Username: user.GetUsername(), + Roles: user.GetRoles(), + }, nil +} + +func (a *JwtLoginGuardAuthenticator) onAuthenticationFailure(req *http.Request, res http.ResponseWriter, err error) bool { + // nothing to do + res.Header().Set("Content-Type", "application/json") + + res.WriteHeader(http.StatusForbidden) + + data, _ := json.Marshal(map[string]string{ + "status": "KO", + "message": "Unable to authenticate request", + }) + + res.Write(data) + + return true +} + +func (a *JwtLoginGuardAuthenticator) onAuthenticationSuccess(req *http.Request, res http.ResponseWriter, token GuardToken) bool { + jwtToken := jwt.New(jwt.SigningMethodHS256) + + // @todo: add support for referenced token on database + // token.Header["kid"] = "the sha1" + + // Set reserved claims + jwtToken.Claims["exp"] = time.Now().Add(time.Minute * 30).Unix() + + // Set shared claims + jwtToken.Claims["rls"] = token.GetRoles() + jwtToken.Claims["usr"] = token.GetUsername() + + // Sign and get the complete encoded token as a string + tokenString, _ := jwtToken.SignedString([]byte(a.Key)) + + res.Header().Set("Content-Type", "application/json") + res.Header().Set("X-Token", tokenString) + + data, _ := json.Marshal(map[string]string{ + "status": "OK", + "message": "Request is authenticated", + "token": tokenString, + }) + + res.Write(data) + + return true +} diff --git a/plugins/guard/guard_jwt_login_test.go b/plugins/guard/guard_jwt_login_test.go new file mode 100644 index 0000000..7fbfcdd --- /dev/null +++ b/plugins/guard/guard_jwt_login_test.go @@ -0,0 +1,166 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package guard + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/rande/gonode/core" + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/bcrypt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +func Test_JwtLoginGuardAuthenticator_getCredentials_Valid_Request(t *testing.T) { + a := &JwtLoginGuardAuthenticator{ + LoginPath: "/login", + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + v := url.Values{ + "username": {"thomas"}, + "password": {"ZePassword"}, + } + + req, _ := http.NewRequest("POST", "/login", strings.NewReader(v.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + c, err := a.getCredentials(req) + + assert.NotNil(t, c) + assert.Nil(t, err) + + cs := c.(*struct{ Username, Password string }) + + assert.Equal(t, cs.Username, "thomas") + assert.Equal(t, cs.Password, "ZePassword") +} + +func Test_JwtLoginGuardAuthenticator_checkCredentials_Valid_Password(t *testing.T) { + a := &JwtLoginGuardAuthenticator{ + LoginPath: "/login", + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + password, _ := bcrypt.GenerateFromPassword([]byte("ZePassword"), 1) + + c := &struct{ Username, Password string }{Username: "thomas", Password: "ZePassword"} + u := &DefaultGuardUser{Username: "thomas", Password: string(password[:])} + + err := a.checkCredentials(c, u) + + assert.Nil(t, err) +} + +func Test_JwtLoginGuardAuthenticator_createAuthenticatedToken(t *testing.T) { + a := &JwtLoginGuardAuthenticator{ + LoginPath: "/login", + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + u := &DefaultGuardUser{ + Username: "Thomas", + Password: "EncryptedPassword", + Roles: []string{"ADMIN"}, + } + + token, err := a.createAuthenticatedToken(u) + + assert.NotNil(t, token) + assert.Nil(t, err) + assert.Equal(t, token.GetUsername(), "Thomas") + assert.Equal(t, token.GetRoles(), []string{"ADMIN"}) +} + +func Test_JwtLoginGuardAuthenticator_onAuthenticationSuccess(t *testing.T) { + a := &JwtLoginGuardAuthenticator{ + LoginPath: "/login", + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + req, _ := http.NewRequest("POST", "/login", nil) + res := httptest.NewRecorder() + token := &DefaultGuardToken{ + Username: "thomas", + Roles: []string{"ADMIN"}, + } + + a.onAuthenticationSuccess(req, res, token) + + b := bytes.NewBuffer([]byte("")) + io.Copy(b, res.Body) + + v := &struct { + Status string `json:"status"` + Message string `json:"message"` + Token string `json:"token"` + }{} + + json.Unmarshal(b.Bytes(), v) + + jwtToken, err := jwt.Parse(v.Token, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + + return a.Key, nil + }) + + assert.Nil(t, err) + assert.Equal(t, token.Username, jwtToken.Claims["usr"]) + assert.Equal(t, "application/json", res.Header().Get("Content-Type")) + assert.Equal(t, v.Token, res.Header().Get("X-Token")) + assert.Equal(t, "Request is authenticated", v.Message) + assert.Equal(t, "OK", v.Status) + + // @todo: I fail on basic golang conversion here ... from []interface{} to []string + //assert.Equal(t, token.Roles, jwtToken.Claims["rls"].([]string)) +} + +func Test_JwtLoginGuardAuthenticator_onAuthenticationFailure(t *testing.T) { + a := &JwtLoginGuardAuthenticator{ + LoginPath: "/login", + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + req, _ := http.NewRequest("POST", "/login", nil) + res := httptest.NewRecorder() + + err := InvalidCredentials + + a.onAuthenticationFailure(req, res, err) + b := bytes.NewBuffer([]byte("")) + io.Copy(b, res.Body) + + v := &struct { + Status string `json:"status"` + Message string `json:"message"` + }{} + + json.Unmarshal(b.Bytes(), v) + + assert.Equal(t, "KO", v.Status) + assert.Equal(t, "Unable to authenticate request", v.Message) + assert.Equal(t, "application/json", res.Header().Get("Content-Type")) +} diff --git a/plugins/guard/guard_jwt_token.go b/plugins/guard/guard_jwt_token.go new file mode 100644 index 0000000..d4fc532 --- /dev/null +++ b/plugins/guard/guard_jwt_token.go @@ -0,0 +1,92 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package guard + +import ( + "encoding/json" + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/rande/gonode/core" + "github.com/rande/gonode/plugins/user" + "net/http" + "regexp" +) + +// this authenticator will create a JWT Token from a standard form +type JwtTokenGuardAuthenticator struct { + Path *regexp.Regexp + NodeManager core.NodeManager + Validity int64 + Key []byte +} + +func (a *JwtTokenGuardAuthenticator) getCredentials(req *http.Request) (interface{}, error) { + if !a.Path.Match([]byte(req.RequestURI)) { + return nil, nil + } + + if credentials, err := jwt.ParseFromRequest(req, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + + return []byte(a.Key), nil + }); err != nil { + return nil, InvalidCredentialsFormat + } else { + return credentials, nil + } +} + +func (a *JwtTokenGuardAuthenticator) getUser(credentials interface{}) (GuardUser, error) { + jwtToken := credentials.(*jwt.Token) + + query := a.NodeManager. + SelectBuilder(). + Where("type = 'core.user' AND data->>'username' = ?", jwtToken.Claims["usr"].(string)) + + if node := a.NodeManager.FindOneBy(query); node != nil { + return node.Data.(*user.User), nil + } + + return nil, UnableRetrieveUser +} + +func (a *JwtTokenGuardAuthenticator) checkCredentials(credentials interface{}, user GuardUser) error { + // nothing to do ... + + return nil +} + +func (a *JwtTokenGuardAuthenticator) createAuthenticatedToken(user GuardUser) (GuardToken, error) { + return &DefaultGuardToken{ + Username: user.GetUsername(), + Roles: user.GetRoles(), + }, nil +} + +func (a *JwtTokenGuardAuthenticator) onAuthenticationFailure(req *http.Request, res http.ResponseWriter, err error) bool { + // nothing to do + res.Header().Set("Content-Type", "application/json") + + res.WriteHeader(http.StatusForbidden) + + data, _ := json.Marshal(map[string]string{ + "status": "KO", + "message": "Unable to validate token", + }) + + res.Write(data) + + return true +} + +func (a *JwtTokenGuardAuthenticator) onAuthenticationSuccess(req *http.Request, res http.ResponseWriter, token GuardToken) bool { + // nothing to do + + return false +} diff --git a/plugins/guard/guard_jwt_token_test.go b/plugins/guard/guard_jwt_token_test.go new file mode 100644 index 0000000..af2989c --- /dev/null +++ b/plugins/guard/guard_jwt_token_test.go @@ -0,0 +1,217 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package guard + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/rande/gonode/core" + "github.com/stretchr/testify/assert" + "io" + "net/http" + "net/http/httptest" + "regexp" + "testing" + "time" +) + +func GetToken() *jwt.Token { + jwtToken := jwt.New(jwt.SigningMethodHS256) + + // @todo: add support for referenced token on database + // token.Header["kid"] = "the sha1" + + // Set reserved claims + jwtToken.Claims["exp"] = time.Now().Add(time.Minute * 30).Unix() + + // Set shared claims + jwtToken.Claims["rls"] = []string{"ADMIN"} + jwtToken.Claims["usr"] = "thomas" + + return jwtToken +} + +func Test_JwtTokenGuardAuthenticator_getCredentials_NoMatch(t *testing.T) { + a := &JwtTokenGuardAuthenticator{ + Path: regexp.MustCompile("/api/*"), + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + req, _ := http.NewRequest("GET", "/ressource", nil) + + c, err := a.getCredentials(req) + + assert.Nil(t, c) + assert.Nil(t, err) +} + +func Test_JwtTokenGuardAuthenticator_getCredentials_NoHeader_Request(t *testing.T) { + a := &JwtTokenGuardAuthenticator{ + Path: regexp.MustCompile("/*"), + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + req, _ := http.NewRequest("GET", "/ressource", nil) + + c, err := a.getCredentials(req) + + assert.Nil(t, c) + assert.NotNil(t, err) +} + +func Test_JwtTokenGuardAuthenticator_getCredentials_Invalid_Token(t *testing.T) { + a := &JwtTokenGuardAuthenticator{ + Path: regexp.MustCompile("/*"), + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + req, _ := http.NewRequest("GET", "/ressource", nil) + req.Header.Set("Authorization", "Bearer XXXX") + + c, err := a.getCredentials(req) + + assert.Nil(t, c) + assert.NotNil(t, err) +} + +func Test_JwtTokenGuardAuthenticator_getCredentials_Valid_Token_Header(t *testing.T) { + a := &JwtTokenGuardAuthenticator{ + Path: regexp.MustCompile("/*"), + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + jwtToken := GetToken() + tokenString, _ := jwtToken.SignedString(a.Key) + + req, _ := http.NewRequest("GET", "/ressource", nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tokenString)) + + c, err := a.getCredentials(req) + + assert.NotNil(t, c) + assert.Nil(t, err) + assert.Equal(t, "thomas", c.(*jwt.Token).Claims["usr"].(string)) +} + +func Test_JwtTokenGuardAuthenticator_getCredentials_Valid_Token_QueryString(t *testing.T) { + a := &JwtTokenGuardAuthenticator{ + Path: regexp.MustCompile("/*"), + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + jwtToken := GetToken() + tokenString, _ := jwtToken.SignedString(a.Key) + + req, _ := http.NewRequest("GET", "/ressource?access_token="+tokenString, nil) + + c, err := a.getCredentials(req) + + assert.NotNil(t, c) + assert.Nil(t, err) + assert.Equal(t, "thomas", c.(*jwt.Token).Claims["usr"].(string)) +} + +func Test_JwtTokenGuardAuthenticator_checkCredentials(t *testing.T) { + a := &JwtTokenGuardAuthenticator{ + Path: regexp.MustCompile("/*"), + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + // not used as the getCredentials check token validity + c := GetToken() + u := &DefaultGuardUser{Username: "thomas", Password: "dontcareaboutpassword"} + + err := a.checkCredentials(c, u) + + assert.Nil(t, err) +} + +func Test_JwtTokenGuardAuthenticator_createAuthenticatedToken(t *testing.T) { + a := &JwtTokenGuardAuthenticator{ + Path: regexp.MustCompile("/*"), + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + u := &DefaultGuardUser{ + Username: "Thomas", + Roles: []string{"ADMIN"}, + } + + token, err := a.createAuthenticatedToken(u) + + assert.NotNil(t, token) + assert.Nil(t, err) + assert.Equal(t, token.GetUsername(), "Thomas") + assert.Equal(t, token.GetRoles(), []string{"ADMIN"}) +} + +func Test_JwtTokenGuardAuthenticator_onAuthenticationSuccess(t *testing.T) { + a := &JwtTokenGuardAuthenticator{ + Path: regexp.MustCompile("/*"), + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + req, _ := http.NewRequest("GET", "/ressource", nil) + res := httptest.NewRecorder() + token := &DefaultGuardToken{ + Username: "thomas", + Roles: []string{"ADMIN"}, + } + + a.onAuthenticationSuccess(req, res, token) + + b := bytes.NewBuffer([]byte("")) + io.Copy(b, res.Body) + + assert.Equal(t, b.Len(), 0) +} + +func Test_JwtTokenGuardAuthenticator_onAuthenticationFailure(t *testing.T) { + a := &JwtTokenGuardAuthenticator{ + Path: regexp.MustCompile("/*"), + NodeManager: &core.MockedManager{}, + Validity: 12, + Key: []byte("ZeKey"), + } + + req, _ := http.NewRequest("GET", "/ressource", nil) + res := httptest.NewRecorder() + + err := InvalidCredentials + + a.onAuthenticationFailure(req, res, err) + + b := bytes.NewBuffer([]byte("")) + io.Copy(b, res.Body) + + v := &struct { + Status string `json:"status"` + Message string `json:"message"` + }{} + + json.Unmarshal(b.Bytes(), v) + + assert.Equal(t, "KO", v.Status) + assert.Equal(t, "Unable to validate token", v.Message) +} diff --git a/plugins/guard/guard_middleware.go b/plugins/guard/guard_middleware.go new file mode 100644 index 0000000..4c82489 --- /dev/null +++ b/plugins/guard/guard_middleware.go @@ -0,0 +1,78 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package guard + +import ( + "github.com/zenazn/goji/web" + "net/http" +) + +func GetGuardMiddleware(auths []GuardAuthenticator) func(c *web.C, h http.Handler) http.Handler { + return func(c *web.C, h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + + // handle security here + for _, authenticator := range auths { + performed, output := performAuthentication(c, authenticator, w, r) + + if performed && output { + return + } else if performed { + break + } + } + + h.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} + +// false means, no authentification has been done +func performAuthentication(c *web.C, a GuardAuthenticator, w http.ResponseWriter, r *http.Request) (bool, bool) { + var o bool + + // get credentials from request + credentials, err := a.getCredentials(r) + + if err == InvalidCredentialsFormat { + o = a.onAuthenticationFailure(r, w, err) + + return true, o + } + + // no credentials, return + if credentials == nil { // nothing to do, next one + return false, false + } + + // ok get the current user for the current credentials + user, err := a.getUser(credentials) + + if err != nil || user == nil { + o = a.onAuthenticationFailure(r, w, err) + + return true, o + } + + // check if the request's credentials match user credentials + if err = a.checkCredentials(credentials, user); err != nil { + o = a.onAuthenticationFailure(r, w, err) + + return true, o + } + + // create a valid security token for the user + token, err := a.createAuthenticatedToken(user) + + c.Env["guard_token"] = token + + // complete the process + o = a.onAuthenticationSuccess(r, w, token) + + return true, o +} diff --git a/plugins/guard/guard_middleware_test.go b/plugins/guard/guard_middleware_test.go new file mode 100644 index 0000000..b158e91 --- /dev/null +++ b/plugins/guard/guard_middleware_test.go @@ -0,0 +1,114 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package guard + +import ( + "github.com/stretchr/testify/assert" + "github.com/zenazn/goji/web" + "net/http" + "net/http/httptest" + "testing" +) + +func Test_Perform_Authentification_With_Not_Found_Credentials(t *testing.T) { + + r, _ := http.NewRequest("GET", "/foobar", nil) + w := httptest.NewRecorder() + + a := &MockedAuthenticator{} + a.On("getCredentials", r).Return(nil, nil) + + cw := &web.C{Env: make(map[interface{}]interface{})} + + performed, output := performAuthentication(cw, a, w, r) + + assert.False(t, performed) + assert.False(t, output) +} + +func Test_Perform_Authentification_With_Not_User_Found(t *testing.T) { + r, _ := http.NewRequest("GET", "/foobar", nil) + w := httptest.NewRecorder() + + c := map[string]string{ + "login": "thomas", + "password": "password", + } + + a := &MockedAuthenticator{} + a.On("getCredentials", r).Return(c, nil) + a.On("getUser", c).Return(nil, nil) + a.On("onAuthenticationFailure", r, w, nil).Return(true) + + cw := &web.C{Env: make(map[interface{}]interface{})} + + performed, output := performAuthentication(cw, a, w, r) + + assert.True(t, performed) + assert.True(t, output) +} + +func Test_Perform_Authentification_With_Invalid_Credentials(t *testing.T) { + r, _ := http.NewRequest("GET", "/foobar", nil) + w := httptest.NewRecorder() + + c := map[string]string{ + "login": "thomas", + "password": "password", + } + + u := &DefaultGuardUser{ + Username: "thomas", + Password: "password", + } + + a := &MockedAuthenticator{} + a.On("getCredentials", r).Return(c, nil) + a.On("getUser", c).Return(u, nil) + a.On("checkCredentials", c, u).Return(InvalidCredentials) + a.On("onAuthenticationFailure", r, w, InvalidCredentials).Return(true) + + cw := &web.C{Env: make(map[interface{}]interface{})} + + performed, output := performAuthentication(cw, a, w, r) + + assert.True(t, performed) + assert.True(t, output) +} + +func Test_Perform_Authentification_With_Valid_User(t *testing.T) { + r, _ := http.NewRequest("GET", "/foobar", nil) + w := httptest.NewRecorder() + + c := map[string]string{ + "login": "thomas", + "password": "password", + } + + u := &DefaultGuardUser{ + Username: "thomas", + Password: "password", + } + + token := &DefaultGuardToken{ + Username: "thomas", + } + + a := &MockedAuthenticator{} + a.On("getCredentials", r).Return(c, nil) + a.On("getUser", c).Return(u, nil) + a.On("checkCredentials", c, u).Return(nil) + a.On("createAuthenticatedToken", u).Return(token, nil) + a.On("onAuthenticationSuccess", r, w, token).Return(true) + + cw := &web.C{Env: make(map[interface{}]interface{})} + + performed, output := performAuthentication(cw, a, w, r) + + assert.True(t, performed) + assert.True(t, output) + assert.Equal(t, token, cw.Env["guard_token"]) +} diff --git a/plugins/guard/guard_mock.go b/plugins/guard/guard_mock.go new file mode 100644 index 0000000..d818f7c --- /dev/null +++ b/plugins/guard/guard_mock.go @@ -0,0 +1,59 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package guard + +import ( + "github.com/stretchr/testify/mock" + "net/http" +) + +type MockedAuthenticator struct { + mock.Mock +} + +func (m *MockedAuthenticator) getCredentials(req *http.Request) (interface{}, error) { + args := m.Mock.Called(req) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(interface{}), args.Error(1) +} +func (m *MockedAuthenticator) getUser(credentials interface{}) (GuardUser, error) { + args := m.Mock.Called(credentials) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(GuardUser), args.Error(1) +} +func (m *MockedAuthenticator) checkCredentials(credentials interface{}, user GuardUser) error { + args := m.Mock.Called(credentials, user) + + return args.Error(0) +} +func (m *MockedAuthenticator) createAuthenticatedToken(u GuardUser) (GuardToken, error) { + args := m.Mock.Called(u) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(GuardToken), args.Error(1) +} +func (m *MockedAuthenticator) onAuthenticationFailure(req *http.Request, res http.ResponseWriter, err error) bool { + args := m.Mock.Called(req, res, err) + + return args.Bool(0) +} + +func (m *MockedAuthenticator) onAuthenticationSuccess(req *http.Request, res http.ResponseWriter, token GuardToken) bool { + args := m.Mock.Called(req, res, token) + + return args.Bool(0) +} diff --git a/plugins/guard/jwt_token.go b/plugins/guard/jwt_token.go new file mode 100644 index 0000000..44e49c7 --- /dev/null +++ b/plugins/guard/jwt_token.go @@ -0,0 +1,61 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package guard + +import ( + "github.com/rande/gonode/core" + "io" + "time" +) + +type JwtTokenMeta struct { + Expiration time.Time `json:"expiration"` +} + +type JwtToken struct { + User *core.Reference `json:"user"` + Key []byte `json:"key"` + Roles []string `json:"roles"` +} + +type JwtTokentHandler struct { +} + +func (h *JwtTokentHandler) GetStruct() (core.NodeData, core.NodeMeta) { + return &JwtToken{}, &JwtTokenMeta{} +} + +func (h *JwtTokentHandler) PreInsert(node *core.Node, m core.NodeManager) error { + return nil +} + +func (h *JwtTokentHandler) PreUpdate(node *core.Node, m core.NodeManager) error { + return nil +} + +func (h *JwtTokentHandler) PostInsert(node *core.Node, m core.NodeManager) error { + return nil +} + +func (h *JwtTokentHandler) PostUpdate(node *core.Node, m core.NodeManager) error { + return nil +} + +func (h *JwtTokentHandler) Validate(node *core.Node, m core.NodeManager, errors core.Errors) { + +} + +func (h *JwtTokentHandler) GetDownloadData(node *core.Node) *core.DownloadData { + return core.GetDownloadData() +} + +func (h *JwtTokentHandler) Load(data []byte, meta []byte, node *core.Node) error { + return core.HandlerLoad(h, data, meta, node) +} + +func (h *JwtTokentHandler) StoreStream(node *core.Node, r io.Reader) (int64, error) { + return core.DefaultHandlerStoreStream(node, r) +} diff --git a/plugins/setup/setup_app.go b/plugins/setup/setup_app.go index 331ec8d..d588f10 100644 --- a/plugins/setup/setup_app.go +++ b/plugins/setup/setup_app.go @@ -14,12 +14,16 @@ import ( func ConfigureServer(l *goapp.Lifecycle, conf *config.ServerConfig) { l.Prepare(func(app *goapp.App) error { + if !conf.Test { + return nil + } + mux := app.Get("goji.mux").(*web.Mux) manager := app.Get("gonode.manager").(*core.PgNodeManager) prefix := "" - mux.Put(prefix+"/uninstall", func(res http.ResponseWriter, req *http.Request) { + mux.Put(prefix+"/setup/uninstall", func(res http.ResponseWriter, req *http.Request) { res.Header().Set("Content-Type", "application/json") res.Header().Set("Access-Control-Allow-Origin", "*") @@ -35,7 +39,7 @@ func ConfigureServer(l *goapp.Lifecycle, conf *config.ServerConfig) { helper.SendWithHttpCode(res, http.StatusOK, "Successfully delete tables!") }) - mux.Put(prefix+"/install", func(res http.ResponseWriter, req *http.Request) { + mux.Put(prefix+"/setup/install", func(res http.ResponseWriter, req *http.Request) { res.Header().Set("Content-Type", "application/json") res.Header().Set("Access-Control-Allow-Origin", "*") @@ -112,19 +116,7 @@ func ConfigureServer(l *goapp.Lifecycle, conf *config.ServerConfig) { } }) - return nil - }) - - l.Prepare(func(app *goapp.App) error { - if !conf.Test { - return nil - } - - mux := app.Get("goji.mux").(*web.Mux) - - prefix := "" - - mux.Put(prefix+"/data/purge", func(res http.ResponseWriter, req *http.Request) { + mux.Put(prefix+"/setup/data/purge", func(res http.ResponseWriter, req *http.Request) { manager := app.Get("gonode.manager").(*core.PgNodeManager) @@ -142,7 +134,7 @@ func ConfigureServer(l *goapp.Lifecycle, conf *config.ServerConfig) { } }) - mux.Put(prefix+"/data/load", func(res http.ResponseWriter, req *http.Request) { + mux.Put(prefix+"/setup/data/load", func(res http.ResponseWriter, req *http.Request) { manager := app.Get("gonode.manager").(*core.PgNodeManager) nodes := manager.FindBy(manager.SelectBuilder(), 0, 10) diff --git a/server.toml.dist b/server.toml.dist index 04d0215..454fb8a 100644 --- a/server.toml.dist +++ b/server.toml.dist @@ -1,5 +1,6 @@ name= "GoNode - poc" bind= ":2405" +test= true [databases.master] type = "master" @@ -9,3 +10,13 @@ prefix = "prod" [filesystem] path = "/tmp/gnode" + +[guard] +key = "ZeSecretKey0oo" + + [guard.jwt] + [guard.jwt.login] + path = "/login" + + [guard.jwt.token] + path = "^\\/nodes\\/(.*)$" diff --git a/test/api/create_test.go b/test/api/create_test.go index 664b033..0da2c38 100644 --- a/test/api/create_test.go +++ b/test/api/create_test.go @@ -20,9 +20,11 @@ import ( func Test_Create_User(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *App) { + auth := test.GetAuthHeader(t, ts) + // WITH file, _ := os.Open("../fixtures/new_user.json") - res, _ := test.RunRequest("POST", ts.URL+"/nodes", file) + res, _ := test.RunRequest("POST", ts.URL+"/nodes", file, auth) assert.Equal(t, 201, res.StatusCode) @@ -43,9 +45,11 @@ func Test_Create_User(t *testing.T) { func Test_Create_Media_With_Binary_Upload(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *App) { + auth := test.GetAuthHeader(t, ts) + // WITH file, _ := os.Open("../fixtures/new_image.json") - res, _ := test.RunRequest("POST", ts.URL+"/nodes", file) + res, _ := test.RunRequest("POST", ts.URL+"/nodes", file, auth) assert.Equal(t, 201, res.StatusCode) @@ -55,15 +59,15 @@ func Test_Create_Media_With_Binary_Upload(t *testing.T) { var message = "The content of the file, yep it is not an image" - res, _ = test.RunRequest("PUT", ts.URL+"/nodes/"+node.Uuid.CleanString()+"?raw", strings.NewReader(message)) + res, _ = test.RunRequest("PUT", ts.URL+"/nodes/"+node.Uuid.CleanString()+"?raw", strings.NewReader(message), auth) assert.Equal(t, 200, res.StatusCode) - res, _ = test.RunRequest("GET", ts.URL+"/nodes/"+node.Uuid.CleanString()+"?raw", nil) + res, _ = test.RunRequest("GET", ts.URL+"/nodes/"+node.Uuid.CleanString()+"?raw", nil, auth) assert.Equal(t, message, string(res.GetBody()[:])) - res, _ = test.RunRequest("GET", ts.URL+"/nodes/"+node.Uuid.CleanString(), nil) + res, _ = test.RunRequest("GET", ts.URL+"/nodes/"+node.Uuid.CleanString(), nil, auth) assert.Equal(t, 200, res.StatusCode) node = core.NewNode() diff --git a/test/api/delete_test.go b/test/api/delete_test.go index 7876a90..6c5d6fc 100644 --- a/test/api/delete_test.go +++ b/test/api/delete_test.go @@ -17,7 +17,9 @@ import ( func Test_Delete_Non_Existant_Node(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *App) { - res, _ := test.RunRequest("DELETE", ts.URL+"/nodes/d703a3ab-8374-4c30-a8a4-2c22aa67763b", nil) + auth := test.GetAuthHeader(t, ts) + + res, _ := test.RunRequest("DELETE", ts.URL+"/nodes/d703a3ab-8374-4c30-a8a4-2c22aa67763b", nil, auth) assert.Equal(t, 404, res.StatusCode, "Delete non existant node") }) @@ -25,8 +27,10 @@ func Test_Delete_Non_Existant_Node(t *testing.T) { func Test_Delete_Existant_Node(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *App) { + auth := test.GetAuthHeader(t, ts) + file, _ := os.Open("../fixtures/new_user.json") - res, _ := test.RunRequest("POST", ts.URL+"/nodes", file) + res, _ := test.RunRequest("POST", ts.URL+"/nodes", file, auth) assert.Equal(t, 201, res.StatusCode, "Node created") @@ -37,7 +41,7 @@ func Test_Delete_Existant_Node(t *testing.T) { assert.Equal(t, "core.user", node.Type) - res, _ = test.RunRequest("DELETE", ts.URL+"/nodes/"+node.Uuid.CleanString(), nil) + res, _ = test.RunRequest("DELETE", ts.URL+"/nodes/"+node.Uuid.CleanString(), nil, auth) assert.Equal(t, 200, res.StatusCode) serializer.Deserialize(res.Body, node) @@ -45,28 +49,29 @@ func Test_Delete_Existant_Node(t *testing.T) { assert.Equal(t, node.Deleted, true) // test if we can delete a deleted node ... - res, _ = test.RunRequest("DELETE", ts.URL+"/nodes/"+node.Uuid.CleanString(), nil) + res, _ = test.RunRequest("DELETE", ts.URL+"/nodes/"+node.Uuid.CleanString(), nil, auth) assert.Equal(t, 410, res.StatusCode) }) } func Test_Delete_Find_Filter(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *App) { + auth := test.GetAuthHeader(t, ts) nodes := InitSearchFixture(app) - res, _ := test.RunRequest("GET", ts.URL+"/nodes", nil) + res, _ := test.RunRequest("GET", ts.URL+"/nodes", nil, auth) p := GetPager(app, res) - assert.Equal(t, 3, len(p.Elements)) + assert.Equal(t, 4, len(p.Elements)) - res, _ = test.RunRequest("DELETE", ts.URL+"/nodes/"+nodes[0].Uuid.CleanString(), nil) + res, _ = test.RunRequest("DELETE", ts.URL+"/nodes/"+nodes[0].Uuid.CleanString(), nil, auth) assert.Equal(t, 200, res.StatusCode) - res, _ = test.RunRequest("GET", ts.URL+"/nodes", nil) + res, _ = test.RunRequest("GET", ts.URL+"/nodes", nil, auth) assert.Equal(t, 200, res.StatusCode) p = GetPager(app, res) - assert.Equal(t, 2, len(p.Elements)) + assert.Equal(t, 3, len(p.Elements)) }) } diff --git a/test/api/find_test.go b/test/api/find_test.go index 1c19a87..1a64f4e 100644 --- a/test/api/find_test.go +++ b/test/api/find_test.go @@ -15,7 +15,9 @@ import ( func Test_Find_Non_Existant(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *App) { - res, _ := test.RunRequest("GET", ts.URL+"/nodes/d703a3ab-8374-4c30-a8a4-2c22aa67763b", nil) + auth := test.GetAuthHeader(t, ts) + + res, _ := test.RunRequest("GET", ts.URL+"/nodes/d703a3ab-8374-4c30-a8a4-2c22aa67763b", nil, auth) assert.Equal(t, 404, res.StatusCode, "Delete non existant node") }) diff --git a/test/api/guard_app_test.go b/test/api/guard_app_test.go new file mode 100644 index 0000000..ade0f4d --- /dev/null +++ b/test/api/guard_app_test.go @@ -0,0 +1,18 @@ +package api + +import ( + "fmt" + "github.com/rande/goapp" + "github.com/rande/gonode/test" + "github.com/stretchr/testify/assert" + "net/http/httptest" + "testing" +) + +func Test_Guard_Error(t *testing.T) { + test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + res, _ := test.RunRequest("GET", fmt.Sprintf("%s/nodes/protected", ts.URL), nil) + + assert.Equal(t, 403, res.StatusCode) + }) +} diff --git a/test/api/login_jwt_test.go b/test/api/login_jwt_test.go new file mode 100644 index 0000000..2d1e32f --- /dev/null +++ b/test/api/login_jwt_test.go @@ -0,0 +1,82 @@ +// Copyright © 2014-2015 Thomas Rabaix . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/rande/goapp" + "github.com/rande/gonode/core" + "github.com/rande/gonode/core/config" + "github.com/rande/gonode/plugins/user" + "github.com/rande/gonode/test" + "github.com/stretchr/testify/assert" + "io" + "net/http/httptest" + "net/url" + "testing" +) + +func Test_Create_Username(t *testing.T) { + test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + + configuration := app.Get("gonode.configuration").(*config.ServerConfig) + + // WITH + // create a valid user into the database ... + manager := app.Get("gonode.manager").(*core.PgNodeManager) + + u := app.Get("gonode.handler_collection").(core.HandlerCollection).NewNode("core.user") + data := u.Data.(*user.User) + data.Email = "test@example.org" + data.Enabled = true + data.FirstName = "Thomas" + data.LastName = "Rxxxx" + data.NewPassword = "ZePassword" + data.Username = "rande" + + meta := u.Meta.(*user.UserMeta) + meta.PasswordCost = 1 // save test time + + manager.Save(u, false) + + res, _ := test.RunRequest("POST", fmt.Sprintf("%s/login", ts.URL), url.Values{ + "username": {data.Username}, + "password": {"ZePassword"}, + }) + + assert.Equal(t, 200, res.StatusCode) + + b := bytes.NewBuffer([]byte("")) + io.Copy(b, res.Body) + + v := &struct { + Status string `json:"status"` + Message string `json:"message"` + Token string `json:"token"` + }{} + + json.Unmarshal(b.Bytes(), v) + + token, err := jwt.Parse(v.Token, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + + return []byte(configuration.Guard.Key), nil + }) + + assert.NotNil(t, configuration.Guard.Key) + assert.True(t, len(configuration.Guard.Key) > 0) + assert.Nil(t, err) + assert.True(t, token.Valid) + + fmt.Printf("%v", token) + }) +} diff --git a/test/api/parent_test.go b/test/api/parent_test.go index 2884650..6efc588 100644 --- a/test/api/parent_test.go +++ b/test/api/parent_test.go @@ -67,6 +67,8 @@ func Test_Create_Parents_With_Manager(t *testing.T) { func Test_Create_Parents_With_Api(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { // WITH + auth := test.GetAuthHeader(t, ts) + handlers := app.Get("gonode.handler_collection").(core.HandlerCollection) manager := app.Get("gonode.manager").(*core.PgNodeManager) @@ -82,16 +84,16 @@ func Test_Create_Parents_With_Api(t *testing.T) { node4 := handlers.NewNode("default") manager.Save(node4, false) - res, _ := test.RunRequest("PUT", fmt.Sprintf("%s/nodes/move/%s/%s", ts.URL, node2.Uuid, node1.Uuid), nil) + res, _ := test.RunRequest("PUT", fmt.Sprintf("%s/nodes/move/%s/%s", ts.URL, node2.Uuid, node1.Uuid), nil, auth) assert.Equal(t, 200, res.StatusCode) - res, _ = test.RunRequest("PUT", fmt.Sprintf("%s/nodes/move/%s/%s", ts.URL, node3.Uuid, node2.Uuid), nil) + res, _ = test.RunRequest("PUT", fmt.Sprintf("%s/nodes/move/%s/%s", ts.URL, node3.Uuid, node2.Uuid), nil, auth) assert.Equal(t, 200, res.StatusCode) - res, _ = test.RunRequest("PUT", fmt.Sprintf("%s/nodes/move/%s/%s", ts.URL, node4.Uuid, node3.Uuid), nil) + res, _ = test.RunRequest("PUT", fmt.Sprintf("%s/nodes/move/%s/%s", ts.URL, node4.Uuid, node3.Uuid), nil, auth) assert.Equal(t, 200, res.StatusCode) - res, _ = test.RunRequest("PUT", fmt.Sprintf("%s/nodes/move/%s/%s", ts.URL, node1.Uuid, node4.Uuid), nil) + res, _ = test.RunRequest("PUT", fmt.Sprintf("%s/nodes/move/%s/%s", ts.URL, node1.Uuid, node4.Uuid), nil, auth) assert.Equal(t, 200, res.StatusCode) serializer := app.Get("gonode.node.serializer").(*core.Serializer) diff --git a/test/api/search_test.go b/test/api/search_test.go index 63fa329..1ba51b0 100644 --- a/test/api/search_test.go +++ b/test/api/search_test.go @@ -25,29 +25,33 @@ func CheckNoResults(t *testing.T, p *api.ApiPager) { } func Test_Search_Basic(t *testing.T) { - urls := []string{ - "/nodes", - "/nodes?type=core.user", - "/nodes?type=core.user&data.username=user12", - "/nodes?type=core.user&data.username=user12&data.username=user13", - "/nodes?&page=-1&page=1", // the last occurrence erase first values + values := []struct { + Url string + Len int + }{ + {"/nodes", 2}, + {"/nodes?type=core.user", 2}, + {"/nodes?type=core.user&data.username=user12", 1}, + {"/nodes?type=core.user&data.username=user12&data.username=user13", 1}, + {"/nodes?&page=-1&page=1", 2}, // the last occurrence erase first values } - for _, url := range urls { + for _, v := range values { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { // WITH + auth := test.GetAuthHeader(t, ts) file, _ := os.Open("../fixtures/new_user.json") - test.RunRequest("POST", ts.URL+"/nodes", file) + test.RunRequest("POST", ts.URL+"/nodes", file, auth) // WHEN - res, _ := test.RunRequest("GET", ts.URL+url, nil) + res, _ := test.RunRequest("GET", ts.URL+v.Url, nil, auth) p := GetPager(app, res) // THEN assert.Equal(t, uint64(32), p.PerPage) assert.Equal(t, uint64(1), p.Page) - assert.Equal(t, 1, len(p.Elements)) + assert.Equal(t, v.Len, len(p.Elements)) assert.Equal(t, uint64(0), p.Next) assert.Equal(t, uint64(0), p.Previous) @@ -62,11 +66,12 @@ func Test_Search_Basic(t *testing.T) { func Test_Search_NoResult(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { // WITH + auth := test.GetAuthHeader(t, ts) file, _ := os.Open("../fixtures/new_user.json") test.RunRequest("POST", ts.URL+"/nodes", file) // WHEN - res, _ := test.RunRequest("GET", ts.URL+"/nodes?type=other", nil) + res, _ := test.RunRequest("GET", ts.URL+"/nodes?type=other", nil, auth) p := GetPager(app, res) @@ -87,11 +92,13 @@ func Test_Search_Invalid_Pagination(t *testing.T) { for _, url := range urls { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + // WITH + auth := test.GetAuthHeader(t, ts) file, _ := os.Open("../fixtures/new_user.json") - test.RunRequest("POST", ts.URL+"/nodes", file) + test.RunRequest("POST", ts.URL+"/nodes", file, auth) // WHEN - res, _ := test.RunRequest("GET", ts.URL+url, nil) + res, _ := test.RunRequest("GET", ts.URL+url, nil, auth) assert.Equal(t, 412, res.StatusCode, "url: "+url) }) @@ -100,27 +107,30 @@ func Test_Search_Invalid_Pagination(t *testing.T) { func Test_Search_Invalid_OrderBy(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + auth := test.GetAuthHeader(t, ts) + // seems goji or golang block this request - res, _ := test.RunRequest("GET", ts.URL+"/nodes?order_by=\"1 = 1\"; DELETE * FROM node,ASC", nil) + res, _ := test.RunRequest("GET", ts.URL+"/nodes?order_by=\"1 = 1\"; DELETE * FROM node,ASC", nil, auth) assert.Equal(t, 400, res.StatusCode, "url: /nodes?order_by=\"1 = 1\"; DELETE * FROM node,ASC") // seems goji or golang block this request - res, _ = test.RunRequest("GET", ts.URL+"/nodes?order_by=DELETE%20*%20FROM%20node,ASC", nil) + res, _ = test.RunRequest("GET", ts.URL+"/nodes?order_by=DELETE%20*%20FROM%20node,ASC", nil, auth) assert.Equal(t, 412, res.StatusCode, "url: /nodes?order_by=DELETE%20*%20FROM%20node,ASC") }) } func Test_Search_OrderBy_Name_ASC(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + auth := test.GetAuthHeader(t, ts) InitSearchFixture(app) - res, _ := test.RunRequest("GET", ts.URL+"/nodes?order_by=name,ASC", nil) + res, _ := test.RunRequest("GET", ts.URL+"/nodes?order_by=name,ASC", nil, auth) assert.Equal(t, 200, res.StatusCode, "url: /nodes?order_by=name,ASC") p := GetPager(app, res) - assert.Equal(t, 3, len(p.Elements)) + assert.Equal(t, 4, len(p.Elements)) assert.Equal(t, "User A", p.Elements[0].(*core.Node).Name) assert.Equal(t, "User AA", p.Elements[1].(*core.Node).Name) assert.Equal(t, "User B", p.Elements[2].(*core.Node).Name) @@ -129,33 +139,35 @@ func Test_Search_OrderBy_Name_ASC(t *testing.T) { func Test_Search_OrderBy_Name_DESC(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + auth := test.GetAuthHeader(t, ts) InitSearchFixture(app) - res, _ := test.RunRequest("GET", ts.URL+"/nodes?order_by=name,DESC", nil) + res, _ := test.RunRequest("GET", ts.URL+"/nodes?order_by=name,DESC", nil, auth) assert.Equal(t, 200, res.StatusCode, "url: /nodes?order_by=name,DESC") p := GetPager(app, res) - assert.Equal(t, 3, len(p.Elements)) - assert.Equal(t, "User B", p.Elements[0].(*core.Node).Name) - assert.Equal(t, "User AA", p.Elements[1].(*core.Node).Name) - assert.Equal(t, "User A", p.Elements[2].(*core.Node).Name) + assert.Equal(t, 4, len(p.Elements)) + assert.Equal(t, "User ZZ", p.Elements[0].(*core.Node).Name) + assert.Equal(t, "User B", p.Elements[1].(*core.Node).Name) + assert.Equal(t, "User AA", p.Elements[2].(*core.Node).Name) }) } func Test_Search_OrderBy_Weight_DESC_Name_ASC(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + auth := test.GetAuthHeader(t, ts) InitSearchFixture(app) // TESTING WITH 2 ORDERING OPTION - res, _ := test.RunRequest("GET", ts.URL+"/nodes?order_by=weight,DESC&order_by=name,ASC", nil) + res, _ := test.RunRequest("GET", ts.URL+"/nodes?order_by=weight,DESC&order_by=name,ASC", nil, auth) assert.Equal(t, 200, res.StatusCode, "url: /nodes?order_by=weight,DESC&order_by=name,ASC") p := GetPager(app, res) - assert.Equal(t, 3, len(p.Elements)) + assert.Equal(t, 4, len(p.Elements)) assert.Equal(t, "User AA", p.Elements[0].(*core.Node).Name) assert.Equal(t, "User A", p.Elements[1].(*core.Node).Name) assert.Equal(t, "User B", p.Elements[2].(*core.Node).Name) @@ -164,27 +176,29 @@ func Test_Search_OrderBy_Weight_DESC_Name_ASC(t *testing.T) { func Test_Search_OrderBy_Meta_Username(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + auth := test.GetAuthHeader(t, ts) InitSearchFixture(app) // TESTING WITH 2 ORDERING OPTION - res, _ := test.RunRequest("GET", ts.URL+"/nodes?order_by=meta.username,DESC", nil) + res, _ := test.RunRequest("GET", ts.URL+"/nodes?order_by=meta.username,DESC", nil, auth) assert.Equal(t, 200, res.StatusCode, "url: /nodes?order_by=meta.username") p := GetPager(app, res) - assert.Equal(t, 3, len(p.Elements)) - assert.Equal(t, "User A", p.Elements[0].(*core.Node).Name) - assert.Equal(t, "User AA", p.Elements[1].(*core.Node).Name) - assert.Equal(t, "User B", p.Elements[2].(*core.Node).Name) + assert.Equal(t, 4, len(p.Elements)) + assert.Equal(t, "User ZZ", p.Elements[0].(*core.Node).Name) + assert.Equal(t, "User A", p.Elements[1].(*core.Node).Name) + assert.Equal(t, "User AA", p.Elements[2].(*core.Node).Name) }) } func Test_Search_OrderBy_Meta_Non_Existant_Meta(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + auth := test.GetAuthHeader(t, ts) InitSearchFixture(app) - res, _ := test.RunRequest("GET", ts.URL+"/nodes?meta.username.fake=foo&order_by=meta.username.fake,DESC", nil) + res, _ := test.RunRequest("GET", ts.URL+"/nodes?meta.username.fake=foo&order_by=meta.username.fake,DESC", nil, auth) assert.Equal(t, 200, res.StatusCode, "url: /nodes?order_by=meta.username.fake") @@ -196,9 +210,10 @@ func Test_Search_OrderBy_Meta_Non_Existant_Meta(t *testing.T) { func Test_Search_Meta(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + auth := test.GetAuthHeader(t, ts) InitSearchFixture(app) - res, _ := test.RunRequest("GET", ts.URL+"/nodes?data.username=user-a", nil) + res, _ := test.RunRequest("GET", ts.URL+"/nodes?data.username=user-a", nil, auth) assert.Equal(t, 200, res.StatusCode, "url: /nodes?data.username=user-a") @@ -210,9 +225,10 @@ func Test_Search_Meta(t *testing.T) { func Test_Search_Slug(t *testing.T) { test.RunHttpTest(t, func(t *testing.T, ts *httptest.Server, app *goapp.App) { + auth := test.GetAuthHeader(t, ts) InitSearchFixture(app) - res, _ := test.RunRequest("GET", ts.URL+"/nodes?slug=user-a", nil) + res, _ := test.RunRequest("GET", ts.URL+"/nodes?slug=user-a", nil, auth) assert.Equal(t, 200, res.StatusCode, "url: /nodes?slug=user-a") diff --git a/test/config_codeship.toml b/test/config_codeship.toml index 7169efa..711f138 100644 --- a/test/config_codeship.toml +++ b/test/config_codeship.toml @@ -10,3 +10,13 @@ prefix = "test" [filesystem] path = "/tmp/gnode" + +[guard] +key = "ZeSecretKey0oo" + + [guard.jwt] + [guard.jwt.login] + path = "/login" + + [guard.jwt.token] + path = "^\\/nodes\\/(.*)$" diff --git a/test/config_test.toml b/test/config_test.toml index 660acc4..dcbda87 100644 --- a/test/config_test.toml +++ b/test/config_test.toml @@ -10,3 +10,13 @@ prefix = "test" [filesystem] path = "/tmp/gnode" + +[guard] +key = "ZeSecretKey0oo" + + [guard.jwt] + [guard.jwt.login] + path = "/login" + + [guard.jwt.token] + path = "^\\/nodes\\/(.*)$" diff --git a/test/config_travis.toml b/test/config_travis.toml index 7f9afd5..ab637c4 100644 --- a/test/config_travis.toml +++ b/test/config_travis.toml @@ -10,3 +10,13 @@ prefix = "test" [filesystem] path = "/tmp/gnode" + +[guard] +key = "ZeSecretKey0oo" + + [guard.jwt] + [guard.jwt.login] + path = "/login" + + [guard.jwt.token] + path = "^\\/nodes\\/(.*)$" diff --git a/test/test.go b/test/test.go index 95045db..856be69 100644 --- a/test/test.go +++ b/test/test.go @@ -6,13 +6,17 @@ package test import ( + "bytes" + "encoding/json" "fmt" "github.com/rande/goapp" "github.com/rande/gonode/commands/server" "github.com/rande/gonode/core" "github.com/rande/gonode/core/config" "github.com/rande/gonode/plugins/api" + "github.com/rande/gonode/plugins/guard" "github.com/rande/gonode/plugins/setup" + "github.com/rande/gonode/plugins/user" "github.com/stretchr/testify/assert" "github.com/zenazn/goji/web" "github.com/zenazn/goji/web/middleware" @@ -54,10 +58,8 @@ func GetLifecycle(file string) *goapp.Lifecycle { app.Set("goji.mux", func(app *goapp.App) interface{} { mux := web.New() - // mux.Use(middleware.RequestID) mux.Use(middleware.Logger) mux.Use(middleware.Recoverer) - // mux.Use(middleware.AutomaticOptions) return mux }) @@ -66,8 +68,11 @@ func GetLifecycle(file string) *goapp.Lifecycle { }) server.ConfigureServer(l, conf) + + // configure plugin api.ConfigureServer(l, conf) setup.ConfigureServer(l, conf) + guard.ConfigureServer(l, conf) return l } @@ -94,7 +99,46 @@ func (r Response) GetBody() []byte { return r.RawBody } -func RunRequest(method string, path string, body interface{}) (*Response, error) { +func GetAuthHeader(t *testing.T, ts *httptest.Server) map[string]string { + return map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", GetAuthToken(t, ts)), + } +} + +func GetAuthToken(t *testing.T, ts *httptest.Server) string { + res, _ := RunRequest("POST", fmt.Sprintf("%s/login", ts.URL), url.Values{ + "username": {"test-admin"}, + "password": {"admin"}, + }) + + assert.Equal(t, 200, res.StatusCode) + + b := bytes.NewBuffer([]byte("")) + io.Copy(b, res.Body) + + v := &struct { + Status string `json:"status"` + Message string `json:"message"` + Token string `json:"token"` + }{} + + json.Unmarshal(b.Bytes(), v) + + return v.Token +} + +func RunRequest(method string, path string, options ...interface{}) (*Response, error) { + var body interface{} + var headers map[string]string + + if len(options) > 0 { + body = options[0] + } + + if len(options) > 1 { + headers = options[1].(map[string]string) + } + client := &http.Client{} var req *http.Request var err error @@ -115,6 +159,12 @@ func RunRequest(method string, path string, body interface{}) (*Response, error) panic(fmt.Sprintf("please add a new test case for %T", body)) } + if headers != nil { + for name, value := range headers { + req.Header.Set(name, value) + } + } + core.PanicOnError(err) resp, err := client.Do(req) @@ -142,14 +192,31 @@ func RunHttpTest(t *testing.T, f func(t *testing.T, ts *httptest.Server, app *go } }() - res, err = RunRequest("PUT", ts.URL+"/uninstall", nil) + res, err = RunRequest("PUT", ts.URL+"/setup/uninstall", nil) core.PanicIf(res.StatusCode != http.StatusOK, fmt.Sprintf("Expected code 200, get %d\n%s", res.StatusCode, string(res.GetBody()[:]))) core.PanicOnError(err) - res, err = RunRequest("PUT", ts.URL+"/install", nil) + res, err = RunRequest("PUT", ts.URL+"/setup/install", nil) core.PanicIf(res.StatusCode != http.StatusOK, fmt.Sprintf("Expected code 200, get %d\n%s", res.StatusCode, string(res.GetBody()[:]))) core.PanicOnError(err) + // create a valid user + manager := app.Get("gonode.manager").(*core.PgNodeManager) + + u := app.Get("gonode.handler_collection").(core.HandlerCollection).NewNode("core.user") + u.Name = "User ZZ" + data := u.Data.(*user.User) + data.Email = "test-admin@example.org" + data.Enabled = true + data.NewPassword = "admin" + data.Username = "test-admin" + data.Roles = []string{"ADMIN"} + + meta := u.Meta.(*user.UserMeta) + meta.PasswordCost = 1 // save test time + + manager.Save(u, false) + f(t, ts, app) state.Out <- 1