Skip to content

Commit

Permalink
Merge pull request #2312 from posit-dev/dotnomad/post-secrets-api
Browse files Browse the repository at this point in the history
Add POST configuration secrets endpoint
  • Loading branch information
dotNomad authored Sep 26, 2024
2 parents b77d14d + 9019fbf commit 02e6eb0
Show file tree
Hide file tree
Showing 5 changed files with 334 additions and 0 deletions.
21 changes: 21 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,24 @@ func (cfg *Config) WriteFile(path util.AbsolutePath) error {
defer f.Close()
return cfg.Write(f)
}

func (cfg *Config) AddSecret(secret string) error {
// Check if the secret already exists before adding
for _, s := range cfg.Secrets {
if s == secret {
return nil // Secret already exists, no need to add
}
}
cfg.Secrets = append(cfg.Secrets, secret)
return nil
}

func (cfg *Config) RemoveSecret(secret string) error {
for i, s := range cfg.Secrets {
if s == secret {
cfg.Secrets = append(cfg.Secrets[:i], cfg.Secrets[i+1:]...)
break
}
}
return nil
}
41 changes: 41 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,44 @@ func (s *ConfigSuite) TestReadComments() {

s.Equal([]string{" These are comments.", " They will be preserved."}, cfg.Comments)
}

func (s *ConfigSuite) TestApplySecretActionAdd() {
cfg := New()
cfg.Secrets = []string{}
err := cfg.AddSecret("secret1")
s.NoError(err)
s.Equal([]string{"secret1"}, cfg.Secrets)
}

func (s *ConfigSuite) TestApplySecretActionAddWithExistingSecrets() {
cfg := New()
cfg.Secrets = []string{"existingSecret1", "existingSecret2"}
err := cfg.AddSecret("newSecret")
s.NoError(err)
s.Equal([]string{"existingSecret1", "existingSecret2", "newSecret"}, cfg.Secrets)
}

func (s *ConfigSuite) TestApplySecretActionAddNoDuplicates() {
cfg := New()
cfg.Secrets = []string{"existingSecret1", "existingSecret2"}

err := cfg.AddSecret("existingSecret1")
s.NoError(err)
s.Equal([]string{"existingSecret1", "existingSecret2"}, cfg.Secrets)
}

func (s *ConfigSuite) TestApplySecretActionRemove() {
cfg := New()
cfg.Secrets = []string{"secret1", "secret2"}
err := cfg.RemoveSecret("secret1")
s.NoError(err)
s.Equal([]string{"secret2"}, cfg.Secrets)
}

func (s *ConfigSuite) TestApplySecretActionRemoveFromEmptySecrets() {
cfg := New()
cfg.Secrets = []string{}
err := cfg.RemoveSecret("nonexistentSecret")
s.NoError(err)
s.Equal([]string{}, cfg.Secrets)
}
4 changes: 4 additions & 0 deletions internal/services/api/api_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ func RouterHandlerFunc(base util.AbsolutePath, lister accounts.AccountList, log
r.Handle(ToPath("configurations", "{name}", "secrets"), GetConfigSecretsHandlerFunc(base, log)).
Methods(http.MethodGet)

// POST /api/configurations/$NAME/secrets
r.Handle(ToPath("configurations", "{name}", "secrets"), PostConfigSecretsHandlerFunc(base, log)).
Methods(http.MethodPost)

// GET /api/configurations/$NAME/packages/python
r.Handle(ToPath("configurations", "{name}", "packages", "python"), NewGetConfigPythonPackagesHandler(base, log)).
Methods(http.MethodGet)
Expand Down
113 changes: 113 additions & 0 deletions internal/services/api/post_config_secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package api

// Copyright (C) 2024 by Posit Software, PBC.

import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/http"

"github.com/gorilla/mux"
"github.com/posit-dev/publisher/internal/config"
"github.com/posit-dev/publisher/internal/logging"
"github.com/posit-dev/publisher/internal/types"
"github.com/posit-dev/publisher/internal/util"
)

const (
secretActionAdd = "add"
secretActionRemove = "remove"
)

type postConfigSecretsRequest struct {
Action string `json:"action"`
Secret string `json:"secret"`
}

func applySecretAction(cfg *config.Config, action string, secret string) error {
switch action {
case secretActionAdd:
return cfg.AddSecret(secret)
case secretActionRemove:
return cfg.RemoveSecret(secret)
default:
return fmt.Errorf("unknown action: %s", action)
}
}

func PostConfigSecretsHandlerFunc(base util.AbsolutePath, log logging.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
name := mux.Vars(req)["name"]

projectDir, relProjectDir, err := ProjectDirFromRequest(base, w, req, log)
if err != nil {
// Response already returned by ProjectDirFromRequest
return
}

configPath := config.GetConfigPath(projectDir, name)
cfg, err := configFromFile(configPath)
if err != nil {
if aerr, ok := err.(*types.AgentError); ok {
if aerr.Code == types.ErrorUnknownTOMLKey {
apiErr := APIErrorUnknownTOMLKeyFromAgentError(*aerr)
apiErr.JSONResponse(w)
return
}

if aerr.Code == types.ErrorInvalidTOML {
apiErr := APIErrorInvalidTOMLFileFromAgentError(*aerr)
apiErr.JSONResponse(w)
return
}
}

if errors.Is(err, fs.ErrNotExist) {
http.NotFound(w, req)
} else {
InternalError(w, req, log, err)
}
return
}

dec := json.NewDecoder(req.Body)
dec.DisallowUnknownFields()
var b postConfigSecretsRequest
err = dec.Decode(&b)
if err != nil {
BadRequest(w, req, log, err)
return
}

err = applySecretAction(cfg, b.Action, b.Secret)
if err != nil {
BadRequest(w, req, log, err)
return
}

err = cfg.WriteFile(configPath)
if err != nil {
InternalError(w, req, log, err)
return
}

relPath, err := configPath.Rel(base)
if err != nil {
InternalError(w, req, log, err)
return
}

response := &configDTO{
configLocation: configLocation{
Name: name,
Path: configPath.String(),
RelPath: relPath.String(),
},
ProjectDir: relProjectDir.String(),
Configuration: cfg,
}
JsonResult(w, http.StatusOK, response)
}
}
155 changes: 155 additions & 0 deletions internal/services/api/post_config_secrets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package api

// Copyright (C) 2024 by Posit Software, PBC.

import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/gorilla/mux"
"github.com/posit-dev/publisher/internal/config"
"github.com/posit-dev/publisher/internal/logging"
"github.com/posit-dev/publisher/internal/util"
"github.com/posit-dev/publisher/internal/util/utiltest"
"github.com/spf13/afero"
"github.com/stretchr/testify/suite"
)

type ApplySecretActionSuite struct {
utiltest.Suite
}

func TestApplySecretActionSuite(t *testing.T) {
suite.Run(t, new(ApplySecretActionSuite))
}

func (s *ApplySecretActionSuite) TestApplySecretActionAdd() {
cfg := config.New()
cfg.Secrets = []string{}
err := applySecretAction(cfg, secretActionAdd, "secret1")
s.NoError(err)
s.Equal([]string{"secret1"}, cfg.Secrets)
}

func (s *ApplySecretActionSuite) TestApplySecretActionRemove() {
cfg := config.New()
cfg.Secrets = []string{"secret1", "secret2"}
err := applySecretAction(cfg, secretActionRemove, "secret1")
s.NoError(err)
s.Equal([]string{"secret2"}, cfg.Secrets)
}

func (s *ApplySecretActionSuite) TestApplySecretActionUnknownAction() {
cfg := config.New()
err := applySecretAction(cfg, "invalidAction", "someSecret")
s.Error(err)
s.Equal("unknown action: invalidAction", err.Error())
}

type PostConfigSecretsSuite struct {
utiltest.Suite
cwd util.AbsolutePath
log logging.Logger
h http.HandlerFunc
}

func TestPostConfigSecretsSuite(t *testing.T) {
suite.Run(t, new(PostConfigSecretsSuite))
}

func (s *PostConfigSecretsSuite) SetupSuite() {
s.log = logging.New()
}

func (s *PostConfigSecretsSuite) SetupTest() {
fs := afero.NewMemMapFs()
cwd, err := util.Getwd(fs)
s.Nil(err)
s.cwd = cwd
s.h = PostConfigSecretsHandlerFunc(s.cwd, s.log)
}

func (s *PostConfigSecretsSuite) TestPostConfigSecretsAdd() {
cfg := config.New()
cfg.Type = config.ContentTypeHTML
err := cfg.WriteFile(config.GetConfigPath(s.cwd, "myConfig"))
s.NoError(err)

body := strings.NewReader(`{"action": "add", "secret": "test_secret"}`)
rec := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/configurations/myConfig/secrets", body)
s.NoError(err)
req = mux.SetURLVars(req, map[string]string{"name": "myConfig"})

s.h(rec, req)

s.Equal(http.StatusOK, rec.Result().StatusCode)
s.Equal("application/json", rec.Header().Get("content-type"))

var res configDTO
dec := json.NewDecoder(rec.Body)
dec.DisallowUnknownFields()
s.NoError(dec.Decode(&res))
s.NotNil(res)
s.Equal([]string{"test_secret"}, res.Configuration.Secrets)
}

func (s *PostConfigSecretsSuite) TestPostConfigSecretsRemove() {
cfg := config.New()
cfg.Type = config.ContentTypeHTML
cfg.Secrets = []string{"existing_secret", "test_secret"}
err := cfg.WriteFile(config.GetConfigPath(s.cwd, "myConfig"))
s.NoError(err)

body := strings.NewReader(`{"action": "remove", "secret": "test_secret"}`)
rec := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/configurations/myConfig/secrets", body)
s.NoError(err)
req = mux.SetURLVars(req, map[string]string{"name": "myConfig"})

s.h(rec, req)

s.Equal(http.StatusOK, rec.Result().StatusCode)
s.Equal("application/json", rec.Header().Get("content-type"))

var res configDTO
dec := json.NewDecoder(rec.Body)
dec.DisallowUnknownFields()
s.NoError(dec.Decode(&res))
s.NotNil(res)
s.Equal([]string{"existing_secret"}, res.Configuration.Secrets)
}

func (s *PostConfigSecretsSuite) TestPostConfigSecretsNotFound() {
rec := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/configurations/myConfig/secrets", nil)
s.NoError(err)
req = mux.SetURLVars(req, map[string]string{"name": "myConfig"})

s.h(rec, req)

s.Equal(http.StatusNotFound, rec.Result().StatusCode)
}

func (s *PostConfigSecretsSuite) TestPostConfigSecretsInvalidAction() {
cfg := config.New()
cfg.Type = config.ContentTypeHTML
err := cfg.WriteFile(config.GetConfigPath(s.cwd, "myConfig"))
s.NoError(err)

body := strings.NewReader(`{"action": "invalid", "secret": "test_secret"}`)
rec := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/configurations/myConfig/secrets", body)
s.NoError(err)
req = mux.SetURLVars(req, map[string]string{"name": "myConfig"})

s.h(rec, req)

s.Equal(http.StatusBadRequest, rec.Result().StatusCode)

bodyRes := rec.Body.String()
s.Contains(bodyRes, "Bad Request: unknown action: invalid")
}

0 comments on commit 02e6eb0

Please sign in to comment.