diff --git a/.gitignore b/.gitignore index 07e5e6e1..a41bb9bb 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ yarn-error.log selenium-debug.log test/unit/coverage test/e2e/reports + +# data folder generated by gaia +data/ diff --git a/cmd/gaia/main.go b/cmd/gaia/main.go index da4ffe23..07053540 100644 --- a/cmd/gaia/main.go +++ b/cmd/gaia/main.go @@ -2,7 +2,9 @@ package main import ( "flag" + "os" + hclog "github.com/hashicorp/go-hclog" "github.com/kataras/iris" "github.com/michelvocks/gaia" "github.com/michelvocks/gaia/handlers" @@ -19,6 +21,7 @@ func init() { // command line arguments flag.StringVar(&cfg.ListenPort, "port", "8080", "Listen port for gaia") + flag.StringVar(&cfg.DataPath, "datapath", "data", "Path to the data folder") flag.StringVar(&cfg.Bolt.Path, "dbpath", "gaia.db", "Path to gaia bolt db file") // Default values @@ -29,6 +32,13 @@ func main() { // Parse command line flgs flag.Parse() + // Initialize shared logger + cfg.Logger = hclog.New(&hclog.LoggerOptions{ + Level: hclog.Trace, + Output: hclog.DefaultOutput, + Name: "Gaia", + }) + // Initialize IRIS irisInstance = iris.New() @@ -36,11 +46,16 @@ func main() { s := store.NewStore() err := s.Init(cfg) if err != nil { - panic(err) + cfg.Logger.Error("cannot initialize store", "error", err.Error()) + os.Exit(1) } // Initialize handlers - handlers.InitHandlers(irisInstance, s) + err = handlers.InitHandlers(cfg, irisInstance, s) + if err != nil { + cfg.Logger.Error("cannot initialize handlers", "error", err.Error()) + os.Exit(1) + } // Start listen irisInstance.Run(iris.Addr(":" + cfg.ListenPort)) diff --git a/gaia.go b/gaia.go index 514d7487..e7696faa 100644 --- a/gaia.go +++ b/gaia.go @@ -2,6 +2,8 @@ package gaia import ( "os" + + hclog "github.com/hashicorp/go-hclog" ) // User is the user object @@ -16,6 +18,8 @@ type User struct { // Config holds all config options type Config struct { ListenPort string + DataPath string + Logger hclog.Logger Bolt struct { Path string diff --git a/handlers/User.go b/handlers/User.go index 4ab95cdd..c8ab6cc8 100644 --- a/handlers/User.go +++ b/handlers/User.go @@ -1,7 +1,6 @@ package handlers import ( - "log" "time" jwt "github.com/dgrijalva/jwt-go" @@ -31,7 +30,7 @@ func UserLogin(ctx iris.Context) { // Authenticate user user, err := storeService.UserAuth(u) if err != nil { - log.Printf("error during UserAuth: %s", err) + cfg.Logger.Error("error during UserAuth: %s", err) ctx.StatusCode(iris.StatusInternalServerError) return } @@ -55,11 +54,10 @@ func UserLogin(ctx iris.Context) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Sign and get encoded token - b := []byte{'f', '2', 'f', 'f', 's', 'h', 's'} - tokenstring, err := token.SignedString(b) + tokenstring, err := token.SignedString(jwtKey) if err != nil { ctx.StatusCode(iris.StatusInternalServerError) - log.Printf("Error signing jwt token: %s", err) + cfg.Logger.Error("error signing jwt token: %s", err) return } user.JwtExpiry = claims.ExpiresAt diff --git a/handlers/handler.go b/handlers/handler.go index 9a4ad62b..8149583e 100644 --- a/handlers/handler.go +++ b/handlers/handler.go @@ -1,7 +1,10 @@ package handlers import ( + "crypto/rand" + "github.com/kataras/iris" + "github.com/michelvocks/gaia" "github.com/michelvocks/gaia/store" ) @@ -13,13 +16,32 @@ const ( // Use this to talk to the store. var storeService *store.Store +// cfg is a pointer to the global config +var cfg *gaia.Config + +// jwtKey is a random generated key for jwt signing +var jwtKey []byte + // InitHandlers initializes(registers) all handlers -func InitHandlers(i *iris.Application, s *store.Store) { +func InitHandlers(c *gaia.Config, i *iris.Application, s *store.Store) error { + // Set config + cfg = c + // Set store instance storeService = s + // Generate signing key for jwt + jwtKey = make([]byte, 64) + _, err := rand.Read(jwtKey) + if err != nil { + return err + } + cfg.Logger.Info("jwt signing key generated", "key", jwtKey) + // Define prefix p := "/api/" + apiVersion + "/" i.Post(p+"users/login", UserLogin) + + return nil } diff --git a/store/store.go b/store/store.go index 5f767996..55deac25 100644 --- a/store/store.go +++ b/store/store.go @@ -3,9 +3,11 @@ package store import ( "encoding/json" "fmt" + "os" bolt "github.com/coreos/bbolt" "github.com/michelvocks/gaia" + "golang.org/x/crypto/bcrypt" ) var ( @@ -32,10 +34,18 @@ func NewStore() *Store { return s } -// UserUpdate takes the given user and saves it +// UserPut takes the given user and saves it // to the bolt database. User will be overwritten // if it already exists. -func (s *Store) UserUpdate(u *gaia.User) error { +// It also clears the password field afterwards. +func (s *Store) UserPut(u *gaia.User) error { + // Encrypt password before we save it + hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.MinCost) + if err != nil { + return err + } + u.Password = string(hash) + return s.db.Update(func(tx *bolt.Tx) error { // Get bucket b := tx.Bucket(userBucket) @@ -46,6 +56,9 @@ func (s *Store) UserUpdate(u *gaia.User) error { return err } + // Clear password from origin object + u.Password = "" + // Put user return b.Put([]byte(u.Username), m) }) @@ -65,7 +78,7 @@ func (s *Store) UserAuth(u *gaia.User) (*gaia.User, error) { } // Check if password is valid - if user.Password != u.Password { + if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password)); err != nil { return nil, nil } @@ -102,16 +115,32 @@ func (s *Store) UserGet(username string) (*gaia.User, error) { return user, err } -// Init initalizes the connection to the database. +// Init creates the data folder if not exists, +// generates private key and bolt database. // This should be called only once per database // because bolt holds a lock on the database file. func (s *Store) Init(cfg *gaia.Config) error { - db, err := bolt.Open(cfg.Bolt.Path, cfg.Bolt.Mode, nil) + // Make sure data folder exists + err := os.MkdirAll(cfg.DataPath, 0700) + if err != nil { + return err + } + + // Open connection to bolt database + path := cfg.DataPath + string(os.PathSeparator) + cfg.Bolt.Path + db, err := bolt.Open(path, cfg.Bolt.Mode, nil) if err != nil { return err } s.db = db + // Setup database + return setupDatabase(s) +} + +// setupDatabase create all buckets in the db. +// Additionally, it makes sure that the admin user exists. +func setupDatabase(s *Store) error { // Create bucket if not exists function var bucketName []byte c := func(tx *bolt.Tx) error { @@ -124,12 +153,12 @@ func (s *Store) Init(cfg *gaia.Config) error { // Make sure buckets exist bucketName = userBucket - err = db.Update(c) + err := s.db.Update(c) if err != nil { return err } bucketName = pipelineBucket - err = db.Update(c) + err = s.db.Update(c) if err != nil { return err } @@ -142,7 +171,7 @@ func (s *Store) Init(cfg *gaia.Config) error { // Create admin user if we cannot find it if admin == nil { - err = s.UserUpdate(&gaia.User{ + err = s.UserPut(&gaia.User{ DisplayName: adminUsername, Username: adminUsername, Password: adminPassword, diff --git a/store/store_test.go b/store/store_test.go index af0b6045..8f4c8c37 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -1,6 +1,7 @@ package store import ( + "fmt" "os" "testing" @@ -13,10 +14,19 @@ var config *gaia.Config func TestMain(m *testing.M) { store = NewStore() config = &gaia.Config{} + config.DataPath = "data" config.Bolt.Path = "test.db" config.Bolt.Mode = 0600 - os.Exit(m.Run()) + r := m.Run() + + // cleanup + err := os.Remove("data") + if err != nil { + fmt.Printf("cannot remove data folder: %s\n", err.Error()) + r = 1 + } + os.Exit(r) } func TestInit(t *testing.T) { @@ -26,7 +36,7 @@ func TestInit(t *testing.T) { } // cleanup - err = os.Remove("test.db") + err = os.Remove("data/test.db") if err != nil { t.Fatal(err) } @@ -42,7 +52,7 @@ func TestUserGet(t *testing.T) { u.Username = "testuser" u.Password = "12345!#+21+" u.DisplayName = "Test" - err = store.UserUpdate(u) + err = store.UserPut(u) if err != nil { t.Fatal(err) } @@ -64,13 +74,13 @@ func TestUserGet(t *testing.T) { } // cleanup - err = os.Remove("test.db") + err = os.Remove("data/test.db") if err != nil { t.Fatal(err) } } -func TestUserUpdate(t *testing.T) { +func TestUserPut(t *testing.T) { err := store.Init(config) if err != nil { t.Fatal(err) @@ -80,13 +90,13 @@ func TestUserUpdate(t *testing.T) { u.Username = "testuser" u.Password = "12345!#+21+" u.DisplayName = "Test" - err = store.UserUpdate(u) + err = store.UserPut(u) if err != nil { t.Fatal(err) } // cleanup - err = os.Remove("test.db") + err = os.Remove("data/test.db") if err != nil { t.Fatal(err) } @@ -102,12 +112,14 @@ func TestUserAuth(t *testing.T) { u.Username = "testuser" u.Password = "12345!#+21+" u.DisplayName = "Test" - err = store.UserUpdate(u) + err = store.UserPut(u) if err != nil { t.Fatal(err) return } + // Password field has been cleared after last UserPut + u.Password = "12345!#+21+" r, err := store.UserAuth(u) if err != nil { t.Fatal(err) @@ -140,7 +152,7 @@ func TestUserAuth(t *testing.T) { } // cleanup - err = os.Remove("test.db") + err = os.Remove("data/test.db") if err != nil { t.Fatal(err) }