From 15d6dde25c8bcd9ddfbe20aa0fc0905bffc12c0e Mon Sep 17 00:00:00 2001 From: Charlie Voiselle <464492+angrycub@users.noreply.github.com> Date: Fri, 13 May 2022 10:11:27 -0700 Subject: [PATCH] Provide mock secure variables implementation (#12980) * Add SecureVariable mock * Add SecureVariableStub * Add SecureVariable Copy and Stub funcs --- command/agent/http.go | 32 ++- command/agent/secure_variable_endpoint.go | 109 ++++++++ .../agent/secure_variable_endpoint_test.go | 254 ++++++++++++++++++ go.mod | 1 + go.sum | 2 + helper/codec/inmem.go | 3 + nomad/mock/mock.go | 35 +++ nomad/secure_variable_mock.go | 111 ++++++++ nomad/secure_variable_mock_test.go | 28 ++ nomad/secure_variables_endpoint.go | 31 ++- nomad/server.go | 7 + nomad/structs/secure_variables.go | 61 ++++- 12 files changed, 661 insertions(+), 13 deletions(-) create mode 100644 command/agent/secure_variable_endpoint.go create mode 100644 command/agent/secure_variable_endpoint_test.go create mode 100644 nomad/secure_variable_mock.go create mode 100644 nomad/secure_variable_mock_test.go diff --git a/command/agent/http.go b/command/agent/http.go index 38317d1ee..7b51883c0 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -57,13 +57,16 @@ var ( // tag isn't enabled stubHTML = "
Nomad UI is disabled
" - // allowCORS sets permissive CORS headers for a handler - allowCORS = cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"HEAD", "GET"}, - AllowedHeaders: []string{"*"}, - AllowCredentials: true, - }) + // allowCORSWithMethods sets permissive CORS headers for a handler, used by + // wrapCORS and wrapCORSWithMethods + allowCORSWithMethods = func(methods ...string) *cors.Cors { + return cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: methods, + AllowedHeaders: []string{"*"}, + AllowCredentials: true, + }) + } ) type handlerFn func(resp http.ResponseWriter, req *http.Request) (interface{}, error) @@ -407,10 +410,14 @@ func (s HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/operator/scheduler/configuration", s.wrap(s.OperatorSchedulerConfiguration)) s.mux.HandleFunc("/v1/event/stream", s.wrap(s.EventStream)) + s.mux.HandleFunc("/v1/namespaces", s.wrap(s.NamespacesRequest)) s.mux.HandleFunc("/v1/namespace", s.wrap(s.NamespaceCreateRequest)) s.mux.HandleFunc("/v1/namespace/", s.wrap(s.NamespaceSpecificRequest)) + s.mux.Handle("/v1/vars", wrapCORS(s.wrap(s.SecureVariablesListRequest))) + s.mux.Handle("/v1/var/", wrapCORSWithAllowedMethods(s.wrap(s.SecureVariableSpecificRequest), "HEAD", "GET", "PUT", "DELETE")) + uiConfigEnabled := s.agent.config.UI != nil && s.agent.config.UI.Enabled if uiEnabled && uiConfigEnabled { @@ -901,7 +908,14 @@ func (s *HTTPServer) wrapUntrustedContent(handler handlerFn) handlerFn { } } -// wrapCORS wraps a HandlerFunc in allowCORS and returns a http.Handler +// wrapCORS wraps a HandlerFunc in allowCORS with read ("HEAD", "GET") methods +// and returns a http.Handler func wrapCORS(f func(http.ResponseWriter, *http.Request)) http.Handler { - return allowCORS.Handler(http.HandlerFunc(f)) + return wrapCORSWithAllowedMethods(f, "HEAD", "GET") +} + +// wrapCORSWithAllowedMethods wraps a HandlerFunc in an allowCORS with the given +// method list and returns a http.Handler +func wrapCORSWithAllowedMethods(f func(http.ResponseWriter, *http.Request), methods ...string) http.Handler { + return allowCORSWithMethods(methods...).Handler(http.HandlerFunc(f)) } diff --git a/command/agent/secure_variable_endpoint.go b/command/agent/secure_variable_endpoint.go new file mode 100644 index 000000000..33c105e8a --- /dev/null +++ b/command/agent/secure_variable_endpoint.go @@ -0,0 +1,109 @@ +package agent + +import ( + "net/http" + "strings" + + "github.com/hashicorp/nomad/nomad/structs" +) + +func (s *HTTPServer) SecureVariablesListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, CodedError(405, ErrInvalidMethod) + } + + args := structs.SecureVariablesListRequest{} + if s.parse(resp, req, &args.Region, &args.QueryOptions) { + return nil, nil + } + + var out structs.SecureVariablesListResponse + if err := s.agent.RPC("SecureVariables.List", &args, &out); err != nil { + return nil, err + } + + setMeta(resp, &out.QueryMeta) + + if out.Data == nil { + out.Data = make([]*structs.SecureVariableStub, 0) + } + return out.Data, nil +} + +func (s *HTTPServer) SecureVariableSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + path := strings.TrimPrefix(req.URL.Path, "/v1/var/") + if len(path) == 0 { + return nil, CodedError(http.StatusBadRequest, "Missing secure variable path") + } + switch req.Method { + case http.MethodGet: + return s.secureVariableQuery(resp, req, path) + case http.MethodPut, http.MethodPost: + return s.secureVariableUpsert(resp, req, path) + case http.MethodDelete: + return s.secureVariableDelete(resp, req, path) + default: + return nil, CodedError(http.StatusBadRequest, ErrInvalidMethod) + } +} + +func (s *HTTPServer) secureVariableQuery(resp http.ResponseWriter, req *http.Request, + path string) (interface{}, error) { + args := structs.SecureVariablesReadRequest{ + Path: path, + } + if s.parse(resp, req, &args.Region, &args.QueryOptions) { + return nil, nil + } + + var out structs.SecureVariablesReadResponse + if err := s.agent.RPC("SecureVariables.Read", &args, &out); err != nil { + return nil, err + } + + setMeta(resp, &out.QueryMeta) + + if out.Data == nil { + return nil, CodedError(404, "Secure variable not found") + } + return out.Data, nil +} + +func (s *HTTPServer) secureVariableUpsert(resp http.ResponseWriter, req *http.Request, + path string) (interface{}, error) { + // Parse the SecureVariable + var SecureVariable structs.SecureVariable + if err := decodeBody(req, &SecureVariable); err != nil { + return nil, CodedError(http.StatusBadRequest, err.Error()) + } + SecureVariable.Path = path + // Format the request + args := structs.SecureVariablesUpsertRequest{ + Data: &SecureVariable, + } + s.parseWriteRequest(req, &args.WriteRequest) + + var out structs.SecureVariablesUpsertResponse + if err := s.agent.RPC("SecureVariables.Update", &args, &out); err != nil { + return nil, err + } + setIndex(resp, out.WriteMeta.Index) + + return nil, nil +} + +func (s *HTTPServer) secureVariableDelete(resp http.ResponseWriter, req *http.Request, + path string) (interface{}, error) { + + args := structs.SecureVariablesDeleteRequest{ + Path: path, + } + s.parseWriteRequest(req, &args.WriteRequest) + + var out structs.SecureVariablesDeleteResponse + if err := s.agent.RPC("SecureVariables.Delete", &args, &out); err != nil { + return nil, err + } + setIndex(resp, out.WriteMeta.Index) + return nil, nil +} diff --git a/command/agent/secure_variable_endpoint_test.go b/command/agent/secure_variable_endpoint_test.go new file mode 100644 index 000000000..34bbb26ee --- /dev/null +++ b/command/agent/secure_variable_endpoint_test.go @@ -0,0 +1,254 @@ +package agent + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/hashicorp/nomad/nomad/mock" + "github.com/hashicorp/nomad/nomad/structs" + "github.com/stretchr/testify/require" +) + +var ( + cb = func(c *Config) { + var ns int + ns = 0 + c.LogLevel = "ERROR" + c.Server.NumSchedulers = &ns + } +) + +func TestHTTP_SecureVariableList(t *testing.T) { + //ci.Parallel(t) + + httpTest(t, cb, func(s *TestAgent) { + // Test the empty list case + req, err := http.NewRequest("GET", "/v1/vars", nil) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.SecureVariablesListRequest(respW, req) + require.NoError(t, err) + + // add vars and test a populated backend + sv1 := mock.SecureVariable() + sv2 := mock.SecureVariable() + sv3 := mock.SecureVariable() + sv4 := mock.SecureVariable() + sv4.Path = sv1.Path + "/child" + for _, sv := range []*structs.SecureVariable{sv1, sv2, sv3, sv4} { + require.NoError(t, rpcWriteSV(s, sv)) + } + + // Make the HTTP request + req, err = http.NewRequest("GET", "/v1/vars", nil) + require.NoError(t, err) + respW = httptest.NewRecorder() + + // Make the request + obj, err = s.Server.SecureVariablesListRequest(respW, req) + require.NoError(t, err) + + // Check for the index + require.NotZero(t, respW.HeaderMap.Get("X-Nomad-Index")) + require.Equal(t, "true", respW.HeaderMap.Get("X-Nomad-KnownLeader")) + require.NotZero(t, respW.HeaderMap.Get("X-Nomad-LastContact")) + + // Check the output (the 3 we register ) + require.Len(t, obj.([]*structs.SecureVariableStub), 4) + + // test prefix query + req, err = http.NewRequest("GET", "/v1/vars?prefix="+sv1.Path, nil) + require.NoError(t, err) + respW = httptest.NewRecorder() + + // Make the request + obj, err = s.Server.SecureVariablesListRequest(respW, req) + require.NoError(t, err) + require.Len(t, obj.([]*structs.SecureVariableStub), 2) + + }) +} + +func TestHTTP_SecureVariableQuery(t *testing.T) { + //ci.Parallel(t) + httpTest(t, cb, func(s *TestAgent) { + // Make a request for a non-existent variable + req, err := http.NewRequest("GET", "/v1/var/does/not/exist", nil) + require.NoError(t, err) + respW := httptest.NewRecorder() + obj, err := s.Server.SecureVariableSpecificRequest(respW, req) + require.EqualError(t, err, "Secure variable not found") + + // Don't pass a path + req, err = http.NewRequest("GET", "/v1/var/", nil) + require.NoError(t, err) + respW = httptest.NewRecorder() + obj, err = s.Server.SecureVariableSpecificRequest(respW, req) + require.EqualError(t, err, "Missing secure variable path") + + // Use an incorrect verb + req, err = http.NewRequest("LOLWUT", "/v1/var/foo", nil) + require.NoError(t, err) + respW = httptest.NewRecorder() + obj, err = s.Server.SecureVariableSpecificRequest(respW, req) + require.EqualError(t, err, ErrInvalidMethod) + + // Use RPC to make a test variable + sv1 := mock.SecureVariable() + require.NoError(t, rpcWriteSV(s, sv1)) + + // Query a variable + req, err = http.NewRequest("GET", "/v1/var/"+sv1.Path, nil) + require.NoError(t, err) + respW = httptest.NewRecorder() + + // Make the request + obj, err = s.Server.SecureVariableSpecificRequest(respW, req) + require.NoError(t, err) + + // Check for the index + require.NotZero(t, respW.HeaderMap.Get("X-Nomad-Index")) + require.Equal(t, "true", respW.HeaderMap.Get("X-Nomad-KnownLeader")) + require.NotZero(t, respW.HeaderMap.Get("X-Nomad-LastContact")) + + // Check the output + require.Equal(t, sv1.Path, obj.(*structs.SecureVariable).Path) + }) +} + +func TestHTTP_SecureVariableCreate(t *testing.T) { + //ci.Parallel(t) + httpTest(t, cb, func(s *TestAgent) { + sv1 := mock.SecureVariable() + require.NoError(t, rpcWriteSV(s, sv1)) + + // Make a change for update + sv1U := sv1.Copy() + sv1U.UnencryptedData["newness"] = "awwyeah" + + // Make the HTTP request + buf := encodeReq(&sv1U) + req, err := http.NewRequest("PUT", "/v1/var/"+sv1.Path, buf) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.SecureVariableSpecificRequest(respW, req) + require.NoError(t, err) + require.Nil(t, obj) + + // Check for the index + require.NotZero(t, respW.HeaderMap.Get("X-Nomad-Index")) + + // Check the variable was created + out, err := rpcReadSV(s, sv1.Path) + require.NoError(t, err) + require.NotNil(t, out) + + sv1.CreateIndex, sv1.ModifyIndex = out.CreateIndex, out.ModifyIndex + require.Equal(t, sv1.Path, out.Path) + require.NotEqual(t, sv1, out) + require.Contains(t, out.UnencryptedData, "newness") + + // break the request body + badBuf := encodeBrokenReq(&sv1U) + + req, err = http.NewRequest("PUT", "/v1/var/"+sv1.Path, badBuf) + require.NoError(t, err) + respW = httptest.NewRecorder() + + // Make the request + obj, err = s.Server.SecureVariableSpecificRequest(respW, req) + require.EqualError(t, err, "unexpected EOF") + var cErr HTTPCodedError + require.ErrorAs(t, err, &cErr) + require.Equal(t, http.StatusBadRequest, cErr.Code()) + }) +} + +func TestHTTP_SecureVariableUpdate(t *testing.T) { + //ci.Parallel(t) + httpTest(t, cb, func(s *TestAgent) { + // Make the HTTP request + sv1 := mock.SecureVariable() + buf := encodeReq(sv1) + req, err := http.NewRequest("PUT", "/v1/var/"+sv1.Path, buf) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.SecureVariableSpecificRequest(respW, req) + require.NoError(t, err) + require.Nil(t, obj) + + // Check for the index + require.NotZero(t, respW.HeaderMap.Get("X-Nomad-Index")) + + // Check the variable was updated + out, err := rpcReadSV(s, sv1.Path) + require.NoError(t, err) + require.NotNil(t, out) + + sv1.CreateIndex, sv1.ModifyIndex = out.CreateIndex, out.ModifyIndex + require.Equal(t, sv1.Path, out.Path) + require.Equal(t, sv1, out) + }) +} + +func TestHTTP_SecureVariableDelete(t *testing.T) { + //ci.Parallel(t) + httpTest(t, cb, func(s *TestAgent) { + sv1 := mock.SecureVariable() + require.NoError(t, rpcWriteSV(s, sv1)) + + // Make the HTTP request + req, err := http.NewRequest("DELETE", "/v1/var/"+sv1.Path, nil) + require.NoError(t, err) + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.SecureVariableSpecificRequest(respW, req) + require.NoError(t, err) + require.Nil(t, obj) + + // Check for the index + require.NotZero(t, respW.HeaderMap.Get("X-Nomad-Index")) + + // Check variable bag was deleted + sv, err := rpcReadSV(s, sv1.Path) + require.NoError(t, err) + require.Nil(t, sv) + }) +} + +func encodeBrokenReq(obj interface{}) io.ReadCloser { + // var buf *bytes.Buffer + // enc := json.NewEncoder(buf) + // enc.Encode(obj) + b, _ := json.Marshal(obj) + b = b[0 : len(b)-5] // strip newline and final } + return ioutil.NopCloser(bytes.NewReader(b)) +} + +func rpcReadSV(s *TestAgent, p string) (*structs.SecureVariable, error) { + checkArgs := structs.SecureVariablesReadRequest{Path: p, QueryOptions: structs.QueryOptions{Region: "global"}} + var checkResp structs.SecureVariablesReadResponse + err := s.Agent.RPC("SecureVariables.Read", &checkArgs, &checkResp) + return checkResp.Data, err +} + +func rpcWriteSV(s *TestAgent, sv *structs.SecureVariable) error { + args := structs.SecureVariablesUpsertRequest{ + Data: sv, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + var resp structs.SecureVariablesUpsertResponse + return s.Agent.RPC("SecureVariables.Create", &args, &resp) +} diff --git a/go.mod b/go.mod index cbefce6f4..6b9ddb56e 100644 --- a/go.mod +++ b/go.mod @@ -162,6 +162,7 @@ require ( github.com/bits-and-blooms/bitset v1.2.0 // indirect github.com/bmatcuk/doublestar v1.1.5 // indirect github.com/boltdb/bolt v1.3.1 // indirect + github.com/brianvoe/gofakeit/v6 v6.16.0 github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect diff --git a/go.sum b/go.sum index a550e659e..7e8d1b3cd 100644 --- a/go.sum +++ b/go.sum @@ -190,6 +190,8 @@ github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/brianvoe/gofakeit/v6 v6.16.0 h1:EelCqtfArd8ppJ0z+TpOxXH8sVWNPBadPNdCDSMMw7k= +github.com/brianvoe/gofakeit/v6 v6.16.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= diff --git a/helper/codec/inmem.go b/helper/codec/inmem.go index cb69e89e1..ca52be3b7 100644 --- a/helper/codec/inmem.go +++ b/helper/codec/inmem.go @@ -20,6 +20,9 @@ func (i *InmemCodec) ReadRequestHeader(req *rpc.Request) error { } func (i *InmemCodec) ReadRequestBody(args interface{}) error { + if args == nil { + return nil + } sourceValue := reflect.Indirect(reflect.Indirect(reflect.ValueOf(i.Args))) dst := reflect.Indirect(reflect.Indirect(reflect.ValueOf(args))) dst.Set(sourceValue) diff --git a/nomad/mock/mock.go b/nomad/mock/mock.go index e2202d4ef..883c706e6 100644 --- a/nomad/mock/mock.go +++ b/nomad/mock/mock.go @@ -3,8 +3,10 @@ package mock import ( "fmt" "math/rand" + "strings" "time" + fake "github.com/brianvoe/gofakeit/v6" "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/helper/envoy" "github.com/hashicorp/nomad/helper/uuid" @@ -2296,3 +2298,36 @@ func ServiceRegistrations() []*structs.ServiceRegistration { }, } } + +func SecureVariable() *structs.SecureVariable { + envs := []string{"dev", "test", "prod"} + envIdx := rand.Intn(3) + env := envs[envIdx] + domain := fake.DomainName() + path := strings.ReplaceAll(env+"."+domain, ".", "/") + // owner := fake.Person() + createIdx := uint64(rand.Intn(100) + 100) + createDT := fake.DateRange(time.Now().AddDate(0, -1, 0), time.Now()) + sv := &structs.SecureVariable{ + Path: path, + Namespace: "default", + // CustomMeta: map[string]string{ + // "owner_name": owner.FirstName + " " + owner.LastName, + // "owner_email": fmt.Sprintf("%v%s@%s", owner.FirstName[0], owner.LastName, domain), + // }, + UnencryptedData: map[string]string{ + "username": fake.Username(), + "password": fake.Password(true, true, true, true, false, 16), + }, + CreateIndex: createIdx, + ModifyIndex: createIdx, + CreateTime: createDT, + ModifyTime: createDT, + } + // Flip a coin to see if we should return a "modified" object + if fake.Bool() { + sv.ModifyTime = fake.DateRange(sv.CreateTime, time.Now()) + sv.ModifyIndex = sv.CreateIndex + uint64(rand.Intn(100)) + } + return sv +} diff --git a/nomad/secure_variable_mock.go b/nomad/secure_variable_mock.go new file mode 100644 index 000000000..114a5e844 --- /dev/null +++ b/nomad/secure_variable_mock.go @@ -0,0 +1,111 @@ +package nomad + +import ( + "strings" + "sync" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/nomad/nomad/structs" +) + +var mvs MockVariableStore + +type MockVariableStore struct { + m sync.RWMutex + backingMap map[string]*structs.SecureVariable + s *Server + l hclog.Logger +} + +func (mvs *MockVariableStore) List(prefix string) []*structs.SecureVariableStub { + mvs.l.Info("***** List *****") + mvs.m.Lock() + mvs.m.Unlock() + if len(mvs.backingMap) == 0 { + return nil + } + vars := make([]*structs.SecureVariableStub, 0, len(mvs.backingMap)) + for p, sVar := range mvs.backingMap { + if strings.HasPrefix(p, prefix) { + outVar := sVar.Stub() + vars = append(vars, &outVar) + } + } + return vars +} +func (mvs *MockVariableStore) Add(p string, bag structs.SecureVariable) { + mvs.l.Info("***** Add *****") + mvs.m.Lock() + mvs.m.Unlock() + nv := bag.Copy() + mvs.backingMap[p] = nv +} + +func (mvs *MockVariableStore) Get(p string) *structs.SecureVariable { + mvs.l.Info("***** Get *****") + var out *structs.SecureVariable + mvs.m.Lock() + defer mvs.m.Unlock() + + if v, ok := mvs.backingMap[p]; ok { + out = v.Copy() + } else { + return nil + } + return out +} + +// Delete removes a key from the store. Removing a non-existent key is a no-op +func (mvs *MockVariableStore) Delete(p string) { + mvs.l.Info("***** Delete *****") + mvs.m.Lock() + defer mvs.m.Unlock() + delete(mvs.backingMap, p) +} + +// Delete removes a key from the store. Removing a non-existent key is a no-op +func (mvs *MockVariableStore) Reset() { + mvs.l.Info("***** Reset *****") + mvs.m.Lock() + mvs.m.Unlock() + mvs.backingMap = make(map[string]*structs.SecureVariable) +} + +func NewMockVariableStore(s *Server, l hclog.Logger) { + l.Info("***** Initializing mock variables backend *****") + mvs.m.Lock() + mvs.m.Unlock() + mvs.backingMap = make(map[string]*structs.SecureVariable) + mvs.s = s + mvs.l = l +} + +func SV_List(args *structs.SecureVariablesListRequest, out *structs.SecureVariablesListResponse) { + out.Data = mvs.List(args.Prefix) + out.QueryMeta.KnownLeader = true + // TODO: Would be nice to at least have a forward moving number for index + // even in testing. + out.QueryMeta.Index = 999 + out.QueryMeta.LastContact = 19 +} + +func SV_Upsert(args *structs.SecureVariablesUpsertRequest, out *structs.SecureVariablesUpsertResponse) { + nv := args.Data.Copy() + mvs.Add(nv.Path, *nv) + // TODO: Would be nice to at least have a forward moving number for index + // even in testing. + out.WriteMeta.Index = 9999 +} +func SV_Read(args *structs.SecureVariablesReadRequest, out *structs.SecureVariablesReadResponse) { + out.Data = mvs.Get(args.Path) + // TODO: Would be nice to at least have a forward moving number for index + // even in testing. + out.Index = 9999 + out.QueryMeta.KnownLeader = true + out.QueryMeta.Index = 999 + out.QueryMeta.LastContact = 19 +} +func SV_Delete(args *structs.SecureVariablesDeleteRequest, out *structs.SecureVariablesDeleteResponse) { + mvs.Delete(args.Path) + out.WriteMeta.Index = 9999 +} diff --git a/nomad/secure_variable_mock_test.go b/nomad/secure_variable_mock_test.go new file mode 100644 index 000000000..3461824c2 --- /dev/null +++ b/nomad/secure_variable_mock_test.go @@ -0,0 +1,28 @@ +package nomad + +import ( + "testing" + + "github.com/hashicorp/nomad/nomad/mock" + "github.com/stretchr/testify/require" +) + +func TestMockVariables(t *testing.T) { + defer mvs.Reset() + sv1 := mock.SecureVariable() + mvs.Add(sv1.Path, *sv1) + out := mvs.List("") + require.NotNil(t, out) + require.Len(t, out, 1) +} + +func TestDeleteMockVariables(t *testing.T) { + defer mvs.Reset() + sv1 := mock.SecureVariable() + mvs.Add(sv1.Path, *sv1) + out := mvs.List("") + require.NotNil(t, out) + require.Len(t, out, 1) + mvs.Delete(sv1.Path) + require.Empty(t, mvs.List("")) +} diff --git a/nomad/secure_variables_endpoint.go b/nomad/secure_variables_endpoint.go index 0d98cfac2..03c05c1fa 100644 --- a/nomad/secure_variables_endpoint.go +++ b/nomad/secure_variables_endpoint.go @@ -41,7 +41,13 @@ func (sv *SecureVariables) Create(args *structs.SecureVariablesUpsertRequest, re if err != nil { return err } - args.Data.EncryptedData.Data = sv.encrypter.Encrypt(buf.Bytes(), "TODO") + + args.Data.EncryptedData = &structs.SecureVariableData{} + args.Data.EncryptedData.KeyID = "TODO" + args.Data.EncryptedData.Data = sv.encrypter.Encrypt(buf.Bytes(), args.Data.EncryptedData.KeyID) + + // TODO: implementation + SV_Upsert(args, reply) return nil } @@ -61,6 +67,7 @@ func (sv *SecureVariables) List(args *structs.SecureVariablesListRequest, reply } // TODO: implementation + SV_List(args, reply) return nil } @@ -80,6 +87,7 @@ func (sv *SecureVariables) Read(args *structs.SecureVariablesReadRequest, reply } // TODO: implementation + SV_Read(args, reply) return nil } @@ -99,6 +107,27 @@ func (sv *SecureVariables) Update(args *structs.SecureVariablesUpsertRequest, re } // TODO: implementation + SV_Upsert(args, reply) + + return nil +} + +func (sv *SecureVariables) Delete(args *structs.SecureVariablesDeleteRequest, reply *structs.SecureVariablesDeleteResponse) error { + if done, err := sv.srv.forward("SecureVariables.Delete", args, args, reply); done { + return err + } + + defer metrics.MeasureSince([]string{"nomad", "secure_variables", "delete"}, time.Now()) + + // TODO: implement real ACL checks + if aclObj, err := sv.srv.ResolveToken(args.AuthToken); err != nil { + return err + } else if aclObj != nil && !aclObj.IsManagement() { + return structs.ErrPermissionDenied + } + + // TODO: implementation + SV_Delete(args, reply) return nil } diff --git a/nomad/server.go b/nomad/server.go index 3c5351add..eb3cb1ba1 100644 --- a/nomad/server.go +++ b/nomad/server.go @@ -287,6 +287,7 @@ type endpoints struct { Enterprise *EnterpriseEndpoints Event *Event Namespace *Namespace + SecureVariables *SecureVariables ServiceRegistration *ServiceRegistration // Client endpoints @@ -473,6 +474,10 @@ func NewServer(config *Config, consulCatalog consul.CatalogAPI, consulConfigEntr // Start enterprise background workers s.startEnterpriseBackground() + // FIXME: Remove once real implemenation exists + // Start Mock Secure Variables Server + go NewMockVariableStore(s, s.logger.Named("secure_variables")) + // Done return s, nil } @@ -1171,6 +1176,7 @@ func (s *Server) setupRpcServer(server *rpc.Server, ctx *RPCContext) { s.staticEndpoints.System = &System{srv: s, logger: s.logger.Named("system")} s.staticEndpoints.Search = &Search{srv: s, logger: s.logger.Named("search")} s.staticEndpoints.Namespace = &Namespace{srv: s} + s.staticEndpoints.SecureVariables = &SecureVariables{srv: s, logger: s.logger.Named("secure_variables"), encrypter: NewEncrypter()} s.staticEndpoints.Enterprise = NewEnterpriseEndpoints(s) // These endpoints are dynamic because they need access to the @@ -1218,6 +1224,7 @@ func (s *Server) setupRpcServer(server *rpc.Server, ctx *RPCContext) { server.Register(s.staticEndpoints.FileSystem) server.Register(s.staticEndpoints.Agent) server.Register(s.staticEndpoints.Namespace) + server.Register(s.staticEndpoints.SecureVariables) // Create new dynamic endpoints and add them to the RPC server. alloc := &Alloc{srv: s, ctx: ctx, logger: s.logger.Named("alloc")} diff --git a/nomad/structs/secure_variables.go b/nomad/structs/secure_variables.go index 09d301389..a3fd63223 100644 --- a/nomad/structs/secure_variables.go +++ b/nomad/structs/secure_variables.go @@ -18,8 +18,8 @@ type SecureVariable struct { // Version uint64 // CustomMetaData map[string]string - EncryptedData *SecureVariableData // removed during serialization - UnencryptedData map[string]string // empty until serialized + EncryptedData *SecureVariableData `json:"-"` // removed during serialization + UnencryptedData map[string]string `json:"Items"` // empty until serialized } // SecureVariableData is the secret data for a Secure Variable @@ -28,6 +28,61 @@ type SecureVariableData struct { KeyID string // ID of root key used to encrypt this entry } +func (sv SecureVariableData) Copy() *SecureVariableData { + out := make([]byte, 0, len(sv.Data)) + copy(out, sv.Data) + return &SecureVariableData{ + Data: out, + KeyID: sv.KeyID, + } +} + +func (sv *SecureVariable) Copy() *SecureVariable { + if sv == nil { + return nil + } + out := *sv + if sv.UnencryptedData != nil { + out.UnencryptedData = make(map[string]string, len(sv.UnencryptedData)) + for k, v := range sv.UnencryptedData { + out.UnencryptedData[k] = v + } + } + if sv.EncryptedData != nil { + out.EncryptedData = sv.EncryptedData.Copy() + } + return &out +} + +func (sv SecureVariable) Stub() SecureVariableStub { + return SecureVariableStub{ + Namespace: sv.Namespace, + Path: sv.Path, + CreateIndex: sv.CreateIndex, + CreateTime: sv.CreateTime, + ModifyIndex: sv.ModifyIndex, + ModifyTime: sv.ModifyTime, + } +} + +// SecureVariableStub is the metadata envelope for a Secure Variable omitting +// the actual data. Intended to be used in list operations. +type SecureVariableStub struct { + Namespace string + Path string + CreateTime time.Time + CreateIndex uint64 + ModifyIndex uint64 + ModifyTime time.Time + + // reserved for post-1.4.0 work + // LockIndex uint64 + // Session string + // DeletedAt time.Time + // Version uint64 + // CustomMetaData map[string]string +} + // SecureVariablesQuota is used to track the total size of secure // variables entries per namespace. The total length of // SecureVariable.EncryptedData will be added to the SecureVariablesQuota @@ -54,7 +109,7 @@ type SecureVariablesListRequest struct { } type SecureVariablesListResponse struct { - Data []*SecureVariable + Data []*SecureVariableStub QueryMeta }