Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: golangci-lint
on:
push:
branches:
- master
- main
pull_request:

permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
# pull-requests: read

jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.20'
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.53
# By default we have below linters set up:
# errcheck - checking for any unchecked errors in our code
# gosimple - checking if the code can be simplified
# govet - reports suspicious constructs
# ineffassign - detects when assignments to variables are not used
# staticcheck - swiss-army knife, checks for bugs, performance issues and more
# unused - checks for unused constants, variables, functions and types
48 changes: 48 additions & 0 deletions demo5/data/note_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package data

import (
"github.com/addetz/secure-code-go/demo4/db"
"github.com/google/uuid"
)

type SecretNote struct {
ID string `json:"id"`
Username string `json:"username"`
Text string `json:"text"`
}

// SecretNoteService maintains the user notes.
type SecretNoteService struct {
dbService db.DatabaseService
}

// NewSecretNoteService creates a SecretNoteService that is ready to use.
func NewSecretNoteService(dbService db.DatabaseService) *SecretNoteService {
return &SecretNoteService{
dbService: dbService,
}
}

// Add adds a new SecretNote for the given user by using the SecretNoteService.
func (ns *SecretNoteService) Add(user string, n SecretNote) error {
id := uuid.New().String()
return ns.dbService.AddNote(id, user, n.Text)
}

// Get returns all the SecretNotes of a given user by using the SecretNoteService.
func (ns *SecretNoteService) GetAll(user string) ([]SecretNote, error) {
dbNotes, err := ns.dbService.GetUserNotes(user)
if err != nil {
return nil, err
}
var notes []SecretNote
for _, n := range dbNotes {
notes = append(notes, SecretNote{
ID: n.ID,
Username: n.Username,
Text: n.Text,
})
}

return notes, nil
}
66 changes: 66 additions & 0 deletions demo5/data/note_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package data_test

import (
"errors"
"testing"

"github.com/addetz/secure-code-go/demo4/data"
"github.com/addetz/secure-code-go/demo4/db"
"github.com/addetz/secure-code-go/demo4/mocks"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestAddNote(t *testing.T) {
mockDB := new(mocks.DatabaseServiceMock)
notes := data.NewSecretNoteService(mockDB)
user := "user1"
note := data.SecretNote{
Text: "My Secret Note",
}
mockDB.On("AddNote", mock.AnythingOfType("string"), user, note.Text).Return(nil)
err := notes.Add(user, note)
assert.Nil(t, err)
}

func TestGetAllNotes(t *testing.T) {
t.Run("no notes found", func(t *testing.T) {
user := "user1"
mockDB := new(mocks.DatabaseServiceMock)
noteService := data.NewSecretNoteService(mockDB)
mockDB.On("GetUserNotes", user).Return(nil, errors.New("no notes found"))
notes, err := noteService.GetAll(user)
assert.Nil(t, notes)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "no notes found")
})
t.Run("notes found", func(t *testing.T) {
user := "user1"
dbNotes := []db.Note{
{
ID: uuid.New().String(),
Username: user,
Text: "My first note",
},
{
ID: uuid.New().String(),
Username: user,
Text: "My second note",
},
}
mockDB := new(mocks.DatabaseServiceMock)
noteService := data.NewSecretNoteService(mockDB)
mockDB.On("GetUserNotes", user).Return(dbNotes, nil)
notes, err := noteService.GetAll(user)
assert.Nil(t, err)
assert.NotNil(t, notes)
assert.Equal(t, len(dbNotes), len(notes))
for i, n := range dbNotes {
assert.Equal(t, n.ID, notes[i].ID)
assert.Equal(t, n.Username, notes[i].Username)
assert.Equal(t, n.Text, notes[i].Text)

}
})
}
62 changes: 62 additions & 0 deletions demo5/data/user_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package data

import (
"github.com/addetz/secure-code-go/demo4/db"
"github.com/pkg/errors"

passwordvalidator "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/bcrypt"
)

const minEntropyBits = 60

type User struct {
Username, Password string
}

// UserService holds
type UserService struct {
dbService db.DatabaseService
}

// NewUserService creates a ready to use user service.
func NewUserService(dbService db.DatabaseService) *UserService {
return &UserService{
dbService: dbService,
}
}

// Add validates a user password and creates a new user.
func (us *UserService) Add(name, password string) error {
if _, err := us.dbService.GetUser(name); err == nil {
return errors.New("user exists already, please log in instead")
}

err := passwordvalidator.Validate(password, minEntropyBits)
if err != nil {
return errors.Wrap(err, "validate new user password")
}

hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
return us.dbService.AddUser(name, string(hashedPassword))
}

// ValidatePassword checks the provided password of an existing user.
func (us *UserService) ValidatePassword(name, providedPwd string) error {
user, err := us.dbService.GetUser(name)
if err != nil {
return errors.Wrap(err, "user does not exist")
}
return bcrypt.CompareHashAndPassword([]byte(user.Pwd), []byte(providedPwd))
}

// ValidateUser checks the provided username belongs to an existing user.
func (us *UserService) ValidateUser(name string) error {
if _, err := us.dbService.GetUser(name); err != nil {
return errors.New("user not found")
}
return nil
}
84 changes: 84 additions & 0 deletions demo5/data/user_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package data_test

import (
"errors"
"testing"

"github.com/addetz/secure-code-go/demo4/data"
"github.com/addetz/secure-code-go/demo4/db"
"github.com/addetz/secure-code-go/demo4/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"golang.org/x/crypto/bcrypt"
)

func TestAdd(t *testing.T) {
t.Run("insufficient password", func(t *testing.T) {
name := "user1"
mockDB := new(mocks.DatabaseServiceMock)
us := data.NewUserService(mockDB)
mockDB.On("GetUser", name).Return(nil, errors.New("no user exists"))
err := us.Add(name, "test")
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "validate new user password: insecure password")
})
t.Run("successful add", func(t *testing.T) {
name := "user1"
password := "test-horse-pen-clam"
mockDB := new(mocks.DatabaseServiceMock)
us := data.NewUserService(mockDB)
mockDB.On("GetUser", name).Return(nil, errors.New("no user exists"))
mockDB.On("AddUser", name, mock.AnythingOfType("string")).Return(nil)
err := us.Add(name, password)
assert.Nil(t, err)
})
t.Run("duplicate user", func(t *testing.T) {
name := "user1"
password := "test-horse-pen-clam"
mockDB := new(mocks.DatabaseServiceMock)
us := data.NewUserService(mockDB)
mockDB.On("GetUser", name).Return(nil, nil)
err := us.Add(name, password)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "user exists already")
})
}

func TestValidate(t *testing.T) {
t.Run("successful validate", func(t *testing.T) {
name := "user1"
password := "test-horse-pen-clam"
expected, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
mockDB := new(mocks.DatabaseServiceMock)
us := data.NewUserService(mockDB)
mockDB.On("GetUser", name).Return(&db.User{
Username: name,
Pwd: string(expected),
}, nil)
err := us.ValidatePassword(name, password)
assert.Nil(t, err)
})
t.Run("failed validate", func(t *testing.T) {
name := "user1"
password := "test-horse-pen-clam"
expected, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
mockDB := new(mocks.DatabaseServiceMock)
mockDB.On("GetUser", name).Return(&db.User{
Username: name,
Pwd: string(expected),
}, nil)
us := data.NewUserService(mockDB)
err := us.ValidatePassword(name, "garbage-password")
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "hashedPassword is not the hash of the given password")
})
t.Run("inexistent user", func(t *testing.T) {
name := "user1"
mockDB := new(mocks.DatabaseServiceMock)
us := data.NewUserService(mockDB)
mockDB.On("GetUser", name).Return(nil, errors.New("no user exists"))
err := us.ValidatePassword(name, "garbage-password")
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "user does not exist")
})
}
99 changes: 99 additions & 0 deletions demo5/db/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package db

import (
"database/sql"
"log"
)

type User struct {
Username, Pwd string
}

type Note struct {
ID, Username, Text string
}

type dbService struct {
db *sql.DB
}

type DatabaseService interface {
AddUser(username, pwd string) error
GetUser(username string) (*User, error)
AddNote(id, username, text string) error
GetUserNotes(username string) ([]Note, error)
}

// NewDatabaseService initialises a DatabaseService given its dependencies.
func NewDatabaseService(db *sql.DB) *dbService {
return &dbService{
db: db,
}
}

// AddUser creates a new user in the DB
func (ds *dbService) AddUser(username, pwd string) error {
stmt, err := ds.db.Prepare("INSERT INTO users (username, pwd) VALUES( $1, $2 )")
if err != nil {
log.Println("error1", err)
return err
}
defer stmt.Close()
if _, err := stmt.Exec(username, pwd); err != nil {
log.Println("error2", err)
return err
}
return nil
}

// GetUser returns a user from the database or an error if none exists.
func (ds *dbService) GetUser(username string) (*User, error) {
var user User
stmt, err := ds.db.Prepare("SELECT * FROM users WHERE username = $1 ")
if err != nil {
log.Println("error3", err)
return nil, err
}
defer stmt.Close()
if err := stmt.QueryRow(username).Scan(&user.Username, &user.Pwd); err != nil {
log.Println("error4", err)
return nil, err
}
return &user, nil
}

// AddNote creates a new note in the DB
func (ds *dbService) AddNote(id, username, text string) error {
stmt, err := ds.db.Prepare("INSERT INTO notes(id, username, noteText) VALUES($1, $2, $3)")
if err != nil {
return err
}
defer stmt.Close()
if _, err := stmt.Exec(id, username, text); err != nil {
return err
}
return nil
}

// GetUserNotes returns all the notes of a given user from the database or an error.
func (ds *dbService) GetUserNotes(username string) ([]Note, error) {
var notes []Note
stmt, err := ds.db.Prepare("SELECT * FROM notes WHERE username = $1")
if err != nil {
return nil, err
}
defer stmt.Close()
rows, err := stmt.Query(username)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
n := Note{}
if err := rows.Scan(&n.ID, &n.Username, &n.Text); err != nil {
return nil, err
}
notes = append(notes, n)
}
return notes, nil
}
Loading