diff --git a/client/client.go b/client/client.go index 0d74096..10b2a57 100644 --- a/client/client.go +++ b/client/client.go @@ -2,6 +2,8 @@ package client import ( "context" + "fmt" + "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -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) } @@ -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 diff --git a/client/gcs_service.go b/client/gcs_service.go index 072983a..1301390 100644 --- a/client/gcs_service.go +++ b/client/gcs_service.go @@ -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") @@ -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) } @@ -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) } @@ -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} } diff --git a/client/monart.go b/client/monart.go index 1c73e3b..fa12d3e 100644 --- a/client/monart.go +++ b/client/monart.go @@ -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 } diff --git a/client/service_test.go b/client/service_test.go index dff657a..26bcef0 100644 --- a/client/service_test.go +++ b/client/service_test.go @@ -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" @@ -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) { @@ -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) diff --git a/cmd/dolores/config.go b/cmd/dolores/config.go index 5508a06..c6493e4 100644 --- a/cmd/dolores/config.go +++ b/cmd/dolores/config.go @@ -2,6 +2,7 @@ package main import ( "context" + "os" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -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 } @@ -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", @@ -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, + } +} diff --git a/cmd/dolores/main.go b/cmd/dolores/main.go index 6355cc6..2456ae5 100644 --- a/cmd/dolores/main.go +++ b/cmd/dolores/main.go @@ -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 diff --git a/secrets/manager.go b/secrets/manager.go index 51b9f8e..ba0106d 100644 --- a/secrets/manager.go +++ b/secrets/manager.go @@ -7,6 +7,7 @@ import ( "io" "os" "strings" + "time" "github.com/rs/zerolog" "github.com/scalescape/dolores" @@ -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 { @@ -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) + updatedAt := obj.UpdatedAt.Format(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} } diff --git a/store/google/gcs.go b/store/google/gcs.go index 9d5e417..a0dd500 100644 --- a/store/google/gcs.go +++ b/store/google/gcs.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "time" "cloud.google.com/go/storage" "github.com/rs/zerolog/log" @@ -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"` @@ -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() @@ -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