Skip to content

Commit

Permalink
only allow state push with correct lock id
Browse files Browse the repository at this point in the history
  • Loading branch information
tobikris committed Oct 8, 2022
1 parent e50f8d0 commit 9fa6484
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 31 deletions.
13 changes: 13 additions & 0 deletions pkg/lock/local/local.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package local

import (
"fmt"
"sync"

"github.com/nimbolus/terraform-backend/pkg/terraform"
Expand Down Expand Up @@ -63,3 +64,15 @@ func (l *Lock) Unlock(s *terraform.State) (bool, error) {

return true, nil
}

func (l *Lock) GetLock(s *terraform.State) ([]byte, error) {
l.mutex.Lock()
defer l.mutex.Unlock()

lock, ok := l.db[s.ID]
if !ok {
return nil, fmt.Errorf("no lock found for state %s", s.ID)
}

return lock, nil
}
1 change: 1 addition & 0 deletions pkg/lock/locker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ type Locker interface {
GetName() string
Lock(s *terraform.State) (ok bool, err error)
Unlock(s *terraform.State) (ok bool, err error)
GetLock(s *terraform.State) ([]byte, error)
}
13 changes: 13 additions & 0 deletions pkg/lock/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,16 @@ func (l *Lock) Unlock(s *terraform.State) (bool, error) {

return true, nil
}

func (l *Lock) GetLock(s *terraform.State) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

var lock []byte

if err := l.db.QueryRowContext(ctx, `SELECT lock_data FROM `+l.table+` WHERE state_id = $1`, s.ID).Scan(&lock); err != nil {
return nil, err
}

return lock, nil
}
26 changes: 26 additions & 0 deletions pkg/lock/redis/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,32 @@ func (r *Lock) Unlock(s *terraform.State) (unlocked bool, err error) {
return true, nil
}

func (r *Lock) GetLock(s *terraform.State) (lock []byte, err error) {
mutex := r.client.NewMutex(lockKey, redsync.WithExpiry(12*time.Hour), redsync.WithTries(1), redsync.WithGenValueFunc(func() (string, error) {
return uuid.New().String(), nil
}))

// lock the global redis mutex
if err := mutex.Lock(); err != nil {
log.Errorf("failed to lock redsync mutex: %v", err)

return nil, err
}

defer func() {
// unlock the global redis mutex
if _, mutErr := mutex.Unlock(); mutErr != nil {
log.Errorf("failed to unlock redsync mutex: %v", mutErr)

if err != nil {
err = multierr.Append(err, mutErr)
}
}
}()

return r.getLock(s)
}

func (r *Lock) setLock(s *terraform.State) error {
ctx := context.Background()

Expand Down
6 changes: 6 additions & 0 deletions pkg/lock/util/locktest.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ func LockTest(t *testing.T, l lock.Locker) {
t.Error(err)
}

if lock, err := l.GetLock(&s1); err != nil {
t.Error(err)
} else if string(lock) != string(s1.Lock) {
t.Errorf("lock is not equal: %s != %s", lock, s1.Lock)
}

if locked, err := l.Lock(&s1); err != nil || !locked {
t.Error("should be able to lock twice from the same process")
}
Expand Down
20 changes: 18 additions & 2 deletions pkg/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"io"
"net/http"
"strings"

"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -63,7 +64,7 @@ func StateHandler(store storage.Storage, locker lock.Locker, kms kms.KMS) func(h
case http.MethodGet:
Get(w, state, store, kms)
case http.MethodPost:
Post(w, state, body, store, kms)
Post(req, w, state, body, locker, store, kms)
case http.MethodDelete:
Delete(w, state, store)
default:
Expand Down Expand Up @@ -128,7 +129,22 @@ func Get(w http.ResponseWriter, state *terraform.State, store storage.Storage, k
HTTPResponse(w, http.StatusOK, string(state.Data))
}

func Post(w http.ResponseWriter, state *terraform.State, body []byte, store storage.Storage, kms kms.KMS) {
func Post(r *http.Request, w http.ResponseWriter, state *terraform.State, body []byte, locker lock.Locker, store storage.Storage, kms kms.KMS) {
reqLockID := r.URL.Query().Get("ID")

lockID, err := locker.GetLock(state)
if err != nil {
log.Warnf("failed to get lock for state with id %s: %v", state.ID, err)
HTTPResponse(w, http.StatusInternalServerError, "")
return
}

if !strings.Contains(string(lockID), fmt.Sprintf(`"ID":"%s"`, reqLockID)) {
log.Warnf("attempting to write state with wrong lock %s (expected %s)", reqLockID, lockID)
HTTPResponse(w, http.StatusBadRequest, "")
return
}

log.Debugf("save state with id %s", state.ID)

data, err := kms.Encrypt(body)
Expand Down
88 changes: 59 additions & 29 deletions pkg/server/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,83 @@ import (

"github.com/gorilla/mux"
"github.com/gruntwork-io/terratest/modules/terraform"

localkms "github.com/nimbolus/terraform-backend/pkg/kms/local"
locallock "github.com/nimbolus/terraform-backend/pkg/lock/local"
"github.com/nimbolus/terraform-backend/pkg/storage/filesystem"
)

var terraformBinary = flag.String("tf", "terraform", "terraform binary")

func TestServerHandler(t *testing.T) {
s := httptest.NewServer(NewStateHandler())
defer s.Close()

address, err := url.JoinPath(s.URL, "/state/project1/example")
func NewStateHandler(t *testing.T) http.Handler {
store, err := filesystem.NewFileSystemStorage(filepath.Join("./handler_test", "storage"))
if err != nil {
t.Fatal(err)
}

terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
locker := locallock.NewLock()

key := "x8DiIkAKRQT7cF55NQLkAZk637W3bGVOUjGeMX5ZGXY="
kms, _ := localkms.NewKMS(key)

r := mux.NewRouter().StrictSlash(true)
r.HandleFunc("/state/{project}/{name}", StateHandler(store, locker, kms))

return r
}

var terraformBinary = flag.String("tf", "terraform", "terraform binary")

func terraformOptions(t *testing.T, addr string) *terraform.Options {
return terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "./handler_test",
TerraformBinary: *terraformBinary,
Vars: map[string]interface{}{},
Reconfigure: true,
BackendConfig: map[string]interface{}{
"address": address,
"lock_address": address,
"unlock_address": address,
"address": addr,
"lock_address": addr,
"unlock_address": addr,
"username": "basic",
"password": "some-random-secret",
},
Lock: true,
LockTimeout: "200ms",
Lock: true,
})
}

func TestServerHandler_VerifyLockOnPush(t *testing.T) {
s := httptest.NewServer(NewStateHandler(t))
defer s.Close()

address, err := url.JoinPath(s.URL, "/state/project1/example")
if err != nil {
t.Fatal(err)
}

simulateLock(t, address, true)

for _, doLock := range []bool{true, false} {
terraformOptions := terraformOptions(t, address)
terraformOptions.Lock = doLock

_, err = terraform.InitAndApplyE(t, terraformOptions)
if err == nil {
t.Fatal("expected error")
}

simulateLock(t, address, false)
}
}

func TestServerHandler(t *testing.T) {
s := httptest.NewServer(NewStateHandler(t))
defer s.Close()

address, err := url.JoinPath(s.URL, "/state/project1/example")
if err != nil {
t.Fatal(err)
}

terraformOptions := terraformOptions(t, address)

// Clean up resources with "terraform destroy" at the end of the test.
defer terraform.Destroy(t, terraformOptions)
Expand Down Expand Up @@ -91,20 +138,3 @@ func simulateLock(t *testing.T, address string, lock bool) {
t.Fatal(err)
}
}

func NewStateHandler() http.Handler {
store, err := filesystem.NewFileSystemStorage(filepath.Join("./handler_test", "storage"))
if err != nil {
panic(err)
}

locker := locallock.NewLock()

key := "x8DiIkAKRQT7cF55NQLkAZk637W3bGVOUjGeMX5ZGXY="
kms, _ := localkms.NewKMS(key)

r := mux.NewRouter().StrictSlash(true)
r.HandleFunc("/state/{project}/{name}", StateHandler(store, locker, kms))

return r
}

0 comments on commit 9fa6484

Please sign in to comment.