3
0

Basic credentials set/check

This commit is contained in:
Denis Arh
2019-04-04 01:41:14 +02:00
parent 68538e93a2
commit d18473568e
6 changed files with 260 additions and 12 deletions
+12
View File
@@ -99,3 +99,15 @@ func (m *MockResourcesInterface) Delete(roleID uint64) error {
func (mr *MockResourcesInterfaceMockRecorder) Delete(roleID interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockResourcesInterface)(nil).Delete), roleID)
}
// Reset mocks base method
func (m *MockResourcesInterface) Reset(rules []Rule) error {
ret := m.ctrl.Call(m, "Reset", rules)
ret0, _ := ret[0].(error)
return ret0
}
// Reset indicates an expected call of Reset
func (mr *MockResourcesInterfaceMockRecorder) Reset(rules interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockResourcesInterface)(nil).Reset), rules)
}
+46 -5
View File
@@ -3,12 +3,15 @@ package cli
import (
"context"
"fmt"
"os"
"syscall"
"github.com/davecgh/go-spew/spew"
"github.com/spf13/cobra"
"github.com/titpetric/factory"
"golang.org/x/crypto/ssh/terminal"
"github.com/crusttech/crust/system/internal/repository"
"github.com/crusttech/crust/system/internal/service"
"github.com/crusttech/crust/system/types"
)
@@ -20,7 +23,7 @@ func usersCmd(ctx context.Context, db *factory.DB) *cobra.Command {
}
// List users.
cmdUsersList := &cobra.Command{
listCmd := &cobra.Command{
Use: "list",
Short: "List users",
Run: func(cmd *cobra.Command, args []string) {
@@ -31,8 +34,7 @@ func usersCmd(ctx context.Context, db *factory.DB) *cobra.Command {
users, err := userRepo.Find(uf)
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
exit(cmd, err)
}
fmt.Println(" Created Updated Email")
@@ -54,7 +56,46 @@ func usersCmd(ctx context.Context, db *factory.DB) *cobra.Command {
},
}
cmd.AddCommand(cmdUsersList)
addCmd := &cobra.Command{
Use: "add [email]",
Short: "Add new user",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var (
userRepo = repository.User(ctx, db)
authSvc = service.Auth(ctx)
// @todo email validation
user = &types.User{Email: args[0]}
err error
password []byte
)
if user, err = userRepo.Create(user); err != nil {
exit(cmd, err)
}
cmd.Printf("User created [%d].\n", user.ID)
cmd.Print("Set password password: ")
if password, err = terminal.ReadPassword(syscall.Stdin); err != nil {
exit(cmd, err)
}
if len(password) == 0 {
// Password not set, that's ok too.
exit(cmd, nil)
}
spew.Dump(password)
if err = authSvc.ChangePassword(user, password); err != nil {
exit(cmd, err)
}
},
}
cmd.AddCommand(listCmd, addCmd)
return cmd
}
@@ -23,6 +23,7 @@ type (
Create(c *types.Credentials) (*types.Credentials, error)
Update(c *types.Credentials) (*types.Credentials, error)
DeleteByID(id uint64) error
DeleteByKind(ownerID uint64, kind string) (err error)
}
credentials struct {
@@ -105,3 +106,10 @@ func (r *credentials) Update(c *types.Credentials) (*types.Credentials, error) {
func (r *credentials) DeleteByID(id uint64) error {
return r.updateColumnByID(r.tblname, "deleted_at", time.Now(), id)
}
func (r *credentials) DeleteByKind(ownerID uint64, kind string) (err error) {
return exec(r.db().Exec(
fmt.Sprintf("UPDATE %s SET deleted_at = NOW() WHERE rel_owner = ? AND kind = ?", r.tblname),
ownerID,
kind))
}
@@ -150,3 +150,15 @@ func (m *MockCredentialsRepository) DeleteByID(id uint64) error {
func (mr *MockCredentialsRepositoryMockRecorder) DeleteByID(id interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByID", reflect.TypeOf((*MockCredentialsRepository)(nil).DeleteByID), id)
}
// DeleteByKind mocks base method
func (m *MockCredentialsRepository) DeleteByKind(ownerID uint64, kind string) error {
ret := m.ctrl.Call(m, "DeleteByKind", ownerID, kind)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteByKind indicates an expected call of DeleteByKind
func (mr *MockCredentialsRepositoryMockRecorder) DeleteByKind(ownerID, kind interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByKind", reflect.TypeOf((*MockCredentialsRepository)(nil).DeleteByKind), ownerID, kind)
}
+99 -6
View File
@@ -3,10 +3,12 @@ package service
import (
"context"
"log"
"regexp"
"time"
"github.com/markbates/goth"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"github.com/crusttech/crust/system/internal/repository"
"github.com/crusttech/crust/system/types"
@@ -29,13 +31,21 @@ type (
External(profile goth.User) (*types.User, error)
CheckPassword(username, password string) (*types.User, error)
ChangePassword(user *types.User, password string) error
CheckPassword(email string, password []byte) (*types.User, error)
ChangePassword(user *types.User, password []byte) error
CheckCredentials(credentialsID uint64, secret string) (*types.User, error)
RevokeCredentialsByID(user *types.User, credentialsID uint64) error
}
)
const (
CredentialsTypePassword = "password"
)
var (
reEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
)
func defaultProviderValidator(provider string) error {
_, err := goth.GetProvider(provider)
return err
@@ -203,15 +213,98 @@ func (svc *auth) External(profile goth.User) (u *types.User, err error) {
// CheckPassword verifies username/password combination
//
// Expects plain text password as an input
func (svc *auth) CheckPassword(username, password string) (*types.User, error) {
panic("svc.auth.CheckPassword, not implemented")
func (svc *auth) CheckPassword(email string, password []byte) (u *types.User, err error) {
if err = svc.validateCredentials(email, password); err != nil {
return
}
return u, svc.db.Transaction(func() error {
var (
cc types.CredentialsSet
)
u, err = svc.users.FindByEmail(email)
if err != repository.ErrUserNotFound {
return errors.New("invalid username/password combination")
}
if err != nil {
return errors.Wrap(err, "could not check password")
}
cc, err := svc.credentials.FindByKind(u.ID, CredentialsTypePassword)
if err != nil {
return errors.Wrap(err, "could not find credentials")
}
return svc.checkPassword(password, cc)
})
}
// validateCredentials does basic format & length check
func (svc auth) validateCredentials(email string, password []byte) error {
if !reEmail.MatchString(email) {
return errors.New("invalid email format")
}
if len(password) == 0 {
return errors.New("empty password")
}
return nil
}
func (svc auth) checkPassword(password []byte, cc types.CredentialsSet) (err error) {
// We need only valid credentials (skip deleted, expired)
cc, _ = cc.Filter(func(c *types.Credentials) (b bool, e error) {
return c.Valid(), nil
})
for _, c := range cc {
if len(c.Credentials) == 0 {
continue
}
err = bcrypt.CompareHashAndPassword([]byte(c.Credentials), password)
if err == bcrypt.ErrMismatchedHashAndPassword {
// Mismatch, continue with the checking
continue
} else if err != nil {
// Some other error
return errors.Wrap(err, "could not compare passwords")
} else {
// Password matched one of credentials
return nil
}
}
return errors.New("invalid username/password combination")
}
// ChangePassword (soft) deletes old password entry and creates a new one
//
// Expects plain text password as an input
func (svc *auth) ChangePassword(user *types.User, password string) error {
panic("svc.auth.ChangePassword, not implemented")
func (svc *auth) ChangePassword(user *types.User, password []byte) (err error) {
var hash []byte
hash, err = bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return errors.Wrap(err, "could not hash password")
}
return svc.db.Transaction(func() error {
if err = svc.credentials.DeleteByKind(user.ID, CredentialsTypePassword); err != nil {
return errors.Wrap(err, "could not remove old passswords")
}
_, err = svc.credentials.Create(&types.Credentials{
OwnerID: user.ID,
Kind: CredentialsTypePassword,
Credentials: string(hash),
})
return errors.Wrap(err, "could not create new password")
})
}
// CheckCredentials searches for credentials/secret combination and returns loaded user if successful
+83 -1
View File
@@ -1,4 +1,4 @@
// +build unit
// +-build unit
package service
@@ -8,6 +8,7 @@ import (
"github.com/golang/mock/gomock"
"github.com/markbates/goth"
"golang.org/x/crypto/bcrypt"
"github.com/crusttech/crust/internal/test"
"github.com/crusttech/crust/system/internal/repository"
@@ -105,3 +106,84 @@ func TestAuth_External_NonExisting(t *testing.T) {
test.Assert(t, auser.ID == u.ID, "Did not receive expected user")
}
}
func Test_auth_validateCredentials(t *testing.T) {
type args struct {
email string
password []byte
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "no email", args: args{"", []byte("")}, wantErr: true},
{name: "bad email", args: args{"test", []byte("")}, wantErr: true},
{name: "no pass", args: args{"test@domain.tld", []byte("")}, wantErr: true},
{name: "all good", args: args{"test@domain.tld", []byte("password")}, wantErr: false},
}
svc := auth{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := svc.validateCredentials(tt.args.email, tt.args.password); (err != nil) != tt.wantErr {
t.Errorf("auth.validateCredentials() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_auth_checkPassword(t *testing.T) {
plainPassword := []byte(" ... plain password ... ")
hashedPassword, _ := bcrypt.GenerateFromPassword(plainPassword, bcrypt.DefaultCost)
type args struct {
password []byte
cc types.CredentialsSet
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty set",
wantErr: true,
args: args{}},
{
name: "bad pwd",
wantErr: true,
args: args{
password: []byte(" foo "),
cc: types.CredentialsSet{&types.Credentials{ID: 1, Credentials: string(hashedPassword)}}}},
{
name: "invalid credentials",
wantErr: true,
args: args{
password: []byte(" foo "),
cc: types.CredentialsSet{&types.Credentials{ID: 0, Credentials: string(hashedPassword)}}}},
{
name: "ok",
wantErr: false,
args: args{
password: plainPassword,
cc: types.CredentialsSet{&types.Credentials{ID: 1, Credentials: string(hashedPassword)}}}},
{
name: "multipass",
wantErr: false,
args: args{
password: plainPassword,
cc: types.CredentialsSet{
&types.Credentials{ID: 0, Credentials: string(hashedPassword)},
&types.Credentials{ID: 1, Credentials: "$2a$10$8sOZxfZinxnu3bAtpkqEx.wBBwOfci6aG1szgUyxm5.BL2WiLu.ni"},
&types.Credentials{ID: 2, Credentials: string(hashedPassword)},
&types.Credentials{ID: 3, Credentials: ""},
}}},
}
svc := auth{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := svc.checkPassword(tt.args.password, tt.args.cc); (err != nil) != tt.wantErr {
t.Errorf("auth.checkPassword() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}