Skip to content

Commit

Permalink
Command to support listing secrets closes #8
Browse files Browse the repository at this point in the history
Adding list secrets feature closing #8
  • Loading branch information
shubhamvernekar authored Nov 9, 2023
1 parent 36ab909 commit f36e031
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 10 deletions.
29 changes: 29 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package client

import (
"context"
"fmt"
"time"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
Expand All @@ -23,6 +25,13 @@ type EncryptedConfig struct {
Data string `json:"data"`
}

type SecretObject struct {
Name string `json:"name"`
Location string `json:"location"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func (c *Client) Init(ctx context.Context, bucket string, cfg Configuration) error {
return c.Service.Init(ctx, bucket, cfg)
}
Expand Down Expand Up @@ -77,6 +86,26 @@ func (c *Client) GetOrgPublicKeys(env string) (OrgPublicKeys, error) {
return OrgPublicKeys{Recipients: recps}, nil
}

type SecretListConfig struct {
Environment string `json:"environment"`
}
type SecretListResponse struct {
Secrets []SecretObject `json:"secrets"`
}

func (c *Client) GetSecretList(_ SecretListConfig) ([]SecretObject, error) {
resp, err := c.Service.ListObject(c.ctx, c.bucket, c.prefix)
if err != nil {
return nil, err
}
objs := make([]SecretObject, 0)
for _, obj := range resp {
o := SecretObject{Name: obj.Name, CreatedAt: obj.Created, UpdatedAt: obj.Updated, Location: fmt.Sprintf("%s/%s", obj.Bucket, obj.Name)}
objs = append(objs, o)
}
return objs, nil
}

func New(ctx context.Context, cfg config.Client) (*Client, error) {
if err := cfg.Valid(); err != nil {
return nil, err
Expand Down
15 changes: 12 additions & 3 deletions client/gcs_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/rs/zerolog/log"
"github.com/scalescape/dolores/config"
"github.com/scalescape/dolores/store/google"
)

var ErrInvalidPublicKeys = errors.New("invalid public keys")
Expand All @@ -23,7 +24,7 @@ type Service struct {
type gcsStore interface {
WriteToObject(ctx context.Context, bucketName, fileName string, data []byte) error
ReadObject(ctx context.Context, bucketName, fileName string) ([]byte, error)
ListOjbect(ctx context.Context, bucketName, path string) ([]string, error)
ListObject(ctx context.Context, bucketName, path string) ([]google.Object, error)
ExistsObject(ctx context.Context, bucketName, fileName string) (bool, error)
}

Expand Down Expand Up @@ -77,13 +78,13 @@ func (s Service) GetOrgPublicKeys(ctx context.Context, env, bucketName, path str
if pubKey != "" {
return []string{pubKey}, nil
}
resp, err := s.store.ListOjbect(ctx, bucketName, path)
resp, err := s.ListObject(ctx, bucketName, path)
if err != nil {
return nil, fmt.Errorf("error listing objects: %w", err)
}
keys := make([]string, len(resp))
for i, obj := range resp {
key, err := s.store.ReadObject(ctx, bucketName, obj)
key, err := s.store.ReadObject(ctx, bucketName, obj.Name)
if err != nil {
return nil, fmt.Errorf("failed to read object %s: %w", obj, err)
}
Expand Down Expand Up @@ -137,6 +138,14 @@ func (s Service) saveObject(ctx context.Context, bucket, fname string, md any) e
return s.store.WriteToObject(ctx, bucket, fname, data)
}

func (s Service) ListObject(ctx context.Context, bucket, path string) ([]google.Object, error) {
resp, err := s.store.ListObject(ctx, bucket, path)
if err != nil {
return nil, err
}
return resp, nil
}

func NewService(st gcsStore) Service {
return Service{store: st}
}
13 changes: 13 additions & 0 deletions client/monart.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ func (s MonartClient) FetchSecrets(fetchReq FetchSecretRequest) ([]byte, error)
return sec, nil
}

func (s MonartClient) GetSecretList(cfg SecretListConfig) ([]SecretObject, error) {
path := fmt.Sprintf("environment/%s/secrets", cfg.Environment)
req, err := http.NewRequest(http.MethodGet, s.serverURL(path), nil)
if err != nil {
return nil, fmt.Errorf("unable to build Get SecretList request: %w", err)
}
result := new(SecretListResponse)
if _, err := s.call(req, &result); err != nil {
return nil, err
}
return result.Secrets, nil
}

func (s MonartClient) serverURL(path string) string {
return "https://relyonmetrics.com/api/" + path
}
Expand Down
9 changes: 5 additions & 4 deletions client/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/scalescape/dolores/client"
"github.com/scalescape/dolores/config"
"github.com/scalescape/dolores/store/google"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
Expand Down Expand Up @@ -38,9 +39,9 @@ func (m *mockGCS) ReadObject(ctx context.Context, bucketName, fileName string) (
return args.Get(0).([]byte), args.Error(1)
}

func (m *mockGCS) ListOjbect(ctx context.Context, bucketName, path string) ([]string, error) {
func (m *mockGCS) ListObject(ctx context.Context, bucketName, path string) ([]google.Object, error) {
args := m.Called(ctx, bucketName, path)
return args.Get(0).([]string), args.Error(1)
return args.Get(0).([]google.Object), args.Error(1)
}

func (m *mockGCS) ExistsObject(ctx context.Context, bucketName, fileName string) (bool, error) {
Expand Down Expand Up @@ -69,8 +70,8 @@ func (s *serviceSuite) TestShouldNotOverwriteMetadata() {
PublicKey: "public_key",
Metadata: config.Metadata{Location: "secrets"},
UserID: "test_user"}
s.gcs.On("ExistsObject", mock.AnythingOfType("*context.emptyCtx"), s.bucket, name).Return(true, nil).Once()
s.gcs.On("WriteToObject", mock.AnythingOfType("*context.emptyCtx"), s.bucket, "secrets/keys/test_user.key", []byte(cfg.PublicKey)).Return(nil).Once()
s.gcs.On("ExistsObject", mock.AnythingOfType("context.backgroundCtx"), s.bucket, name).Return(true, nil).Once()
s.gcs.On("WriteToObject", mock.AnythingOfType("context.backgroundCtx"), s.bucket, "secrets/keys/test_user.key", []byte(cfg.PublicKey)).Return(nil).Once()

err := s.Service.Init(s.ctx, s.bucket, cfg)

Expand Down
21 changes: 21 additions & 0 deletions cmd/dolores/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"os"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -30,6 +31,7 @@ func NewConfig(client GetClient) *ConfigCommand {
cfg.Subcommands = append(cfg.Subcommands, EncryptCommand(cfg.encryptAction))
cfg.Subcommands = append(cfg.Subcommands, DecryptCommand(cfg.decryptAction))
cfg.Subcommands = append(cfg.Subcommands, EditCommand(cfg.editAction))
cfg.Subcommands = append(cfg.Subcommands, ListSecretCommand(cfg.listSecretAction))
return cfg
}

Expand Down Expand Up @@ -74,6 +76,17 @@ func (c *ConfigCommand) decryptAction(ctx *cli.Context) error {
return sec.Decrypt(req)
}

func (c *ConfigCommand) listSecretAction(ctx *cli.Context) error {
env := ctx.String("environment")
log := c.log.With().Str("cmd", "config.list").Str("environment", env).Logger()
secMan := secrets.NewSecretsManager(log, c.rcli(ctx.Context))
req := secrets.ListSecretConfig{Environment: env, Out: os.Stdout}
if err := secMan.ListSecret(req); err != nil {
return err
}
return nil
}

func EncryptCommand(action cli.ActionFunc) *cli.Command {
return &cli.Command{
Name: "encrypt",
Expand Down Expand Up @@ -134,3 +147,11 @@ func EditCommand(action cli.ActionFunc) *cli.Command {
Action: action,
}
}

func ListSecretCommand(action cli.ActionFunc) *cli.Command {
return &cli.Command{
Name: "list",
Usage: "shows the list of secrets uploaded in cloud",
Action: action,
}
}
1 change: 1 addition & 0 deletions cmd/dolores/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type secretsClient interface {
FetchSecrets(req client.FetchSecretRequest) ([]byte, error)
GetOrgPublicKeys(env string) (client.OrgPublicKeys, error)
Init(ctx context.Context, bucket string, cfg client.Configuration) error
GetSecretList(req client.SecretListConfig) ([]client.SecretObject, error)
}

type CtxKey string
Expand Down
55 changes: 55 additions & 0 deletions secrets/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"os"
"strings"
"time"

"github.com/rs/zerolog"
"github.com/scalescape/dolores"
Expand All @@ -17,6 +18,7 @@ type secClient interface {
FetchSecrets(req client.FetchSecretRequest) ([]byte, error)
UploadSecrets(req client.EncryptedConfig) error
GetOrgPublicKeys(env string) (client.OrgPublicKeys, error)
GetSecretList(cfg client.SecretListConfig) ([]client.SecretObject, error)
}

type EncryptConfig struct {
Expand Down Expand Up @@ -130,6 +132,59 @@ func (sm SecretManager) Decrypt(cfg DecryptConfig) error {
return nil
}

type ListSecretConfig struct {
Environment string
Out io.Writer
}

func (c ListSecretConfig) output() io.Writer {
if c.Out == nil {
return os.Stdout
}
return c.Out
}

func (c ListSecretConfig) Valid() error {
env := strings.ToLower(c.Environment)
if env != "production" && env != "staging" {
return ErrInvalidEnvironment
}
return nil
}

func (sm SecretManager) ListSecret(cfg ListSecretConfig) error {
if err := cfg.Valid(); err != nil {
return fmt.Errorf("invalid config: %w", err)
}
req := client.SecretListConfig{Environment: cfg.Environment}
resp, err := sm.client.GetSecretList(req)
if err != nil {
return fmt.Errorf("failed to get secrets: %w", err)
}

lineFormat := "%-10s %-65s %-30s %-30s\n"
header := []byte(fmt.Sprintf(lineFormat, "Name", "Location", "Created At (UTC)", "Updated At (UTC)"))
if _, err := cfg.output().Write(header); err != nil {
return err
}
for _, obj := range resp {
if !strings.HasSuffix(obj.Name, ".key") && !strings.HasSuffix(obj.Name, "/") {
arr := strings.SplitN(obj.Name, "/", 2)
name := obj.Name
if len(arr) == 2 {
name = arr[1]
}
createdAt := obj.CreatedAt.Format(time.DateTime)

Check failure on line 177 in secrets/manager.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: time.DateTime

Check failure on line 177 in secrets/manager.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: time.DateTime

Check failure on line 177 in secrets/manager.go

View workflow job for this annotation

GitHub Actions / Build app

undefined: time.DateTime
updatedAt := obj.UpdatedAt.Format(time.DateTime)

Check failure on line 178 in secrets/manager.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: time.DateTime (typecheck)

Check failure on line 178 in secrets/manager.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: time.DateTime) (typecheck)

Check failure on line 178 in secrets/manager.go

View workflow job for this annotation

GitHub Actions / Build app

undefined: time.DateTime
line := []byte(fmt.Sprintf(lineFormat, name, obj.Location, createdAt, updatedAt))
if _, err := cfg.output().Write(line); err != nil {
return err
}
}
}
return nil
}

func NewSecretsManager(log zerolog.Logger, rcli secClient) SecretManager {
return SecretManager{client: rcli, log: log}
}
15 changes: 12 additions & 3 deletions store/google/gcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os"
"time"

"cloud.google.com/go/storage"
"github.com/rs/zerolog/log"
Expand All @@ -25,6 +26,13 @@ type Config struct {
ServiceAccountFile string
}

type Object struct {
Name string `json:"name"`
Bucket string `json:"bucket"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}

type ServiceAccount struct {
Type string `json:"type"`
ProjectID string `json:"project_id"`
Expand Down Expand Up @@ -105,12 +113,12 @@ func (s StorageClient) ListBuckets(ctx context.Context) ([]string, error) {
return buckets, nil
}

func (s StorageClient) ListOjbect(ctx context.Context, bucketName, path string) ([]string, error) {
func (s StorageClient) ListObject(ctx context.Context, bucketName, path string) ([]Object, error) {
bucket := s.Client.Bucket(bucketName)
if _, err := bucket.Attrs(ctx); err != nil {
return nil, fmt.Errorf("failed to get bucket: %w", err)
}
objs := make([]string, 0)
objs := make([]Object, 0)
iter := bucket.Objects(ctx, &storage.Query{Prefix: path})
for {
attrs, err := iter.Next()
Expand All @@ -120,7 +128,8 @@ func (s StorageClient) ListOjbect(ctx context.Context, bucketName, path string)
if err != nil {
return nil, fmt.Errorf("failed to iterate object list: %w", err)
}
objs = append(objs, attrs.Name)
o := Object{Name: attrs.Name, Created: attrs.Created, Updated: attrs.Updated, Bucket: attrs.Bucket}
objs = append(objs, o)
}
log.Trace().Msgf("list of objects from path: %s %+v", path, objs)
return objs, nil
Expand Down

0 comments on commit f36e031

Please sign in to comment.