feat(secret): transition the secret service to isolated pattern (#18340)

* feat(secret): transition the secret service to isolated pattern

We needed a secret service handler that would be pluggable in the
org service and building it right is better then doing it with messy code.
pull/18357/head
Lyon Hill 2020-06-03 11:37:51 -06:00 committed by GitHub
parent d21fce5f6d
commit ab2f4ecdbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1852 additions and 0 deletions

2
go.sum
View File

@ -149,6 +149,7 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@ -471,6 +472,7 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5i
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=

88
secret/http_client.go Normal file
View File

@ -0,0 +1,88 @@
package secret
import (
"context"
"fmt"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/tracing"
"github.com/influxdata/influxdb/v2/pkg/httpc"
)
type Client struct {
Client *httpc.Client
}
// LoadSecret is not implemented for http
func (s *Client) LoadSecret(ctx context.Context, orgID influxdb.ID, k string) (string, error) {
return "", &influxdb.Error{
Code: influxdb.EMethodNotAllowed,
Msg: "load secret is not implemented for http",
}
}
// PutSecret is not implemented for http.
func (s *Client) PutSecret(ctx context.Context, orgID influxdb.ID, k string, v string) error {
return &influxdb.Error{
Code: influxdb.EMethodNotAllowed,
Msg: "put secret is not implemented for http",
}
}
// GetSecretKeys get all secret keys mathing an org ID via HTTP.
func (s *Client) GetSecretKeys(ctx context.Context, orgID influxdb.ID) ([]string, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
span.LogKV("org-id", orgID)
path := fmt.Sprintf("/api/v2/orgs/%s/secrets", orgID.String())
var ss secretsResponse
err := s.Client.
Get(path).
DecodeJSON(&ss).
Do(ctx)
if err != nil {
return nil, err
}
return ss.Secrets, nil
}
// PutSecrets is not implemented for http.
func (s *Client) PutSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return &influxdb.Error{
Code: influxdb.EMethodNotAllowed,
Msg: "put secrets is not implemented for http",
}
}
// PatchSecrets will update the existing secret with new via http.
func (s *Client) PatchSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if orgID != 0 {
span.LogKV("org-id", orgID)
}
path := fmt.Sprintf("/api/v2/orgs/%s/secrets", orgID.String())
return s.Client.
PatchJSON(m, path).
Do(ctx)
}
// DeleteSecret removes a single secret via HTTP.
func (s *Client) DeleteSecret(ctx context.Context, orgID influxdb.ID, ks ...string) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
path := fmt.Sprintf("/api/v2/orgs/%s/secrets/delete", orgID.String())
return s.Client.
PostJSON(secretsDeleteBody{
Secrets: ks,
}, path).
Do(ctx)
}

134
secret/http_server.go Normal file
View File

@ -0,0 +1,134 @@
package secret
import (
"fmt"
"net/http"
"github.com/go-chi/chi"
"github.com/influxdata/influxdb/v2"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"go.uber.org/zap"
)
type handler struct {
log *zap.Logger
svc influxdb.SecretService
api *kithttp.API
idLookupKey string
}
// NewHandler creates a new handler for the secret service
func NewHandler(log *zap.Logger, idLookupKey string, svc influxdb.SecretService) http.Handler {
h := &handler{
log: log,
svc: svc,
api: kithttp.NewAPI(kithttp.WithLog(log)),
idLookupKey: idLookupKey,
}
r := chi.NewRouter()
r.Get("/", h.handleGetSecrets)
r.Patch("/", h.handlePatchSecrets)
// TODO: this shouldn't be a post to delete
r.Post("/delete", h.handleDeleteSecrets)
return r
}
// handleGetSecrets is the HTTP handler for the GET /api/v2/orgs/:id/secrets route.
func (h *handler) handleGetSecrets(w http.ResponseWriter, r *http.Request) {
orgID, err := h.decodeOrgID(r)
if err != nil {
h.api.Err(w, r, err)
}
ks, err := h.svc.GetSecretKeys(r.Context(), orgID)
if err != nil && influxdb.ErrorCode(err) != influxdb.ENotFound {
h.api.Err(w, r, err)
return
}
h.api.Respond(w, r, http.StatusOK, newSecretsResponse(orgID, ks))
}
type secretsResponse struct {
Links map[string]string `json:"links"`
Secrets []string `json:"secrets"`
}
func newSecretsResponse(orgID influxdb.ID, ks []string) *secretsResponse {
if ks == nil {
ks = []string{}
}
return &secretsResponse{
Links: map[string]string{
"org": fmt.Sprintf("/api/v2/orgs/%s", orgID),
"self": fmt.Sprintf("/api/v2/orgs/%s/secrets", orgID),
},
Secrets: ks,
}
}
// handleGetPatchSecrets is the HTTP handler for the PATCH /api/v2/orgs/:id/secrets route.
func (h *handler) handlePatchSecrets(w http.ResponseWriter, r *http.Request) {
orgID, err := h.decodeOrgID(r)
if err != nil {
h.api.Err(w, r, err)
}
var secrets map[string]string
if err := h.api.DecodeJSON(r.Body, &secrets); err != nil {
h.api.Err(w, r, err)
return
}
if err := h.svc.PatchSecrets(r.Context(), orgID, secrets); err != nil {
h.api.Err(w, r, err)
return
}
h.api.Respond(w, r, http.StatusNoContent, nil)
}
type secretsDeleteBody struct {
Secrets []string `json:"secrets"`
}
// handleDeleteSecrets is the HTTP handler for the DELETE /api/v2/orgs/:id/secrets route.
func (h *handler) handleDeleteSecrets(w http.ResponseWriter, r *http.Request) {
orgID, err := h.decodeOrgID(r)
if err != nil {
h.api.Err(w, r, err)
}
var reqBody secretsDeleteBody
if err := h.api.DecodeJSON(r.Body, &reqBody); err != nil {
h.api.Err(w, r, err)
return
}
if err := h.svc.DeleteSecret(r.Context(), orgID, reqBody.Secrets...); err != nil {
h.api.Err(w, r, err)
return
}
h.api.Respond(w, r, http.StatusNoContent, nil)
}
func (h *handler) decodeOrgID(r *http.Request) (influxdb.ID, error) {
org := chi.URLParam(r, h.idLookupKey)
if org == "" {
return influxdb.InvalidID(), &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
id, err := influxdb.IDFromString(org)
if err != nil {
return influxdb.InvalidID(), err
}
return *id, nil
}

337
secret/http_server_test.go Normal file
View File

@ -0,0 +1,337 @@
package secret
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi"
"github.com/influxdata/influxdb/v2"
influxdbhttp "github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/inmem"
"github.com/influxdata/influxdb/v2/mock"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func initSecretService(f influxdbtesting.SecretServiceFields, t *testing.T) (influxdb.SecretService, func()) {
t.Helper()
s := inmem.NewKVStore()
storage, err := NewStore(s)
if err != nil {
t.Fatal(err)
}
svc := NewService(storage)
for _, s := range f.Secrets {
if err := svc.PutSecrets(context.Background(), s.OrganizationID, s.Env); err != nil {
t.Fatalf("failed to populate users")
}
}
ctx := context.Background()
for _, ss := range f.Secrets {
if err := svc.PutSecrets(ctx, ss.OrganizationID, ss.Env); err != nil {
t.Fatalf("failed to populate secrets")
}
}
handler := NewHandler(zaptest.NewLogger(t), "id", svc)
router := chi.NewRouter()
router.Mount("/api/v2/orgs/{id}/secrets", handler)
server := httptest.NewServer(router)
httpClient, err := influxdbhttp.NewHTTPClient(server.URL, "", false)
if err != nil {
t.Fatal(err)
}
client := Client{
Client: httpClient,
}
return &client, server.Close
}
func TestSecretService(t *testing.T) {
t.Parallel()
influxdbtesting.GetSecretKeys(initSecretService, t)
influxdbtesting.PatchSecrets(initSecretService, t)
influxdbtesting.DeleteSecrets(initSecretService, t)
}
func TestSecretService_handleGetSecrets(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
orgID influxdb.ID
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get basic secrets",
fields: fields{
&mock.SecretService{
GetSecretKeysFn: func(ctx context.Context, orgID influxdb.ID) ([]string, error) {
return []string{"hello", "world"}, nil
},
},
},
args: args{
orgID: 1,
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: "{\n\t\"links\": {\n\t\t\"org\": \"/api/v2/orgs/0000000000000001\",\n\t\t\"self\": \"/api/v2/orgs/0000000000000001/secrets\"\n\t},\n\t\"secrets\": [\n\t\t\"hello\",\n\t\t\"world\"\n\t]\n}",
},
},
{
name: "get secrets when there are none",
fields: fields{
&mock.SecretService{
GetSecretKeysFn: func(ctx context.Context, orgID influxdb.ID) ([]string, error) {
return []string{}, nil
},
},
},
args: args{
orgID: 1,
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: "{\n\t\"links\": {\n\t\t\"org\": \"/api/v2/orgs/0000000000000001\",\n\t\t\"self\": \"/api/v2/orgs/0000000000000001/secrets\"\n\t},\n\t\"secrets\": []\n}",
},
},
{
name: "get secrets when organization has no secret keys",
fields: fields{
&mock.SecretService{
GetSecretKeysFn: func(ctx context.Context, orgID influxdb.ID) ([]string, error) {
return []string{}, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "organization has no secret keys",
}
},
},
},
args: args{
orgID: 1,
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: "{\n\t\"links\": {\n\t\t\"org\": \"/api/v2/orgs/0000000000000001\",\n\t\t\"self\": \"/api/v2/orgs/0000000000000001/secrets\"\n\t},\n\t\"secrets\": []\n}",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHandler(zaptest.NewLogger(t), "id", tt.fields.SecretService)
router := chi.NewRouter()
router.Mount("/api/v2/orgs/{id}/secrets", h)
u := fmt.Sprintf("http://any.url/api/v2/orgs/%s/secrets", tt.args.orgID)
r := httptest.NewRequest("GET", u, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("handleGetSecrets() = %v, want %v", res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("handleGetSecrets() = %v, want %v", content, tt.wants.contentType)
}
if tt.wants.body != "" {
if string(body) != tt.wants.body {
t.Errorf("%q. handleGetSecrets() invalid body: %q", tt.name, body)
}
}
})
}
}
func TestSecretService_handlePatchSecrets(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
orgID influxdb.ID
secrets map[string]string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get basic secrets",
fields: fields{
&mock.SecretService{
PatchSecretsFn: func(ctx context.Context, orgID influxdb.ID, s map[string]string) error {
return nil
},
},
},
args: args{
orgID: 1,
secrets: map[string]string{
"abc": "123",
},
},
wants: wants{
statusCode: http.StatusNoContent,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHandler(zaptest.NewLogger(t), "id", tt.fields.SecretService)
router := chi.NewRouter()
router.Mount("/api/v2/orgs/{id}/secrets", h)
b, err := json.Marshal(tt.args.secrets)
if err != nil {
t.Fatalf("failed to marshal secrets: %v", err)
}
buf := bytes.NewReader(b)
u := fmt.Sprintf("http://any.url/api/v2/orgs/%s/secrets", tt.args.orgID)
r := httptest.NewRequest("PATCH", u, buf)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("handlePatchSecrets() = %v, want %v", res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("handlePatchSecrets() = %v, want %v", content, tt.wants.contentType)
}
if tt.wants.body != "" {
if string(body) != tt.wants.body {
t.Errorf("%q. handlePatchSecrets() invalid body", tt.name)
}
}
})
}
}
func TestSecretService_handleDeleteSecrets(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
orgID influxdb.ID
secrets []string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get basic secrets",
fields: fields{
&mock.SecretService{
DeleteSecretFn: func(ctx context.Context, orgID influxdb.ID, s ...string) error {
return nil
},
},
},
args: args{
orgID: 1,
secrets: []string{
"abc",
},
},
wants: wants{
statusCode: http.StatusNoContent,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHandler(zaptest.NewLogger(t), "id", tt.fields.SecretService)
router := chi.NewRouter()
router.Mount("/api/v2/orgs/{id}/secrets", h)
b, err := json.Marshal(struct {
Secrets []string `json:"secrets"`
}{
Secrets: tt.args.secrets,
})
if err != nil {
t.Fatalf("failed to marshal secrets: %v", err)
}
buf := bytes.NewReader(b)
u := fmt.Sprintf("http://any.url/api/v2/orgs/%s/secrets/delete", tt.args.orgID)
r := httptest.NewRequest("POST", u, buf)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("handleDeleteSecrets() = %v, want %v", res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("handleDeleteSecrets() = %v, want %v", content, tt.wants.contentType)
}
if tt.wants.body != "" {
if string(body) != tt.wants.body {
t.Errorf("%q. handleDeleteSecrets() invalid body", tt.name)
}
}
})
}
}

100
secret/middleware_auth.go Normal file
View File

@ -0,0 +1,100 @@
package secret
import (
"context"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorizer"
)
var _ influxdb.SecretService = (*AuthedSvc)(nil)
// AuthedSvc wraps a influxdb.AuthedSvc and authorizes actions
// against it appropriately.
type AuthedSvc struct {
s influxdb.SecretService
}
// NewAuthedService constructs an instance of an authorizing secret service.
func NewAuthedService(s influxdb.SecretService) *AuthedSvc {
return &AuthedSvc{
s: s,
}
}
// LoadSecret checks to see if the authorizer on context has read access to the secret key provided.
func (s *AuthedSvc) LoadSecret(ctx context.Context, orgID influxdb.ID, key string) (string, error) {
if _, _, err := authorizer.AuthorizeOrgReadResource(ctx, influxdb.SecretsResourceType, orgID); err != nil {
return "", err
}
secret, err := s.s.LoadSecret(ctx, orgID, key)
if err != nil {
return "", err
}
return secret, nil
}
// GetSecretKeys checks to see if the authorizer on context has read access to all the secrets belonging to orgID.
func (s *AuthedSvc) GetSecretKeys(ctx context.Context, orgID influxdb.ID) ([]string, error) {
if _, _, err := authorizer.AuthorizeOrgReadResource(ctx, influxdb.SecretsResourceType, orgID); err != nil {
return []string{}, err
}
secrets, err := s.s.GetSecretKeys(ctx, orgID)
if err != nil {
return []string{}, err
}
return secrets, nil
}
// PutSecret checks to see if the authorizer on context has write access to the secret key provided.
func (s *AuthedSvc) PutSecret(ctx context.Context, orgID influxdb.ID, key string, val string) error {
if _, _, err := authorizer.AuthorizeCreate(ctx, influxdb.SecretsResourceType, orgID); err != nil {
return err
}
err := s.s.PutSecret(ctx, orgID, key, val)
if err != nil {
return err
}
return nil
}
// PutSecrets checks to see if the authorizer on context has read and write access to the secret keys provided.
func (s *AuthedSvc) PutSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
// PutSecrets operates on intersection between m and keys beloging to orgID.
// We need to have read access to those secrets since it deletes the secrets (within the intersection) that have not be overridden.
if _, _, err := authorizer.AuthorizeOrgReadResource(ctx, influxdb.SecretsResourceType, orgID); err != nil {
return err
}
if _, _, err := authorizer.AuthorizeOrgWriteResource(ctx, influxdb.SecretsResourceType, orgID); err != nil {
return err
}
err := s.s.PutSecrets(ctx, orgID, m)
if err != nil {
return err
}
return nil
}
// PatchSecrets checks to see if the authorizer on context has write access to the secret keys provided.
func (s *AuthedSvc) PatchSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
if _, _, err := authorizer.AuthorizeOrgWriteResource(ctx, influxdb.SecretsResourceType, orgID); err != nil {
return err
}
err := s.s.PatchSecrets(ctx, orgID, m)
if err != nil {
return err
}
return nil
}
// DeleteSecret checks to see if the authorizer on context has write access to the secret keys provided.
func (s *AuthedSvc) DeleteSecret(ctx context.Context, orgID influxdb.ID, keys ...string) error {
if _, _, err := authorizer.AuthorizeOrgWriteResource(ctx, influxdb.SecretsResourceType, orgID); err != nil {
return err
}
err := s.s.DeleteSecret(ctx, orgID, keys...)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,698 @@
package secret_test
import (
"bytes"
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/influxdb/v2"
influxdbcontext "github.com/influxdata/influxdb/v2/context"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/secret"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
)
var secretCmpOptions = cmp.Options{
cmp.Comparer(func(x, y []byte) bool {
return bytes.Equal(x, y)
}),
}
func TestSecretService_LoadSecret(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
permission influxdb.Permission
org influxdb.ID
key string
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized to access secret within org",
fields: fields{
SecretService: &mock.SecretService{
LoadSecretFn: func(ctx context.Context, orgID influxdb.ID, k string) (string, error) {
if k == "key" {
return "val", nil
}
return "", &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrSecretNotFound,
}
},
},
},
args: args{
permission: influxdb.Permission{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(10),
},
},
key: "key",
org: influxdb.ID(10),
},
wants: wants{
err: nil,
},
},
{
name: "cannot access not existing secret",
fields: fields{
SecretService: &mock.SecretService{
LoadSecretFn: func(ctx context.Context, orgID influxdb.ID, k string) (string, error) {
if k == "key" {
return "val", nil
}
return "", &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrSecretNotFound,
}
},
},
},
args: args{
permission: influxdb.Permission{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(10),
},
},
key: "not existing",
org: influxdb.ID(10),
},
wants: wants{
err: &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrSecretNotFound,
},
},
},
{
name: "unauthorized to access secret within org",
fields: fields{
SecretService: &mock.SecretService{
LoadSecretFn: func(ctx context.Context, orgID influxdb.ID, k string) (string, error) {
if k == "key" {
return "val", nil
}
return "", &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrSecretNotFound,
}
},
},
},
args: args{
permission: influxdb.Permission{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
ID: influxdbtesting.IDPtr(10),
},
},
org: influxdb.ID(2),
key: "key",
},
wants: wants{
err: &influxdb.Error{
Msg: "read:orgs/0000000000000002/secrets is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := secret.NewAuthedService(tt.fields.SecretService)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{tt.args.permission}))
_, err := s.LoadSecret(ctx, tt.args.org, tt.args.key)
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
})
}
}
func TestSecretService_GetSecretKeys(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
permission influxdb.Permission
org influxdb.ID
}
type wants struct {
err error
secrets []string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized to see all secrets within an org",
fields: fields{
SecretService: &mock.SecretService{
GetSecretKeysFn: func(ctx context.Context, orgID influxdb.ID) ([]string, error) {
return []string{
"0000000000000001secret1",
"0000000000000001secret2",
"0000000000000001secret3",
}, nil
},
},
},
args: args{
permission: influxdb.Permission{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
org: influxdb.ID(1),
},
wants: wants{
secrets: []string{
"0000000000000001secret1",
"0000000000000001secret2",
"0000000000000001secret3",
},
},
},
{
name: "unauthorized to see all secrets within an org",
fields: fields{
SecretService: &mock.SecretService{
GetSecretKeysFn: func(ctx context.Context, orgID influxdb.ID) ([]string, error) {
return []string{
"0000000000000002secret1",
"0000000000000002secret2",
"0000000000000002secret3",
}, nil
},
},
},
args: args{
permission: influxdb.Permission{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
org: influxdb.ID(2),
},
wants: wants{
err: &influxdb.Error{
Code: influxdb.EUnauthorized,
Msg: "read:orgs/0000000000000002/secrets is unauthorized",
},
secrets: []string{},
},
},
{
name: "errors when there are not secret into an org",
fields: fields{
SecretService: &mock.SecretService{
GetSecretKeysFn: func(ctx context.Context, orgID influxdb.ID) ([]string, error) {
return []string(nil), &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "organization has no secret keys",
}
},
},
},
args: args{
permission: influxdb.Permission{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(10),
},
},
org: influxdb.ID(10),
},
wants: wants{
err: &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "organization has no secret keys",
},
secrets: []string{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := secret.NewAuthedService(tt.fields.SecretService)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{tt.args.permission}))
secrets, err := s.GetSecretKeys(ctx, tt.args.org)
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
if diff := cmp.Diff(secrets, tt.wants.secrets, secretCmpOptions...); diff != "" {
t.Errorf("secrets are different -got/+want\ndiff %s", diff)
}
})
}
}
func TestSecretService_PatchSecrets(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
org influxdb.ID
permissions []influxdb.Permission
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized to patch secrets",
fields: fields{
SecretService: &mock.SecretService{
PatchSecretsFn: func(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return nil
},
},
},
args: args{
org: influxdb.ID(1),
permissions: []influxdb.Permission{
{
Action: influxdb.WriteAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
},
},
wants: wants{
err: nil,
},
},
{
name: "unauthorized to update secret",
fields: fields{
SecretService: &mock.SecretService{
PatchSecretsFn: func(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return nil
},
},
},
args: args{
org: influxdb.ID(1),
permissions: []influxdb.Permission{
{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(10),
},
},
},
},
wants: wants{
err: &influxdb.Error{
Msg: "write:orgs/0000000000000001/secrets is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := secret.NewAuthedService(tt.fields.SecretService)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, tt.args.permissions))
patches := make(map[string]string)
err := s.PatchSecrets(ctx, tt.args.org, patches)
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
})
}
}
func TestSecretService_DeleteSecret(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
org influxdb.ID
permissions []influxdb.Permission
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized to delete secret",
fields: fields{
SecretService: &mock.SecretService{
DeleteSecretFn: func(ctx context.Context, orgID influxdb.ID, keys ...string) error {
return nil
},
},
},
args: args{
org: influxdb.ID(1),
permissions: []influxdb.Permission{
{
Action: influxdb.WriteAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
},
},
wants: wants{
err: nil,
},
},
{
name: "unauthorized to delete secret",
fields: fields{
SecretService: &mock.SecretService{
DeleteSecretFn: func(ctx context.Context, orgID influxdb.ID, keys ...string) error {
return nil
},
},
},
args: args{
org: 10,
permissions: []influxdb.Permission{
{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
},
},
wants: wants{
err: &influxdb.Error{
Msg: "write:orgs/000000000000000a/secrets is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := secret.NewAuthedService(tt.fields.SecretService)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, tt.args.permissions))
err := s.DeleteSecret(ctx, tt.args.org)
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
})
}
}
func TestSecretService_PutSecret(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
permission influxdb.Permission
orgID influxdb.ID
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized to put a secret",
fields: fields{
SecretService: &mock.SecretService{
PutSecretFn: func(ctx context.Context, orgID influxdb.ID, key string, val string) error {
return nil
},
},
},
args: args{
orgID: influxdb.ID(10),
permission: influxdb.Permission{
Action: influxdb.WriteAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(10),
},
},
},
wants: wants{
err: nil,
},
},
{
name: "unauthorized to put a secret",
fields: fields{
SecretService: &mock.SecretService{
PutSecretFn: func(ctx context.Context, orgID influxdb.ID, key string, val string) error {
return nil
},
},
},
args: args{
orgID: 10,
permission: influxdb.Permission{
Action: influxdb.WriteAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
ID: influxdbtesting.IDPtr(1),
},
},
},
wants: wants{
err: &influxdb.Error{
Msg: "write:orgs/000000000000000a/secrets is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := secret.NewAuthedService(tt.fields.SecretService)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{tt.args.permission}))
err := s.PutSecret(ctx, tt.args.orgID, "", "")
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
})
}
}
func TestSecretService_PutSecrets(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
permissions []influxdb.Permission
orgID influxdb.ID
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized to put secrets",
fields: fields{
SecretService: &mock.SecretService{
PutSecretsFn: func(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return nil
},
},
},
args: args{
orgID: influxdb.ID(10),
permissions: []influxdb.Permission{
{
Action: influxdb.WriteAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(10),
},
},
{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(10),
},
},
},
},
wants: wants{
err: nil,
},
},
{
name: "unauthorized to put secrets",
fields: fields{
SecretService: &mock.SecretService{
PutSecretsFn: func(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return nil
},
},
},
args: args{
orgID: influxdb.ID(2),
permissions: []influxdb.Permission{
{
Action: influxdb.WriteAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(2),
},
},
},
},
wants: wants{
err: &influxdb.Error{
Msg: "write:orgs/0000000000000002/secrets is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
{
name: "unauthorized to put secrets without read access to their org",
fields: fields{
SecretService: &mock.SecretService{
PutSecretFn: func(ctx context.Context, orgID influxdb.ID, key string, val string) error {
return nil
},
PutSecretsFn: func(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return nil
},
},
},
args: args{
orgID: 10,
permissions: []influxdb.Permission{
{
Action: influxdb.WriteAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(10),
},
},
},
},
wants: wants{
err: &influxdb.Error{
Msg: "read:orgs/000000000000000a/secrets is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
{
name: "unauthorized to put secrets without write access to their org",
fields: fields{
SecretService: &mock.SecretService{
PutSecretFn: func(ctx context.Context, orgID influxdb.ID, key string, val string) error {
return nil
},
PutSecretsFn: func(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return nil
},
},
},
args: args{
orgID: 10,
permissions: []influxdb.Permission{
{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.SecretsResourceType,
OrgID: influxdbtesting.IDPtr(10),
},
},
},
},
wants: wants{
err: &influxdb.Error{
Msg: "write:orgs/000000000000000a/secrets is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := secret.NewAuthedService(tt.fields.SecretService)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, tt.args.permissions))
secrets := make(map[string]string)
err := s.PutSecrets(ctx, tt.args.orgID, secrets)
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
})
}
}

View File

@ -0,0 +1,109 @@
package secret
import (
"context"
"time"
"github.com/influxdata/influxdb/v2"
"go.uber.org/zap"
)
// Logger is a logger service middleware for secrets
type Logger struct {
logger *zap.Logger
secretService influxdb.SecretService
}
var _ influxdb.SecretService = (*Logger)(nil)
// NewLogger returns a logging service middleware for the User Service.
func NewLogger(log *zap.Logger, s influxdb.SecretService) *Logger {
return &Logger{
logger: log,
secretService: s,
}
}
// LoadSecret retrieves the secret value v found at key k for organization orgID.
func (l *Logger) LoadSecret(ctx context.Context, orgID influxdb.ID, key string) (str string, err error) {
defer func(start time.Time) {
dur := zap.Duration("took", time.Since(start))
if err != nil {
l.logger.Debug("failed to load secret", zap.Error(err), dur)
return
}
l.logger.Debug("secret load", dur)
}(time.Now())
return l.secretService.LoadSecret(ctx, orgID, key)
}
// GetSecretKeys retrieves all secret keys that are stored for the organization orgID.
func (l *Logger) GetSecretKeys(ctx context.Context, orgID influxdb.ID) (strs []string, err error) {
defer func(start time.Time) {
dur := zap.Duration("took", time.Since(start))
if err != nil {
l.logger.Debug("failed to get secret keys", zap.Error(err), dur)
return
}
l.logger.Debug("secret get keys", dur)
}(time.Now())
return l.secretService.GetSecretKeys(ctx, orgID)
}
// PutSecret stores the secret pair (k,v) for the organization orgID.
func (l *Logger) PutSecret(ctx context.Context, orgID influxdb.ID, key string, val string) (err error) {
defer func(start time.Time) {
dur := zap.Duration("took", time.Since(start))
if err != nil {
l.logger.Debug("failed to put secret", zap.Error(err), dur)
return
}
l.logger.Debug("secret put", dur)
}(time.Now())
return l.secretService.PutSecret(ctx, orgID, key, val)
}
// PutSecrets puts all provided secrets and overwrites any previous values.
func (l *Logger) PutSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) (err error) {
defer func(start time.Time) {
dur := zap.Duration("took", time.Since(start))
if err != nil {
l.logger.Debug("failed to put secrets", zap.Error(err), dur)
return
}
l.logger.Debug("secret puts", dur)
}(time.Now())
return l.secretService.PutSecrets(ctx, orgID, m)
}
// PatchSecrets patches all provided secrets and updates any previous values.
func (l *Logger) PatchSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) (err error) {
defer func(start time.Time) {
dur := zap.Duration("took", time.Since(start))
if err != nil {
l.logger.Debug("failed to patch secret", zap.Error(err), dur)
return
}
l.logger.Debug("secret patch", dur)
}(time.Now())
return l.secretService.PatchSecrets(ctx, orgID, m)
}
// DeleteSecret removes a single secret from the secret store.
func (l *Logger) DeleteSecret(ctx context.Context, orgID influxdb.ID, keys ...string) (err error) {
defer func(start time.Time) {
dur := zap.Duration("took", time.Since(start))
if err != nil {
l.logger.Debug("failed to delete secret", zap.Error(err), dur)
return
}
l.logger.Debug("secret delete", dur)
}(time.Now())
return l.secretService.DeleteSecret(ctx, orgID, keys...)
}

View File

@ -0,0 +1,69 @@
package secret
import (
"context"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/metric"
"github.com/prometheus/client_golang/prometheus"
)
// SecreteService is a metrics middleware system for the secret service
type SecreteService struct {
// RED metrics
rec *metric.REDClient
secretSvc influxdb.SecretService
}
var _ influxdb.SecretService = (*SecreteService)(nil)
// NewMetricService creates a new secret metrics middleware
func NewMetricService(reg prometheus.Registerer, s influxdb.SecretService) *SecreteService {
return &SecreteService{
rec: metric.New(reg, "secret"),
secretSvc: s,
}
}
// LoadSecret retrieves the secret value v found at key k for organization orgID.
func (ms *SecreteService) LoadSecret(ctx context.Context, orgID influxdb.ID, key string) (string, error) {
rec := ms.rec.Record("load_secret")
secret, err := ms.secretSvc.LoadSecret(ctx, orgID, key)
return secret, rec(err)
}
// GetSecretKeys retrieves all secret keys that are stored for the organization orgID.
func (ms *SecreteService) GetSecretKeys(ctx context.Context, orgID influxdb.ID) ([]string, error) {
rec := ms.rec.Record("get_secret_keys")
secrets, err := ms.secretSvc.GetSecretKeys(ctx, orgID)
return secrets, rec(err)
}
// PutSecret stores the secret pair (k,v) for the organization orgID.
func (ms *SecreteService) PutSecret(ctx context.Context, orgID influxdb.ID, key string, val string) error {
rec := ms.rec.Record("put_secret")
err := ms.secretSvc.PutSecret(ctx, orgID, key, val)
return rec(err)
}
// PutSecrets puts all provided secrets and overwrites any previous values.
func (ms *SecreteService) PutSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
rec := ms.rec.Record("put_secrets")
err := ms.secretSvc.PutSecrets(ctx, orgID, m)
return rec(err)
}
// PatchSecrets patches all provided secrets and updates any previous values.
func (ms *SecreteService) PatchSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
rec := ms.rec.Record("patch_secrets")
err := ms.secretSvc.PatchSecrets(ctx, orgID, m)
return rec(err)
}
// DeleteSecret removes a single secret from the secret store.
func (ms *SecreteService) DeleteSecret(ctx context.Context, orgID influxdb.ID, keys ...string) error {
rec := ms.rec.Record("delete_secret")
err := ms.secretSvc.DeleteSecret(ctx, orgID, keys...)
return rec(err)
}

89
secret/service.go Normal file
View File

@ -0,0 +1,89 @@
package secret
import (
"context"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
)
type Service struct {
s *Storage
}
// NewService creates a new service implementaiton for secrets
func NewService(s *Storage) *Service {
return &Service{s}
}
// LoadSecret retrieves the secret value v found at key k for organization orgID.
func (s *Service) LoadSecret(ctx context.Context, orgID influxdb.ID, k string) (string, error) {
var v string
err := s.s.View(ctx, func(tx kv.Tx) error {
var err error
v, err = s.s.GetSecret(ctx, tx, orgID, k)
return err
})
return v, err
}
// GetSecretKeys retrieves all secret keys that are stored for the organization orgID.
func (s *Service) GetSecretKeys(ctx context.Context, orgID influxdb.ID) ([]string, error) {
var v []string
err := s.s.View(ctx, func(tx kv.Tx) error {
var err error
v, err = s.s.ListSecret(ctx, tx, orgID)
return err
})
return v, err
}
// PutSecret stores the secret pair (k,v) for the organization orgID.
func (s *Service) PutSecret(ctx context.Context, orgID influxdb.ID, k, v string) error {
err := s.s.Update(ctx, func(tx kv.Tx) error {
return s.s.PutSecret(ctx, tx, orgID, k, v)
})
return err
}
// PutSecrets puts all provided secrets and overwrites any previous values.
func (s *Service) PutSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
// put secretes expects to replace all existing secretes
keys, err := s.GetSecretKeys(ctx, orgID)
if err != nil {
return err
}
if err := s.DeleteSecret(ctx, orgID, keys...); err != nil {
return err
}
return s.PatchSecrets(ctx, orgID, m)
}
// PatchSecrets patches all provided secrets and updates any previous values.
func (s *Service) PatchSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
err := s.s.Update(ctx, func(tx kv.Tx) error {
for k, v := range m {
err := s.s.PutSecret(ctx, tx, orgID, k, v)
if err != nil {
return err
}
}
return nil
})
return err
}
// DeleteSecret removes a single secret from the secret store.
func (s *Service) DeleteSecret(ctx context.Context, orgID influxdb.ID, ks ...string) error {
err := s.s.Update(ctx, func(tx kv.Tx) error {
for _, k := range ks {
err := s.s.DeleteSecret(ctx, tx, orgID, k)
if err != nil {
return err
}
}
return nil
})
return err
}

33
secret/service_test.go Normal file
View File

@ -0,0 +1,33 @@
package secret_test
import (
"context"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/inmem"
"github.com/influxdata/influxdb/v2/secret"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
)
func TestBoltSecretService(t *testing.T) {
influxdbtesting.SecretService(initSvc, t)
}
func initSvc(f influxdbtesting.SecretServiceFields, t *testing.T) (influxdb.SecretService, func()) {
s := inmem.NewKVStore()
storage, err := secret.NewStore(s)
if err != nil {
t.Fatal(err)
}
svc := secret.NewService(storage)
for _, s := range f.Secrets {
if err := svc.PutSecrets(context.Background(), s.OrganizationID, s.Env); err != nil {
t.Fatalf("failed to populate users")
}
}
return svc, func() {}
}

193
secret/storage.go Normal file
View File

@ -0,0 +1,193 @@
package secret
import (
"context"
"encoding/base64"
"errors"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
)
var secretBucket = []byte("secretsv1")
// Storage is a store translation layer between the data storage unit and the
// service layer.
type Storage struct {
store kv.Store
}
// NewStore creates a new storage system
func NewStore(s kv.Store) (*Storage, error) {
err := s.Update(context.Background(), func(tx kv.Tx) error {
if _, err := tx.Bucket(secretBucket); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return &Storage{s}, nil
}
func (s *Storage) View(ctx context.Context, fn func(kv.Tx) error) error {
return s.store.View(ctx, fn)
}
func (s *Storage) Update(ctx context.Context, fn func(kv.Tx) error) error {
return s.store.Update(ctx, fn)
}
// GetSecret Returns the value of a secret
func (s *Storage) GetSecret(ctx context.Context, tx kv.Tx, orgID influxdb.ID, k string) (string, error) {
key, err := encodeSecretKey(orgID, k)
if err != nil {
return "", err
}
b, err := tx.Bucket(secretBucket)
if err != nil {
return "", err
}
val, err := b.Get(key)
if kv.IsNotFound(err) {
return "", &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrSecretNotFound,
}
}
if err != nil {
return "", err
}
v, err := decodeSecretValue(val)
if err != nil {
return "", err
}
return v, nil
}
// ListSecrets returns a list of secret keys
func (s *Storage) ListSecret(ctx context.Context, tx kv.Tx, orgID influxdb.ID) ([]string, error) {
b, err := tx.Bucket(secretBucket)
if err != nil {
return nil, err
}
prefix, err := orgID.Encode()
if err != nil {
return nil, err
}
cur, err := b.ForwardCursor(prefix, kv.WithCursorPrefix(prefix))
if err != nil {
return nil, err
}
keys := []string{}
err = kv.WalkCursor(ctx, cur, func(k, v []byte) error {
id, key, err := decodeSecretKey(k)
if err != nil {
return err
}
if id != orgID {
// We've reached the end of the keyspace for the provided orgID
return nil
}
keys = append(keys, key)
return nil
})
if err != nil {
return nil, err
}
return keys, nil
}
// PutSecret sets a secret in the db.
func (s *Storage) PutSecret(ctx context.Context, tx kv.Tx, orgID influxdb.ID, k, v string) error {
key, err := encodeSecretKey(orgID, k)
if err != nil {
return err
}
val := encodeSecretValue(v)
b, err := tx.Bucket(secretBucket)
if err != nil {
return err
}
if err := b.Put(key, val); err != nil {
return err
}
return nil
}
// DeleteSecret removes a secret for the db
func (s *Storage) DeleteSecret(ctx context.Context, tx kv.Tx, orgID influxdb.ID, k string) error {
key, err := encodeSecretKey(orgID, k)
if err != nil {
return err
}
b, err := tx.Bucket(secretBucket)
if err != nil {
return err
}
return b.Delete(key)
}
func encodeSecretKey(orgID influxdb.ID, k string) ([]byte, error) {
buf, err := orgID.Encode()
if err != nil {
return nil, err
}
key := make([]byte, 0, influxdb.IDLength+len(k))
key = append(key, buf...)
key = append(key, k...)
return key, nil
}
func decodeSecretKey(key []byte) (influxdb.ID, string, error) {
if len(key) < influxdb.IDLength {
// This should not happen.
return influxdb.InvalidID(), "", errors.New("provided key is too short to contain an ID (please report this error)")
}
var id influxdb.ID
if err := id.Decode(key[:influxdb.IDLength]); err != nil {
return influxdb.InvalidID(), "", err
}
k := string(key[influxdb.IDLength:])
return id, k, nil
}
func decodeSecretValue(val []byte) (string, error) {
// store the secret value base64 encoded so that it's marginally better than plaintext
v, err := base64.StdEncoding.DecodeString(string(val))
if err != nil {
return "", err
}
return string(v), nil
}
func encodeSecretValue(v string) []byte {
val := make([]byte, base64.StdEncoding.EncodedLen(len(v)))
base64.StdEncoding.Encode(val, []byte(v))
return val
}