Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local storage implementation #3

Merged
merged 2 commits into from
Oct 26, 2023
Merged
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
7 changes: 7 additions & 0 deletions pkg/store/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package store

import "errors"

var (
ErrNotFound = errors.New("resource not found in store")
)
141 changes: 141 additions & 0 deletions pkg/store/local/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package local

import (
"archive/zip"
"io"
"os"
"path/filepath"
"sync"

"github.com/trisacrypto/courier/pkg/config"
"github.com/trisacrypto/courier/pkg/store"
)

const (
passwordPrefix = "pkcs12"
passwordFile = "pkcs12.password"
certificatePrefix = "certificate"
certificateFile = "certificate"
)

// Open the local storage backend.
func Open(conf config.LocalStorageConfig) (store *Store, err error) {
store = &Store{
path: conf.Path,
}

// Ensure the path exists
if err = os.MkdirAll(conf.Path, 0755); err != nil {
return nil, err
}

return store, nil
}

// Store implements the store.Store interface for local storage.
type Store struct {
sync.RWMutex
path string
}

var _ store.Store = &Store{}

// Close the local storage backend.
func (s *Store) Close() error {
return nil
}

//===========================================================================
// Password Methods
//===========================================================================

// GetPassword retrieves a password by id from the local storage backend.
func (s *Store) GetPassword(id string) (password []byte, err error) {
s.RLock()
defer s.RUnlock()
return s.load(s.fullPath(passwordPrefix, id))
}

// UpdatePassword updates a password by id in the local storage backend. If the
// password does not exist, it is created. Otherwise, it is overwritten.
func (s *Store) UpdatePassword(id string, password []byte) (err error) {
s.Lock()
defer s.Unlock()
return s.store(s.fullPath(passwordPrefix, id), passwordFile, password)
}

//===========================================================================
// Certificate Methods
//===========================================================================

// GetCertificate retrieves a certificate by id from the local storage backend.
func (s *Store) GetCertificate(name string) (cert []byte, err error) {
s.RLock()
defer s.RUnlock()
return s.load(s.fullPath(certificatePrefix, name))
}

// UpdateCertificate updates a certificate in the local storage backend.
func (s *Store) UpdateCertificate(name string, cert []byte) (err error) {
s.Lock()
defer s.Unlock()
return s.store(s.fullPath(certificatePrefix, name), certificateFile, cert)
}

//===========================================================================
// Helper methods
//===========================================================================

// fullPath returns the full path to an archive file in the local storage backend.
func (s *Store) fullPath(prefix, name string) string {
return filepath.Join(s.path, prefix+"-"+name+".zip")
}

// load returns file data by archive path from the local storage
func (s *Store) load(path string) (data []byte, err error) {
var archive *zip.ReadCloser
if archive, err = zip.OpenReader(path); err != nil {
if os.IsNotExist(err) {
return nil, store.ErrNotFound
}
return nil, err
}
defer archive.Close()

// Load the file from the archive
if len(archive.File) == 0 {
return nil, store.ErrNotFound
}

var reader io.ReadCloser
if reader, err = archive.File[0].Open(); err != nil {
return nil, err
}
defer reader.Close()

return io.ReadAll(reader)
}

// store saves file data to an archive and file name in the local storage
func (s *Store) store(path, name string, data []byte) (err error) {
var archive *os.File
if archive, err = os.Create(path); err != nil {
return err
}
defer archive.Close()

// Write the file to the archive
zipWriter := zip.NewWriter(archive)
defer zipWriter.Close()

var writer io.Writer
if writer, err = zipWriter.Create(name); err != nil {
return err
}

if _, err = writer.Write(data); err != nil {
return err
}

return nil
}
74 changes: 74 additions & 0 deletions pkg/store/local/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package local_test

import (
"os"
"testing"

"github.com/stretchr/testify/suite"
"github.com/trisacrypto/courier/pkg/config"
"github.com/trisacrypto/courier/pkg/store"
"github.com/trisacrypto/courier/pkg/store/local"
)

type localStoreTestSuite struct {
suite.Suite
conf config.LocalStorageConfig
store *local.Store
}

func (s *localStoreTestSuite) SetupSuite() {
// Open the storage backend in a temporary directory
var err error
path := s.T().TempDir()
s.store, err = local.Open(config.LocalStorageConfig{
Enabled: true,
Path: path,
})
s.NoError(err, "could not open local storage backend")
}

func (s *localStoreTestSuite) TearDownSuite() {
// Remove the temporary directory
s.NoError(s.store.Close(), "could not close local storage backend")
s.NoError(os.RemoveAll(s.conf.Path), "could not remove temporary directory")
}

func TestLocalStore(t *testing.T) {
suite.Run(t, new(localStoreTestSuite))
}

func (s *localStoreTestSuite) TestPasswordStore() {
require := s.Require()

// Try to get a password that does not exist
_, err := s.store.GetPassword("does-not-exist")
require.ErrorIs(err, store.ErrNotFound, "should return error if password does not exist")

// Create a password
password := []byte("password")
err = s.store.UpdatePassword("password_id", password)
require.NoError(err, "should be able to create a password")

// Get the password
actual, err := s.store.GetPassword("password_id")
require.NoError(err, "should be able to get a password")
require.Equal(password, actual, "wrong password returned")
}

func (s *localStoreTestSuite) TestCertificateStore() {
require := s.Require()

// Try to get a certificate that does not exist
_, err := s.store.GetCertificate("does-not-exist")
require.ErrorIs(err, store.ErrNotFound, "should return error if certificate does not exist")

// Create a certificate
cert := []byte("certificate")
err = s.store.UpdateCertificate("certificate_id", cert)
require.NoError(err, "should be able to create a certificate")

// Get the certificate
actual, err := s.store.GetCertificate("certificate_id")
require.NoError(err, "should be able to get a certificate")
require.Equal(cert, actual, "wrong certificate returned")
}
20 changes: 20 additions & 0 deletions pkg/store/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package store

// Store is a generic interface for storing and retrieving data.
type Store interface {
Close() error
PasswordStore
CertificateStore
}

// PasswordStore is a generic interface for storing and retrieving passwords.
type PasswordStore interface {
GetPassword(name string) ([]byte, error)
UpdatePassword(name string, password []byte) error
}

// CertificateStore is a generic interface for storing and retrieving certificates.
type CertificateStore interface {
GetCertificate(name string) ([]byte, error)
UpdateCertificate(name string, cert []byte) error
}