store download & add key
This commit is contained in:
		@ -12,13 +12,15 @@ var (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func casCleaner() {
 | 
			
		||||
	for {
 | 
			
		||||
	for range time.Tick(*cacheCleanDelay) {
 | 
			
		||||
		if !wPublicState.Get().Store.Open {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := cleanCAS()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Print("warn: couldn't clean cache: ", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		time.Sleep(*cacheCleanDelay)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,4 +6,7 @@ import (
 | 
			
		||||
	"m.cluseau.fr/go/httperr"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ErrNotFound = httperr.NewStd(404, http.StatusNotFound, "not found")
 | 
			
		||||
var (
 | 
			
		||||
	ErrNotFound     = httperr.NewStd(404, http.StatusNotFound, "not found")
 | 
			
		||||
	ErrInvalidToken = httperr.NewStd(403, http.StatusForbidden, "invalid token")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ func main() {
 | 
			
		||||
		if autoUnlock != "" {
 | 
			
		||||
			log.Printf("auto-unlocking the store")
 | 
			
		||||
			err := unlockSecretStore([]byte(autoUnlock))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
			if err.Any() {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,12 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"encoding/base32"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
    "sort"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
@ -20,7 +18,8 @@ import (
 | 
			
		||||
 | 
			
		||||
var secStore *secretstore.Store
 | 
			
		||||
 | 
			
		||||
func secStorePath(name string) string { return filepath.Join(*dataDir, "secrets", name) }
 | 
			
		||||
func secStoreRoot() string            { return filepath.Join(*dataDir, "secrets") }
 | 
			
		||||
func secStorePath(name string) string { return filepath.Join(secStoreRoot(), name) }
 | 
			
		||||
func secKeysStorePath() string        { return secStorePath(".keys") }
 | 
			
		||||
 | 
			
		||||
func openSecretStore() {
 | 
			
		||||
@ -64,7 +63,7 @@ var (
 | 
			
		||||
	ErrInvalidPassphrase    = httperr.NewStd(http.StatusBadRequest, 2, "invalid passphrase")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func unlockSecretStore(passphrase []byte) *httperr.Error {
 | 
			
		||||
func unlockSecretStore(passphrase []byte) (err httperr.Error) {
 | 
			
		||||
	unlockMutex.Lock()
 | 
			
		||||
	defer unlockMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
@ -75,7 +74,7 @@ func unlockSecretStore(passphrase []byte) *httperr.Error {
 | 
			
		||||
	if secStore.IsNew() {
 | 
			
		||||
		err := secStore.Init(passphrase)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return httperr.New(http.StatusInternalServerError, err)
 | 
			
		||||
			return httperr.Internal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = secStore.SaveTo(secKeysStorePath())
 | 
			
		||||
@ -83,7 +82,7 @@ func unlockSecretStore(passphrase []byte) *httperr.Error {
 | 
			
		||||
			log.Print("secret store save error: ", err)
 | 
			
		||||
			secStore.Close()
 | 
			
		||||
 | 
			
		||||
			return httperr.New(http.StatusInternalServerError, err)
 | 
			
		||||
			return httperr.Internal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
@ -98,25 +97,21 @@ func unlockSecretStore(passphrase []byte) *httperr.Error {
 | 
			
		||||
			log.Print("failed to read admin token: ", err)
 | 
			
		||||
			secStore.Close()
 | 
			
		||||
 | 
			
		||||
			return httperr.New(http.StatusInternalServerError, err)
 | 
			
		||||
			return httperr.Internal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		randBytes := make([]byte, 32)
 | 
			
		||||
		_, err := rand.Read(randBytes)
 | 
			
		||||
		token, err = newToken(32)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Print("rand read error: ", err)
 | 
			
		||||
			secStore.Close()
 | 
			
		||||
 | 
			
		||||
			return httperr.New(http.StatusInternalServerError, err)
 | 
			
		||||
			return httperr.Internal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randBytes)
 | 
			
		||||
		err = writeSecret("admin-token", token)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Print("write error: ", err)
 | 
			
		||||
			secStore.Close()
 | 
			
		||||
 | 
			
		||||
			return httperr.New(http.StatusInternalServerError, err)
 | 
			
		||||
			return httperr.Internal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Print("wrote new admin token")
 | 
			
		||||
@ -124,6 +119,18 @@ func unlockSecretStore(passphrase []byte) *httperr.Error {
 | 
			
		||||
 | 
			
		||||
	*adminToken = token
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		token, err := newToken(16)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			secStore.Close()
 | 
			
		||||
			return httperr.Internal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		wState.Change(func(v *State) {
 | 
			
		||||
			v.Store.DownloadToken = token
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wPublicState.Change(func(v *PublicState) {
 | 
			
		||||
		v.Store.New = false
 | 
			
		||||
		v.Store.Open = true
 | 
			
		||||
@ -132,7 +139,7 @@ func unlockSecretStore(passphrase []byte) *httperr.Error {
 | 
			
		||||
	go updateState()
 | 
			
		||||
	go migrateSecrets()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func readSecret(name string, value any) (err error) {
 | 
			
		||||
@ -179,12 +186,12 @@ func writeSecret(name string, value any) (err error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = os.Rename(f.Name(), secStorePath(name+".data"))
 | 
			
		||||
    if err != nil {
 | 
			
		||||
    return
 | 
			
		||||
    }
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
    go updateState()
 | 
			
		||||
    return
 | 
			
		||||
	go updateState()
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var secL sync.Mutex
 | 
			
		||||
@ -255,7 +262,7 @@ func (s KVSecrets[T]) Keys(prefix string) (keys []string, err error) {
 | 
			
		||||
		keys = append(keys, k[len(prefix):])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
    sort.Strings(keys)
 | 
			
		||||
	sort.Strings(keys)
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,10 @@ var wPublicState = watchable.New[PublicState]()
 | 
			
		||||
type State struct {
 | 
			
		||||
	HasConfig bool
 | 
			
		||||
 | 
			
		||||
	Store struct {
 | 
			
		||||
		DownloadToken string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Clusters []ClusterState
 | 
			
		||||
	Hosts    []HostState
 | 
			
		||||
	Config   *localconfig.Config
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								cmd/dkl-local-server/token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								cmd/dkl-local-server/token.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"encoding/base32"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"m.cluseau.fr/go/httperr"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func newToken(sizeInBytes int) (token string, err error) {
 | 
			
		||||
	randBytes := make([]byte, sizeInBytes)
 | 
			
		||||
 | 
			
		||||
	_, err = rand.Read(randBytes)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Print("rand read error: ", err)
 | 
			
		||||
		err = httperr.New(http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randBytes)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,12 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"archive/tar"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	restful "github.com/emicklei/go-restful"
 | 
			
		||||
)
 | 
			
		||||
@ -14,10 +19,80 @@ func wsUnlockStore(req *restful.Request, resp *restful.Response) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := unlockSecretStore([]byte(passphrase)); err != nil {
 | 
			
		||||
	if err := unlockSecretStore([]byte(passphrase)); err.Any() {
 | 
			
		||||
		err.WriteJSON(resp.ResponseWriter)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.WriteEntity(*adminToken)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func wsStoreDownload(req *restful.Request, resp *restful.Response) {
 | 
			
		||||
	token := req.QueryParameter("token")
 | 
			
		||||
	if token != wState.Get().Store.DownloadToken {
 | 
			
		||||
		wsError(resp, ErrInvalidToken)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf := new(bytes.Buffer)
 | 
			
		||||
	arch := tar.NewWriter(buf)
 | 
			
		||||
 | 
			
		||||
	root := os.DirFS(secStoreRoot())
 | 
			
		||||
 | 
			
		||||
	err := fs.WalkDir(root, ".", func(path string, d fs.DirEntry, readErr error) (err error) {
 | 
			
		||||
		if readErr != nil {
 | 
			
		||||
			err = readErr
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if path == "." {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fi, err := d.Info()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		hdr, err := tar.FileInfoHeader(fi, "")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		hdr.Name = path
 | 
			
		||||
		hdr.Uid = 0
 | 
			
		||||
		hdr.Gid = 0
 | 
			
		||||
 | 
			
		||||
		err = arch.WriteHeader(hdr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if fi.IsDir() {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		f, err := root.Open(path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		defer f.Close()
 | 
			
		||||
 | 
			
		||||
		io.Copy(arch, f)
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		wsError(resp, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = arch.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		wsError(resp, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf.WriteTo(resp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								cmd/dkl-local-server/ws-store.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								cmd/dkl-local-server/ws-store.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	restful "github.com/emicklei/go-restful"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
 | 
			
		||||
	var passphrase string
 | 
			
		||||
 | 
			
		||||
	err := req.ReadEntity(&passphrase)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		wsBadRequest(resp, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(passphrase) == 0 {
 | 
			
		||||
		wsBadRequest(resp, "no passphrase given")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	secStore.AddKey([]byte(passphrase))
 | 
			
		||||
	err = secStore.SaveTo(secKeysStorePath())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		wsError(resp, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net"
 | 
			
		||||
@ -10,6 +11,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	cfsslconfig "github.com/cloudflare/cfssl/config"
 | 
			
		||||
	"github.com/emicklei/go-restful"
 | 
			
		||||
	"m.cluseau.fr/go/httperr"
 | 
			
		||||
 | 
			
		||||
	"novit.tech/direktil/pkg/localconfig"
 | 
			
		||||
 | 
			
		||||
@ -28,6 +30,10 @@ func registerWS(rest *restful.Container) {
 | 
			
		||||
				Reads("").
 | 
			
		||||
				Writes("").
 | 
			
		||||
				Doc("Try to unlock the store")).
 | 
			
		||||
			Route(ws.GET("/store.tar").To(wsStoreDownload).
 | 
			
		||||
				Produces(mime.TAR).
 | 
			
		||||
				Param(ws.QueryParameter("token", "the download token")).
 | 
			
		||||
				Doc("Fetch the encrypted store")).
 | 
			
		||||
			Route(ws.GET("/downloads/{token}/{asset}").To(wsDownload).
 | 
			
		||||
				Param(ws.PathParameter("token", "the download token")).
 | 
			
		||||
				Param(ws.PathParameter("asset", "the requested asset")).
 | 
			
		||||
@ -42,12 +48,21 @@ func registerWS(rest *restful.Container) {
 | 
			
		||||
		Filter(adminAuth).
 | 
			
		||||
		HeaderParameter("Authorization", "Admin bearer token")
 | 
			
		||||
 | 
			
		||||
	// - store management
 | 
			
		||||
	ws.Route(ws.POST("/store/add-key").To(wsStoreAddKey).
 | 
			
		||||
		Consumes("application/json").Reads("").
 | 
			
		||||
		Produces("application/json").
 | 
			
		||||
		Doc("Add an unlock key to the store"))
 | 
			
		||||
 | 
			
		||||
	// - downloads
 | 
			
		||||
	ws.Route(ws.POST("/authorize-download").To(wsAuthorizeDownload).
 | 
			
		||||
		Consumes("application/json").Reads(DownloadSpec{}).
 | 
			
		||||
		Produces("application/json").
 | 
			
		||||
		Doc("Create a download token for the given download"))
 | 
			
		||||
 | 
			
		||||
	// - configs API
 | 
			
		||||
	ws.Route(ws.POST("/configs").To(wsUploadConfig).
 | 
			
		||||
		Consumes(mime.YAML).
 | 
			
		||||
		Doc("Upload a new current configuration, archiving the previous one"))
 | 
			
		||||
 | 
			
		||||
	// - clusters API
 | 
			
		||||
@ -180,11 +195,20 @@ func wsNotFound(req *restful.Request, resp *restful.Response) {
 | 
			
		||||
	http.NotFound(resp.ResponseWriter, req.Request)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func wsBadRequest(resp *restful.Response, err string) {
 | 
			
		||||
	httperr.New(http.StatusBadRequest, errors.New(err)).WriteJSON(resp.ResponseWriter)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func wsError(resp *restful.Response, err error) {
 | 
			
		||||
	log.Output(2, fmt.Sprint("request failed: ", err))
 | 
			
		||||
	resp.WriteErrorString(
 | 
			
		||||
		http.StatusInternalServerError,
 | 
			
		||||
		http.StatusText(http.StatusInternalServerError))
 | 
			
		||||
 | 
			
		||||
	switch err := err.(type) {
 | 
			
		||||
	case httperr.Error:
 | 
			
		||||
		err.WriteJSON(resp.ResponseWriter)
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		httperr.Internal(err).WriteJSON(resp.ResponseWriter)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string, value interface{}) {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user