support for per-organization secrets
This commit is contained in:
parent
727177da13
commit
96132e3d0a
34 changed files with 1819 additions and 6 deletions
|
@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- support for Cron job name in Yaml when block, by [@bradrydzewski](https://github.com/bradrydzewski). [#2628](https://github.com/drone/drone/issues/2628).
|
||||
- sqlite username column changed to case-insensitive, by [@bradrydzewski](https://github.com/bradrydzewski).
|
||||
- endpoint to purge repository from database, by [@bradrydzewski](https://github.com/bradrydzewski).
|
||||
- support for per-organization secrets, by [@bradrydzewski](https://github.com/bradrydzewski).
|
||||
- update drone-yaml from version 1.0.6 to 1.0.8.
|
||||
- update drone-runtime from version 1.0.4 to 1.0.6.
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/drone/drone/store/perm"
|
||||
"github.com/drone/drone/store/repos"
|
||||
"github.com/drone/drone/store/secret"
|
||||
"github.com/drone/drone/store/secret/global"
|
||||
"github.com/drone/drone/store/shared/db"
|
||||
"github.com/drone/drone/store/shared/encrypt"
|
||||
"github.com/drone/drone/store/stage"
|
||||
|
@ -47,6 +48,7 @@ var storeSet = wire.NewSet(
|
|||
cron.New,
|
||||
perm.New,
|
||||
secret.New,
|
||||
global.New,
|
||||
step.New,
|
||||
)
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/drone/drone/store/cron"
|
||||
"github.com/drone/drone/store/perm"
|
||||
"github.com/drone/drone/store/secret"
|
||||
"github.com/drone/drone/store/secret/global"
|
||||
"github.com/drone/drone/store/step"
|
||||
"github.com/drone/drone/trigger"
|
||||
cron2 "github.com/drone/drone/trigger/cron"
|
||||
|
@ -70,8 +71,9 @@ func InitializeApplication(config2 config.Config) (application, error) {
|
|||
return application{}, err
|
||||
}
|
||||
secretStore := secret.New(db, encrypter)
|
||||
globalSecretStore := global.New(db, encrypter)
|
||||
stepStore := step.New(db)
|
||||
buildManager := manager.New(buildStore, configService, corePubsub, logStore, logStream, netrcService, repositoryStore, scheduler, secretStore, statusService, stageStore, stepStore, system, userStore, webhookSender)
|
||||
buildManager := manager.New(buildStore, configService, corePubsub, logStore, logStream, netrcService, repositoryStore, scheduler, secretStore, globalSecretStore, statusService, stageStore, stepStore, system, userStore, webhookSender)
|
||||
secretService := provideSecretPlugin(config2)
|
||||
registryService := provideRegistryPlugin(config2)
|
||||
runner := provideRunner(buildManager, secretService, registryService, config2)
|
||||
|
@ -82,7 +84,7 @@ func InitializeApplication(config2 config.Config) (application, error) {
|
|||
session := provideSession(userStore, config2)
|
||||
batcher := batch.New(db)
|
||||
syncer := provideSyncer(repositoryService, repositoryStore, userStore, batcher, config2)
|
||||
server := api.New(buildStore, commitService, cronStore, corePubsub, hookService, logStore, coreLicense, licenseService, permStore, repositoryStore, repositoryService, scheduler, secretStore, stageStore, stepStore, statusService, session, logStream, syncer, system, triggerer, userStore, webhookSender)
|
||||
server := api.New(buildStore, commitService, cronStore, corePubsub, globalSecretStore, hookService, logStore, coreLicense, licenseService, permStore, repositoryStore, repositoryService, scheduler, secretStore, stageStore, stepStore, statusService, session, logStream, syncer, system, triggerer, userStore, webhookSender)
|
||||
organizationService := orgs.New(client, renewer)
|
||||
userService := user.New(client)
|
||||
admissionService := provideAdmissionPlugin(client, organizationService, userService, config2)
|
||||
|
|
|
@ -33,7 +33,9 @@ type (
|
|||
Secret struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
RepoID int64 `json:"repo_id,omitempty"`
|
||||
Namespace string `json:"repo_namespace,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
PullRequest bool `json:"pull_request,omitempty"`
|
||||
PullRequestPush bool `json:"pull_request_push,omitempty"`
|
||||
|
@ -69,6 +71,32 @@ type (
|
|||
Delete(context.Context, *Secret) error
|
||||
}
|
||||
|
||||
// GlobalSecretStore manages global secrets accessible to
|
||||
// all repositories in the system.
|
||||
GlobalSecretStore interface {
|
||||
// List returns a secret list from the datastore.
|
||||
List(ctx context.Context, namespace string) ([]*Secret, error)
|
||||
|
||||
// ListAll returns a secret list from the datastore
|
||||
// for all namespaces.
|
||||
ListAll(ctx context.Context) ([]*Secret, error)
|
||||
|
||||
// Find returns a secret from the datastore.
|
||||
Find(ctx context.Context, id int64) (*Secret, error)
|
||||
|
||||
// FindName returns a secret from the datastore.
|
||||
FindName(ctx context.Context, namespace, name string) (*Secret, error)
|
||||
|
||||
// Create persists a new secret to the datastore.
|
||||
Create(ctx context.Context, secret *Secret) error
|
||||
|
||||
// Update persists an updated secret to the datastore.
|
||||
Update(ctx context.Context, secret *Secret) error
|
||||
|
||||
// Delete deletes a secret from the datastore.
|
||||
Delete(ctx context.Context, secret *Secret) error
|
||||
}
|
||||
|
||||
// SecretService provides secrets from an external service.
|
||||
SecretService interface {
|
||||
// Find returns a named secret from the global remote service.
|
||||
|
@ -95,7 +123,9 @@ func (s *Secret) Copy() *Secret {
|
|||
return &Secret{
|
||||
ID: s.ID,
|
||||
RepoID: s.RepoID,
|
||||
Namespace: s.Namespace,
|
||||
Name: s.Name,
|
||||
Type: s.Type,
|
||||
PullRequest: s.PullRequest,
|
||||
PullRequestPush: s.PullRequestPush,
|
||||
}
|
||||
|
|
|
@ -47,6 +47,8 @@ func TestSecretSafeCopy(t *testing.T) {
|
|||
ID: 1,
|
||||
RepoID: 2,
|
||||
Name: "docker_password",
|
||||
Namespace: "octocat",
|
||||
Type: "",
|
||||
Data: "correct-horse-battery-staple",
|
||||
PullRequest: true,
|
||||
PullRequestPush: true,
|
||||
|
@ -61,6 +63,9 @@ func TestSecretSafeCopy(t *testing.T) {
|
|||
if got, want := after.Name, before.Name; got != want {
|
||||
t.Errorf("Want secret Name %s, got %s", want, got)
|
||||
}
|
||||
if got, want := after.Namespace, before.Namespace; got != want {
|
||||
t.Errorf("Want secret Namespace %s, got %s", want, got)
|
||||
}
|
||||
if got, want := after.PullRequest, before.PullRequest; got != want {
|
||||
t.Errorf("Want secret PullRequest %v, got %v", want, got)
|
||||
}
|
||||
|
|
1
go.sum
1
go.sum
|
@ -65,6 +65,7 @@ github.com/drone/go-login v1.0.3 h1:YmZMUoWWd3QrgmobC1DcExFjW7w2ZEBO1R1VeeobIRU=
|
|||
github.com/drone/go-login v1.0.3/go.mod h1:FLxy9vRzLbyBxoCJYxGbG9R0WGn6OyuvBmAtYNt43uw=
|
||||
github.com/drone/go-login v1.0.4-0.20190308175602-213d1719faed h1:Y0qiKFf6gsgTRTQS1roMh7kKVyrx+HSQmFsIgcZsHsM=
|
||||
github.com/drone/go-login v1.0.4-0.20190308175602-213d1719faed/go.mod h1:FLxy9vRzLbyBxoCJYxGbG9R0WGn6OyuvBmAtYNt43uw=
|
||||
github.com/drone/go-login v1.0.4-0.20190311170324-2a4df4f242a2 h1:RGpgNkowJc5LAVn/ZONx70qmnaTA0z/3hHPzTBdAEO8=
|
||||
github.com/drone/go-login v1.0.4-0.20190311170324-2a4df4f242a2/go.mod h1:FLxy9vRzLbyBxoCJYxGbG9R0WGn6OyuvBmAtYNt43uw=
|
||||
github.com/drone/go-scm v1.2.0 h1:ezb8xCvMHX99cSOf3WPI2bmYS6tDVTTap9BiPsPmmXg=
|
||||
github.com/drone/go-scm v1.2.0/go.mod h1:YT4FxQ3U/ltdCrBJR9B0tRpJ1bYA/PM3NyaLE/rYIvw=
|
||||
|
|
|
@ -34,6 +34,7 @@ import (
|
|||
"github.com/drone/drone/handler/api/repos/encrypt"
|
||||
"github.com/drone/drone/handler/api/repos/secrets"
|
||||
"github.com/drone/drone/handler/api/repos/sign"
|
||||
globalsecrets "github.com/drone/drone/handler/api/secrets"
|
||||
"github.com/drone/drone/handler/api/system"
|
||||
"github.com/drone/drone/handler/api/user"
|
||||
"github.com/drone/drone/handler/api/users"
|
||||
|
@ -58,6 +59,7 @@ func New(
|
|||
commits core.CommitService,
|
||||
cron core.CronStore,
|
||||
events core.Pubsub,
|
||||
globals core.GlobalSecretStore,
|
||||
hooks core.HookService,
|
||||
logs core.LogStore,
|
||||
license *core.License,
|
||||
|
@ -83,6 +85,7 @@ func New(
|
|||
Cron: cron,
|
||||
Commits: commits,
|
||||
Events: events,
|
||||
Globals: globals,
|
||||
Hooks: hooks,
|
||||
Logs: logs,
|
||||
License: license,
|
||||
|
@ -111,6 +114,7 @@ type Server struct {
|
|||
Cron core.CronStore
|
||||
Commits core.CommitService
|
||||
Events core.Pubsub
|
||||
Globals core.GlobalSecretStore
|
||||
Hooks core.HookService
|
||||
Logs core.LogStore
|
||||
License *core.License
|
||||
|
@ -298,6 +302,16 @@ func (s Server) Handler() http.Handler {
|
|||
r.Get("/incomplete", globalbuilds.HandleIncomplete(s.Repos))
|
||||
})
|
||||
|
||||
r.Route("/secrets", func(r chi.Router) {
|
||||
r.Use(acl.AuthorizeAdmin)
|
||||
r.Get("/", globalsecrets.HandleAll(s.Globals))
|
||||
r.Get("/{namespace}", globalsecrets.HandleList(s.Globals))
|
||||
r.Post("/{namespace}", globalsecrets.HandleCreate(s.Globals))
|
||||
r.Get("/{namespace}/{name}", globalsecrets.HandleFind(s.Globals))
|
||||
r.Patch("/{namespace}/{name}", globalsecrets.HandleUpdate(s.Globals))
|
||||
r.Delete("/{namespace}/{name}", globalsecrets.HandleDelete(s.Globals))
|
||||
})
|
||||
|
||||
r.Route("/system", func(r chi.Router) {
|
||||
r.Use(acl.AuthorizeAdmin)
|
||||
// r.Get("/license", system.HandleLicense())
|
||||
|
|
33
handler/api/secrets/all.go
Normal file
33
handler/api/secrets/all.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/render"
|
||||
)
|
||||
|
||||
// HandleAll returns an http.HandlerFunc that writes a json-encoded
|
||||
// list of secrets to the response body.
|
||||
func HandleAll(secrets core.GlobalSecretStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := secrets.ListAll(r.Context())
|
||||
if err != nil {
|
||||
render.NotFound(w, err)
|
||||
return
|
||||
}
|
||||
// the secret list is copied and the secret value is
|
||||
// removed from the response.
|
||||
secrets := []*core.Secret{}
|
||||
for _, secret := range list {
|
||||
secrets = append(secrets, secret.Copy())
|
||||
}
|
||||
render.JSON(w, secrets, 200)
|
||||
}
|
||||
}
|
65
handler/api/secrets/all_test.go
Normal file
65
handler/api/secrets/all_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/errors"
|
||||
"github.com/drone/drone/mock"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestHandleAll(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().ListAll(gomock.Any()).Return(dummySecretList, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
|
||||
HandleAll(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusOK; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := []*core.Secret{}, dummySecretListScrubbed
|
||||
json.NewDecoder(w.Body).Decode(&got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAll_SecretListErr(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().ListAll(gomock.Any()).Return(nil, errors.ErrNotFound)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
|
||||
HandleAll(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusNotFound; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(errors.Error), errors.ErrNotFound
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
60
handler/api/secrets/create.go
Normal file
60
handler/api/secrets/create.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/render"
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
type secretInput struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Data string `json:"data"`
|
||||
PullRequest bool `json:"pull_request"`
|
||||
PullRequestPush bool `json:"pull_request_push"`
|
||||
}
|
||||
|
||||
// HandleCreate returns an http.HandlerFunc that processes http
|
||||
// requests to create a new secret.
|
||||
func HandleCreate(secrets core.GlobalSecretStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
in := new(secretInput)
|
||||
err := json.NewDecoder(r.Body).Decode(in)
|
||||
if err != nil {
|
||||
render.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
s := &core.Secret{
|
||||
Namespace: chi.URLParam(r, "namespace"),
|
||||
Name: in.Name,
|
||||
Data: in.Data,
|
||||
PullRequest: in.PullRequest,
|
||||
PullRequestPush: in.PullRequestPush,
|
||||
}
|
||||
|
||||
err = s.Validate()
|
||||
if err != nil {
|
||||
render.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = secrets.Create(r.Context(), s)
|
||||
if err != nil {
|
||||
render.InternalError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
s = s.Copy()
|
||||
render.JSON(w, s, 200)
|
||||
}
|
||||
}
|
139
handler/api/secrets/create_test.go
Normal file
139
handler/api/secrets/create_test.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/errors"
|
||||
"github.com/drone/drone/mock"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestHandleCreate(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
|
||||
in := new(bytes.Buffer)
|
||||
json.NewEncoder(in).Encode(dummySecret)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", in)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleCreate(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusOK; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := &core.Secret{}, dummySecretScrubbed
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCreate_ValidationError(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
|
||||
in := new(bytes.Buffer)
|
||||
json.NewEncoder(in).Encode(&core.Secret{Name: "", Data: "pa55word"})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", in)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleCreate(nil).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusBadRequest; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := &errors.Error{}, &errors.Error{Message: "Invalid Secret Name"}
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCreate_BadRequest(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleCreate(nil).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusBadRequest; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := &errors.Error{}, &errors.Error{Message: "EOF"}
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCreate_CreateError(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().Create(gomock.Any(), gomock.Any()).Return(errors.ErrNotFound)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
|
||||
in := new(bytes.Buffer)
|
||||
json.NewEncoder(in).Encode(dummySecret)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", in)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleCreate(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusInternalServerError; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(errors.Error), errors.ErrNotFound
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
38
handler/api/secrets/delete.go
Normal file
38
handler/api/secrets/delete.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/render"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
// HandleDelete returns an http.HandlerFunc that processes http
|
||||
// requests to delete the secret.
|
||||
func HandleDelete(secrets core.GlobalSecretStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
namespace = chi.URLParam(r, "namespace")
|
||||
name = chi.URLParam(r, "name")
|
||||
)
|
||||
s, err := secrets.FindName(r.Context(), namespace, name)
|
||||
if err != nil {
|
||||
render.NotFound(w, err)
|
||||
return
|
||||
}
|
||||
err = secrets.Delete(r.Context(), s)
|
||||
if err != nil {
|
||||
render.InternalError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
105
handler/api/secrets/delete_test.go
Normal file
105
handler/api/secrets/delete_test.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/handler/api/errors"
|
||||
"github.com/drone/drone/mock"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestHandleDelete(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().FindName(gomock.Any(), dummySecret.Namespace, dummySecret.Name).Return(dummySecret, nil)
|
||||
secrets.EXPECT().Delete(gomock.Any(), dummySecret).Return(nil)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
c.URLParams.Add("name", "github_password")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleDelete(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusNoContent; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDelete_SecretNotFound(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().FindName(gomock.Any(), dummySecret.Namespace, dummySecret.Name).Return(nil, errors.ErrNotFound)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
c.URLParams.Add("name", "github_password")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleDelete(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusNotFound; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(errors.Error), errors.ErrNotFound
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDelete_DeleteError(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().FindName(gomock.Any(), dummySecret.Namespace, dummySecret.Name).Return(dummySecret, nil)
|
||||
secrets.EXPECT().Delete(gomock.Any(), dummySecret).Return(errors.ErrNotFound)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
c.URLParams.Add("name", "github_password")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleDelete(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusInternalServerError; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(errors.Error), errors.ErrNotFound
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
34
handler/api/secrets/find.go
Normal file
34
handler/api/secrets/find.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/render"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
// HandleFind returns an http.HandlerFunc that writes json-encoded
|
||||
// secret details to the the response body.
|
||||
func HandleFind(secrets core.GlobalSecretStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
namespace = chi.URLParam(r, "namespace")
|
||||
name = chi.URLParam(r, "name")
|
||||
)
|
||||
secret, err := secrets.FindName(r.Context(), namespace, name)
|
||||
if err != nil {
|
||||
render.NotFound(w, err)
|
||||
return
|
||||
}
|
||||
safe := secret.Copy()
|
||||
render.JSON(w, safe, 200)
|
||||
}
|
||||
}
|
81
handler/api/secrets/find_test.go
Normal file
81
handler/api/secrets/find_test.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/errors"
|
||||
"github.com/drone/drone/mock"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestHandleFind(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().FindName(gomock.Any(), dummySecret.Namespace, dummySecret.Name).Return(dummySecret, nil)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
c.URLParams.Add("name", "github_password")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleFind(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusOK; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := &core.Secret{}, dummySecretScrubbed
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleFind_SecretNotFound(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().FindName(gomock.Any(), dummySecret.Namespace, dummySecret.Name).Return(nil, errors.ErrNotFound)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
c.URLParams.Add("name", "github_password")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleFind(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusNotFound; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(errors.Error), errors.ErrNotFound
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
36
handler/api/secrets/list.go
Normal file
36
handler/api/secrets/list.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/render"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
// HandleList returns an http.HandlerFunc that writes a json-encoded
|
||||
// list of secrets to the response body.
|
||||
func HandleList(secrets core.GlobalSecretStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
namespace := chi.URLParam(r, "namespace")
|
||||
list, err := secrets.List(r.Context(), namespace)
|
||||
if err != nil {
|
||||
render.NotFound(w, err)
|
||||
return
|
||||
}
|
||||
// the secret list is copied and the secret value is
|
||||
// removed from the response.
|
||||
secrets := []*core.Secret{}
|
||||
for _, secret := range list {
|
||||
secrets = append(secrets, secret.Copy())
|
||||
}
|
||||
render.JSON(w, secrets, 200)
|
||||
}
|
||||
}
|
105
handler/api/secrets/list_test.go
Normal file
105
handler/api/secrets/list_test.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/errors"
|
||||
"github.com/drone/drone/mock"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
var (
|
||||
dummySecret = &core.Secret{
|
||||
Namespace: "octocat",
|
||||
Name: "github_password",
|
||||
Data: "pa55word",
|
||||
}
|
||||
|
||||
dummySecretScrubbed = &core.Secret{
|
||||
Namespace: "octocat",
|
||||
Name: "github_password",
|
||||
Data: "",
|
||||
}
|
||||
|
||||
dummySecretList = []*core.Secret{
|
||||
dummySecret,
|
||||
}
|
||||
|
||||
dummySecretListScrubbed = []*core.Secret{
|
||||
dummySecretScrubbed,
|
||||
}
|
||||
)
|
||||
|
||||
//
|
||||
// HandleList
|
||||
//
|
||||
|
||||
func TestHandleList(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().List(gomock.Any(), dummySecret.Namespace).Return(dummySecretList, nil)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleList(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusOK; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := []*core.Secret{}, dummySecretListScrubbed
|
||||
json.NewDecoder(w.Body).Decode(&got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleList_SecretListErr(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().List(gomock.Any(), dummySecret.Namespace).Return(nil, errors.ErrNotFound)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleList(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusNotFound; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(errors.Error), errors.ErrNotFound
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
52
handler/api/secrets/none.go
Normal file
52
handler/api/secrets/none.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2019 Drone IO, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/render"
|
||||
)
|
||||
|
||||
var notImplemented = func(w http.ResponseWriter, r *http.Request) {
|
||||
render.NotImplemented(w, render.ErrNotImplemented)
|
||||
}
|
||||
|
||||
func HandleCreate(core.GlobalSecretStore) http.HandlerFunc {
|
||||
return notImplemented
|
||||
}
|
||||
|
||||
func HandleUpdate(core.GlobalSecretStore) http.HandlerFunc {
|
||||
return notImplemented
|
||||
}
|
||||
|
||||
func HandleDelete(core.GlobalSecretStore) http.HandlerFunc {
|
||||
return notImplemented
|
||||
}
|
||||
|
||||
func HandleFind(core.GlobalSecretStore) http.HandlerFunc {
|
||||
return notImplemented
|
||||
}
|
||||
|
||||
func HandleList(core.GlobalSecretStore) http.HandlerFunc {
|
||||
return notImplemented
|
||||
}
|
||||
|
||||
func HandleAll(core.GlobalSecretStore) http.HandlerFunc {
|
||||
return notImplemented
|
||||
}
|
72
handler/api/secrets/update.go
Normal file
72
handler/api/secrets/update.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/render"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
type secretUpdate struct {
|
||||
Data *string `json:"data"`
|
||||
PullRequest *bool `json:"pull_request"`
|
||||
PullRequestPush *bool `json:"pull_request_push"`
|
||||
}
|
||||
|
||||
// HandleUpdate returns an http.HandlerFunc that processes http
|
||||
// requests to update a secret.
|
||||
func HandleUpdate(secrets core.GlobalSecretStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
namespace = chi.URLParam(r, "namespace")
|
||||
name = chi.URLParam(r, "name")
|
||||
)
|
||||
|
||||
in := new(secretUpdate)
|
||||
err := json.NewDecoder(r.Body).Decode(in)
|
||||
if err != nil {
|
||||
render.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := secrets.FindName(r.Context(), namespace, name)
|
||||
if err != nil {
|
||||
render.NotFound(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if in.Data != nil {
|
||||
s.Data = *in.Data
|
||||
}
|
||||
if in.PullRequest != nil {
|
||||
s.PullRequest = *in.PullRequest
|
||||
}
|
||||
if in.PullRequestPush != nil {
|
||||
s.PullRequestPush = *in.PullRequestPush
|
||||
}
|
||||
|
||||
err = s.Validate()
|
||||
if err != nil {
|
||||
render.BadRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = secrets.Update(r.Context(), s)
|
||||
if err != nil {
|
||||
render.InternalError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
s = s.Copy()
|
||||
render.JSON(w, s, 200)
|
||||
}
|
||||
}
|
180
handler/api/secrets/update_test.go
Normal file
180
handler/api/secrets/update_test.go
Normal file
|
@ -0,0 +1,180 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/handler/api/errors"
|
||||
"github.com/drone/drone/mock"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestHandleUpdate(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().FindName(gomock.Any(), dummySecret.Namespace, dummySecret.Name).Return(dummySecret, nil)
|
||||
secrets.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
c.URLParams.Add("name", "github_password")
|
||||
|
||||
in := new(bytes.Buffer)
|
||||
json.NewEncoder(in).Encode(dummySecret)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", in)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleUpdate(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusOK; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(core.Secret), dummySecretScrubbed
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdate_ValidationError(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().FindName(gomock.Any(), dummySecret.Namespace, dummySecret.Name).Return(&core.Secret{Name: "github_password"}, nil)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
c.URLParams.Add("name", "github_password")
|
||||
|
||||
in := new(bytes.Buffer)
|
||||
json.NewEncoder(in).Encode(&core.Secret{Data: ""})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", in)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleUpdate(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusBadRequest; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(errors.Error), &errors.Error{Message: "Invalid Secret Value"}
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdate_BadRequest(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
c.URLParams.Add("name", "github_password")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleUpdate(nil).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusBadRequest; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(errors.Error), &errors.Error{Message: "EOF"}
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdate_SecretNotFound(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().FindName(gomock.Any(), dummySecret.Namespace, dummySecret.Name).Return(nil, errors.ErrNotFound)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
c.URLParams.Add("name", "github_password")
|
||||
|
||||
in := new(bytes.Buffer)
|
||||
json.NewEncoder(in).Encode(&core.Secret{})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", in)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleUpdate(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusNotFound; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(errors.Error), errors.ErrNotFound
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdate_UpdateError(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
|
||||
secrets := mock.NewMockGlobalSecretStore(controller)
|
||||
secrets.EXPECT().FindName(gomock.Any(), dummySecret.Namespace, dummySecret.Name).Return(&core.Secret{Name: "github_password"}, nil)
|
||||
secrets.EXPECT().Update(gomock.Any(), gomock.Any()).Return(errors.ErrNotFound)
|
||||
|
||||
c := new(chi.Context)
|
||||
c.URLParams.Add("namespace", "octocat")
|
||||
c.URLParams.Add("name", "github_password")
|
||||
|
||||
in := new(bytes.Buffer)
|
||||
json.NewEncoder(in).Encode(&core.Secret{Data: "password"})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", in)
|
||||
r = r.WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, c),
|
||||
)
|
||||
|
||||
HandleUpdate(secrets).ServeHTTP(w, r)
|
||||
if got, want := w.Code, http.StatusInternalServerError; want != got {
|
||||
t.Errorf("Want response code %d, got %d", want, got)
|
||||
}
|
||||
|
||||
got, want := new(errors.Error), errors.ErrNotFound
|
||||
json.NewDecoder(w.Body).Decode(got)
|
||||
if diff := cmp.Diff(got, want); len(diff) != 0 {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
|
@ -6,4 +6,4 @@
|
|||
|
||||
package mock
|
||||
|
||||
//go:generate mockgen -package=mock -destination=mock_gen.go github.com/drone/drone/core NetrcService,Renewer,HookParser,UserService,RepositoryService,CommitService,StatusService,HookService,FileService,Batcher,BuildStore,CronStore,LogStore,PermStore,SecretStore,StageStore,StepStore,RepositoryStore,UserStore,Scheduler,Session,OrganizationService,SecretService,RegistryService,ConfigService,Triggerer,Syncer,LogStream,WebhookSender,LicenseService
|
||||
//go:generate mockgen -package=mock -destination=mock_gen.go github.com/drone/drone/core NetrcService,Renewer,HookParser,UserService,RepositoryService,CommitService,StatusService,HookService,FileService,Batcher,BuildStore,CronStore,LogStore,PermStore,SecretStore,GlobalSecretStore,StageStore,StepStore,RepositoryStore,UserStore,Scheduler,Session,OrganizationService,SecretService,RegistryService,ConfigService,Triggerer,Syncer,LogStream,WebhookSender,LicenseService
|
||||
|
|
113
mock/mock_gen.go
113
mock/mock_gen.go
|
@ -1,5 +1,5 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/drone/drone/core (interfaces: NetrcService,Renewer,HookParser,UserService,RepositoryService,CommitService,StatusService,HookService,FileService,Batcher,BuildStore,CronStore,LogStore,PermStore,SecretStore,StageStore,StepStore,RepositoryStore,UserStore,Scheduler,Session,OrganizationService,SecretService,RegistryService,ConfigService,Triggerer,Syncer,LogStream,WebhookSender,LicenseService)
|
||||
// Source: github.com/drone/drone/core (interfaces: NetrcService,Renewer,HookParser,UserService,RepositoryService,CommitService,StatusService,HookService,FileService,Batcher,BuildStore,CronStore,LogStore,PermStore,SecretStore,GlobalSecretStore,StageStore,StepStore,RepositoryStore,UserStore,Scheduler,Session,OrganizationService,SecretService,RegistryService,ConfigService,Triggerer,Syncer,LogStream,WebhookSender,LicenseService)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
@ -963,6 +963,117 @@ func (mr *MockSecretStoreMockRecorder) Update(arg0, arg1 interface{}) *gomock.Ca
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSecretStore)(nil).Update), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockGlobalSecretStore is a mock of GlobalSecretStore interface
|
||||
type MockGlobalSecretStore struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockGlobalSecretStoreMockRecorder
|
||||
}
|
||||
|
||||
// MockGlobalSecretStoreMockRecorder is the mock recorder for MockGlobalSecretStore
|
||||
type MockGlobalSecretStoreMockRecorder struct {
|
||||
mock *MockGlobalSecretStore
|
||||
}
|
||||
|
||||
// NewMockGlobalSecretStore creates a new mock instance
|
||||
func NewMockGlobalSecretStore(ctrl *gomock.Controller) *MockGlobalSecretStore {
|
||||
mock := &MockGlobalSecretStore{ctrl: ctrl}
|
||||
mock.recorder = &MockGlobalSecretStoreMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockGlobalSecretStore) EXPECT() *MockGlobalSecretStoreMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Create mocks base method
|
||||
func (m *MockGlobalSecretStore) Create(arg0 context.Context, arg1 *core.Secret) error {
|
||||
ret := m.ctrl.Call(m, "Create", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Create indicates an expected call of Create
|
||||
func (mr *MockGlobalSecretStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockGlobalSecretStore)(nil).Create), arg0, arg1)
|
||||
}
|
||||
|
||||
// Delete mocks base method
|
||||
func (m *MockGlobalSecretStore) Delete(arg0 context.Context, arg1 *core.Secret) error {
|
||||
ret := m.ctrl.Call(m, "Delete", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete
|
||||
func (mr *MockGlobalSecretStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockGlobalSecretStore)(nil).Delete), arg0, arg1)
|
||||
}
|
||||
|
||||
// Find mocks base method
|
||||
func (m *MockGlobalSecretStore) Find(arg0 context.Context, arg1 int64) (*core.Secret, error) {
|
||||
ret := m.ctrl.Call(m, "Find", arg0, arg1)
|
||||
ret0, _ := ret[0].(*core.Secret)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Find indicates an expected call of Find
|
||||
func (mr *MockGlobalSecretStoreMockRecorder) Find(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockGlobalSecretStore)(nil).Find), arg0, arg1)
|
||||
}
|
||||
|
||||
// FindName mocks base method
|
||||
func (m *MockGlobalSecretStore) FindName(arg0 context.Context, arg1, arg2 string) (*core.Secret, error) {
|
||||
ret := m.ctrl.Call(m, "FindName", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*core.Secret)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindName indicates an expected call of FindName
|
||||
func (mr *MockGlobalSecretStoreMockRecorder) FindName(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindName", reflect.TypeOf((*MockGlobalSecretStore)(nil).FindName), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// List mocks base method
|
||||
func (m *MockGlobalSecretStore) List(arg0 context.Context, arg1 string) ([]*core.Secret, error) {
|
||||
ret := m.ctrl.Call(m, "List", arg0, arg1)
|
||||
ret0, _ := ret[0].([]*core.Secret)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// List indicates an expected call of List
|
||||
func (mr *MockGlobalSecretStoreMockRecorder) List(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockGlobalSecretStore)(nil).List), arg0, arg1)
|
||||
}
|
||||
|
||||
// ListAll mocks base method
|
||||
func (m *MockGlobalSecretStore) ListAll(arg0 context.Context) ([]*core.Secret, error) {
|
||||
ret := m.ctrl.Call(m, "ListAll", arg0)
|
||||
ret0, _ := ret[0].([]*core.Secret)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListAll indicates an expected call of ListAll
|
||||
func (mr *MockGlobalSecretStoreMockRecorder) ListAll(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAll", reflect.TypeOf((*MockGlobalSecretStore)(nil).ListAll), arg0)
|
||||
}
|
||||
|
||||
// Update mocks base method
|
||||
func (m *MockGlobalSecretStore) Update(arg0 context.Context, arg1 *core.Secret) error {
|
||||
ret := m.ctrl.Call(m, "Update", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update
|
||||
func (mr *MockGlobalSecretStoreMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockGlobalSecretStore)(nil).Update), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockStageStore is a mock of StageStore interface
|
||||
type MockStageStore struct {
|
||||
ctrl *gomock.Controller
|
||||
|
|
|
@ -108,6 +108,7 @@ func New(
|
|||
repos core.RepositoryStore,
|
||||
scheduler core.Scheduler,
|
||||
secrets core.SecretStore,
|
||||
globals core.GlobalSecretStore,
|
||||
status core.StatusService,
|
||||
stages core.StageStore,
|
||||
steps core.StepStore,
|
||||
|
@ -119,6 +120,7 @@ func New(
|
|||
Builds: builds,
|
||||
Config: config,
|
||||
Events: events,
|
||||
Globals: globals,
|
||||
Logs: logs,
|
||||
Logz: logz,
|
||||
Netrcs: netrcs,
|
||||
|
@ -140,6 +142,7 @@ type Manager struct {
|
|||
Builds core.BuildStore
|
||||
Config core.ConfigService
|
||||
Events core.Pubsub
|
||||
Globals core.GlobalSecretStore
|
||||
Logs core.LogStore
|
||||
Logz core.LogStream
|
||||
Netrcs core.NetrcService
|
||||
|
@ -287,6 +290,12 @@ func (m *Manager) Details(ctx context.Context, id int64) (*Context, error) {
|
|||
logger.Warnln("manager: cannot list secrets")
|
||||
return nil, err
|
||||
}
|
||||
tmpGlobalSecrets, err := m.Globals.List(noContext, repo.Namespace)
|
||||
if err != nil {
|
||||
logger = logger.WithError(err)
|
||||
logger.Warnln("manager: cannot list global secrets")
|
||||
return nil, err
|
||||
}
|
||||
// TODO(bradrydzewski) can we delegate filtering
|
||||
// secrets to the agent? If not, we should add
|
||||
// unit tests.
|
||||
|
@ -297,6 +306,13 @@ func (m *Manager) Details(ctx context.Context, id int64) (*Context, error) {
|
|||
}
|
||||
secrets = append(secrets, secret)
|
||||
}
|
||||
for _, secret := range tmpGlobalSecrets {
|
||||
if secret.PullRequest == false &&
|
||||
build.Event == core.EventPullRequest {
|
||||
continue
|
||||
}
|
||||
secrets = append(secrets, secret)
|
||||
}
|
||||
return &Context{
|
||||
Repo: repo,
|
||||
Build: build,
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
package license
|
||||
|
||||
import (
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/core"
|
||||
)
|
||||
|
||||
// DefaultLicense is an empty license with no restrictions.
|
||||
var DefaultLicense = &core.License{Kind: core.LicenseFoss}
|
||||
|
||||
func Trial(string) *core.License { return nil }
|
||||
func Trial(string) *core.License { return DefaultLicense }
|
||||
func Load(string) (*core.License, error) { return DefaultLicense, nil }
|
||||
|
|
74
store/secret/global/scan.go
Normal file
74
store/secret/global/scan.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package global
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/store/shared/db"
|
||||
"github.com/drone/drone/store/shared/encrypt"
|
||||
)
|
||||
|
||||
// helper function converts the User structure to a set
|
||||
// of named query parameters.
|
||||
func toParams(encrypt encrypt.Encrypter, secret *core.Secret) (map[string]interface{}, error) {
|
||||
ciphertext, err := encrypt.Encrypt(secret.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"secret_id": secret.ID,
|
||||
"secret_namespace": secret.Namespace,
|
||||
"secret_name": secret.Name,
|
||||
"secret_type": secret.Type,
|
||||
"secret_data": ciphertext,
|
||||
"secret_pull_request": secret.PullRequest,
|
||||
"secret_pull_request_push": secret.PullRequestPush,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// helper function scans the sql.Row and copies the column
|
||||
// values to the destination object.
|
||||
func scanRow(encrypt encrypt.Encrypter, scanner db.Scanner, dst *core.Secret) error {
|
||||
var ciphertext []byte
|
||||
err := scanner.Scan(
|
||||
&dst.ID,
|
||||
&dst.Namespace,
|
||||
&dst.Name,
|
||||
&dst.Type,
|
||||
&ciphertext,
|
||||
&dst.PullRequest,
|
||||
&dst.PullRequestPush,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plaintext, err := encrypt.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst.Data = plaintext
|
||||
return nil
|
||||
}
|
||||
|
||||
// helper function scans the sql.Row and copies the column
|
||||
// values to the destination object.
|
||||
func scanRows(encrypt encrypt.Encrypter, rows *sql.Rows) ([]*core.Secret, error) {
|
||||
defer rows.Close()
|
||||
|
||||
secrets := []*core.Secret{}
|
||||
for rows.Next() {
|
||||
sec := new(core.Secret)
|
||||
err := scanRow(encrypt, rows, sec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secrets = append(secrets, sec)
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
233
store/secret/global/secret.go
Normal file
233
store/secret/global/secret.go
Normal file
|
@ -0,0 +1,233 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/store/shared/db"
|
||||
"github.com/drone/drone/store/shared/encrypt"
|
||||
)
|
||||
|
||||
// New returns a new global Secret database store.
|
||||
func New(db *db.DB, enc encrypt.Encrypter) core.GlobalSecretStore {
|
||||
return &secretStore{
|
||||
db: db,
|
||||
enc: enc,
|
||||
}
|
||||
}
|
||||
|
||||
type secretStore struct {
|
||||
db *db.DB
|
||||
enc encrypt.Encrypter
|
||||
}
|
||||
|
||||
func (s *secretStore) List(ctx context.Context, namespace string) ([]*core.Secret, error) {
|
||||
var out []*core.Secret
|
||||
err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
|
||||
params := map[string]interface{}{"secret_namespace": namespace}
|
||||
stmt, args, err := binder.BindNamed(queryNamespace, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := queryer.Query(stmt, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err = scanRows(s.enc, rows)
|
||||
return err
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *secretStore) ListAll(ctx context.Context) ([]*core.Secret, error) {
|
||||
var out []*core.Secret
|
||||
err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
|
||||
rows, err := queryer.Query(queryAll)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err = scanRows(s.enc, rows)
|
||||
return err
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *secretStore) Find(ctx context.Context, id int64) (*core.Secret, error) {
|
||||
out := &core.Secret{ID: id}
|
||||
err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
|
||||
params, err := toParams(s.enc, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query, args, err := binder.BindNamed(queryKey, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row := queryer.QueryRow(query, args...)
|
||||
return scanRow(s.enc, row, out)
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *secretStore) FindName(ctx context.Context, namespace, name string) (*core.Secret, error) {
|
||||
out := &core.Secret{Name: name, Namespace: namespace}
|
||||
err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
|
||||
params, err := toParams(s.enc, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query, args, err := binder.BindNamed(queryName, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row := queryer.QueryRow(query, args...)
|
||||
return scanRow(s.enc, row, out)
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *secretStore) Create(ctx context.Context, secret *core.Secret) error {
|
||||
if s.db.Driver() == db.Postgres {
|
||||
return s.createPostgres(ctx, secret)
|
||||
}
|
||||
return s.create(ctx, secret)
|
||||
}
|
||||
|
||||
func (s *secretStore) create(ctx context.Context, secret *core.Secret) error {
|
||||
return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
|
||||
params, err := toParams(s.enc, secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, args, err := binder.BindNamed(stmtInsert, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := execer.Exec(stmt, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret.ID, err = res.LastInsertId()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *secretStore) createPostgres(ctx context.Context, secret *core.Secret) error {
|
||||
return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
|
||||
params, err := toParams(s.enc, secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, args, err := binder.BindNamed(stmtInsertPg, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return execer.QueryRow(stmt, args...).Scan(&secret.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *secretStore) Update(ctx context.Context, secret *core.Secret) error {
|
||||
return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
|
||||
params, err := toParams(s.enc, secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, args, err := binder.BindNamed(stmtUpdate, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = execer.Exec(stmt, args...)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *secretStore) Delete(ctx context.Context, secret *core.Secret) error {
|
||||
return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
|
||||
params, err := toParams(s.enc, secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, args, err := binder.BindNamed(stmtDelete, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = execer.Exec(stmt, args...)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
const queryBase = `
|
||||
SELECT
|
||||
secret_id
|
||||
,secret_namespace
|
||||
,secret_name
|
||||
,secret_type
|
||||
,secret_data
|
||||
,secret_pull_request
|
||||
,secret_pull_request_push
|
||||
`
|
||||
|
||||
const queryKey = queryBase + `
|
||||
FROM orgsecrets
|
||||
WHERE secret_id = :secret_id
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
const queryAll = queryBase + `
|
||||
FROM orgsecrets
|
||||
ORDER BY secret_name
|
||||
`
|
||||
|
||||
const queryName = queryBase + `
|
||||
FROM orgsecrets
|
||||
WHERE secret_name = :secret_name
|
||||
AND secret_namespace = :secret_namespace
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
const queryNamespace = queryBase + `
|
||||
FROM orgsecrets
|
||||
WHERE secret_namespace = :secret_namespace
|
||||
ORDER BY secret_name
|
||||
`
|
||||
|
||||
const stmtUpdate = `
|
||||
UPDATE orgsecrets SET
|
||||
secret_data = :secret_data
|
||||
,secret_pull_request = :secret_pull_request
|
||||
,secret_pull_request_push = :secret_pull_request_push
|
||||
WHERE secret_id = :secret_id
|
||||
`
|
||||
|
||||
const stmtDelete = `
|
||||
DELETE FROM orgsecrets
|
||||
WHERE secret_id = :secret_id
|
||||
`
|
||||
|
||||
const stmtInsert = `
|
||||
INSERT INTO orgsecrets (
|
||||
secret_namespace
|
||||
,secret_name
|
||||
,secret_type
|
||||
,secret_data
|
||||
,secret_pull_request
|
||||
,secret_pull_request_push
|
||||
) VALUES (
|
||||
:secret_namespace
|
||||
,:secret_name
|
||||
,:secret_type
|
||||
,:secret_data
|
||||
,:secret_pull_request
|
||||
,:secret_pull_request_push
|
||||
)
|
||||
`
|
||||
|
||||
const stmtInsertPg = stmtInsert + `
|
||||
RETURNING secret_id
|
||||
`
|
60
store/secret/global/secret_oss.go
Normal file
60
store/secret/global/secret_oss.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright 2019 Drone IO, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build oss
|
||||
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/store/shared/db"
|
||||
"github.com/drone/drone/store/shared/encrypt"
|
||||
)
|
||||
|
||||
// New returns a new Secret database store.
|
||||
func New(db *db.DB, enc encrypt.Encrypter) core.GlobalSecretStore {
|
||||
return new(noop)
|
||||
}
|
||||
|
||||
type noop struct{}
|
||||
|
||||
func (noop) List(context.Context, string) ([]*core.Secret, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (noop) ListAll(context.Context) ([]*core.Secret, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (noop) Find(context.Context, int64) (*core.Secret, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (noop) FindName(context.Context, string, string) (*core.Secret, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (noop) Create(context.Context, *core.Secret) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (noop) Update(context.Context, *core.Secret) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (noop) Delete(context.Context, *core.Secret) error {
|
||||
return nil
|
||||
}
|
165
store/secret/global/secret_test.go
Normal file
165
store/secret/global/secret_test.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
// Copyright 2019 Drone.IO Inc. All rights reserved.
|
||||
// Use of this source code is governed by the Drone Non-Commercial License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// +build !oss
|
||||
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone/core"
|
||||
"github.com/drone/drone/store/shared/db/dbtest"
|
||||
"github.com/drone/drone/store/shared/encrypt"
|
||||
)
|
||||
|
||||
var noContext = context.TODO()
|
||||
|
||||
func TestSecret(t *testing.T) {
|
||||
conn, err := dbtest.Connect()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
dbtest.Reset(conn)
|
||||
dbtest.Disconnect(conn)
|
||||
}()
|
||||
|
||||
store := New(conn, nil).(*secretStore)
|
||||
store.enc, _ = encrypt.New("fb4b4d6267c8a5ce8231f8b186dbca92")
|
||||
t.Run("Create", testSecretCreate(store))
|
||||
}
|
||||
|
||||
func testSecretCreate(store *secretStore) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
item := &core.Secret{
|
||||
Namespace: "octocat",
|
||||
Name: "password",
|
||||
Data: "correct-horse-battery-staple",
|
||||
}
|
||||
err := store.Create(noContext, item)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if item.ID == 0 {
|
||||
t.Errorf("Want secret ID assigned, got %d", item.ID)
|
||||
}
|
||||
|
||||
t.Run("Find", testSecretFind(store, item))
|
||||
t.Run("FindName", testSecretFindName(store))
|
||||
t.Run("List", testSecretList(store))
|
||||
t.Run("ListAll", testSecretListAll(store))
|
||||
t.Run("Update", testSecretUpdate(store))
|
||||
t.Run("Delete", testSecretDelete(store))
|
||||
}
|
||||
}
|
||||
|
||||
func testSecretFind(store *secretStore, secret *core.Secret) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
item, err := store.Find(noContext, secret.ID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
t.Run("Fields", testSecret(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSecretFindName(store *secretStore) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
item, err := store.FindName(noContext, "octocat", "password")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
t.Run("Fields", testSecret(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSecretList(store *secretStore) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
list, err := store.List(noContext, "octocat")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if got, want := len(list), 1; got != want {
|
||||
t.Errorf("Want count %d, got %d", want, got)
|
||||
} else {
|
||||
t.Run("Fields", testSecret(list[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSecretListAll(store *secretStore) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
list, err := store.ListAll(noContext)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if got, want := len(list), 1; got != want {
|
||||
t.Errorf("Want count %d, got %d", want, got)
|
||||
} else {
|
||||
t.Run("Fields", testSecret(list[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSecretUpdate(store *secretStore) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
before, err := store.FindName(noContext, "octocat", "password")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
err = store.Update(noContext, before)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
after, err := store.Find(noContext, before.ID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if after == nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSecretDelete(store *secretStore) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
secret, err := store.FindName(noContext, "octocat", "password")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
err = store.Delete(noContext, secret)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
_, err = store.Find(noContext, secret.ID)
|
||||
if got, want := sql.ErrNoRows, err; got != want {
|
||||
t.Errorf("Want sql.ErrNoRows, got %v", got)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSecret(item *core.Secret) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
if got, want := item.Name, "password"; got != want {
|
||||
t.Errorf("Want secret name %q, got %q", want, got)
|
||||
}
|
||||
if got, want := item.Data, "correct-horse-battery-staple"; got != want {
|
||||
t.Errorf("Want secret data %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -120,6 +120,10 @@ var migrations = []struct {
|
|||
name: "alter-table-builds-add-column-cron",
|
||||
stmt: alterTableBuildsAddColumnCron,
|
||||
},
|
||||
{
|
||||
name: "create-table-org-secrets",
|
||||
stmt: createTableOrgSecrets,
|
||||
},
|
||||
}
|
||||
|
||||
// Migrate performs the database migration. If the migration fails
|
||||
|
@ -556,3 +560,20 @@ CREATE TABLE IF NOT EXISTS nodes (
|
|||
var alterTableBuildsAddColumnCron = `
|
||||
ALTER TABLE builds ADD COLUMN build_cron VARCHAR(50) NOT NULL DEFAULT '';
|
||||
`
|
||||
|
||||
//
|
||||
// 012_create_table_global_secrets.sql
|
||||
//
|
||||
|
||||
var createTableOrgSecrets = `
|
||||
CREATE TABLE IF NOT EXISTS orgsecrets (
|
||||
secret_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
,secret_namespace VARCHAR(50)
|
||||
,secret_name VARCHAR(200)
|
||||
,secret_type VARCHAR(50)
|
||||
,secret_data BLOB
|
||||
,secret_pull_request BOOLEAN
|
||||
,secret_pull_request_push BOOLEAN
|
||||
,UNIQUE(secret_namespace, secret_name)
|
||||
);
|
||||
`
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
-- name: create-table-org-secrets
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orgsecrets (
|
||||
secret_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
,secret_namespace VARCHAR(50)
|
||||
,secret_name VARCHAR(200)
|
||||
,secret_type VARCHAR(50)
|
||||
,secret_data BLOB
|
||||
,secret_pull_request BOOLEAN
|
||||
,secret_pull_request_push BOOLEAN
|
||||
,UNIQUE(secret_namespace, secret_name)
|
||||
);
|
|
@ -116,6 +116,10 @@ var migrations = []struct {
|
|||
name: "alter-table-builds-add-column-cron",
|
||||
stmt: alterTableBuildsAddColumnCron,
|
||||
},
|
||||
{
|
||||
name: "create-table-org-secrets",
|
||||
stmt: createTableOrgSecrets,
|
||||
},
|
||||
}
|
||||
|
||||
// Migrate performs the database migration. If the migration fails
|
||||
|
@ -534,3 +538,20 @@ CREATE TABLE IF NOT EXISTS nodes (
|
|||
var alterTableBuildsAddColumnCron = `
|
||||
ALTER TABLE builds ADD COLUMN build_cron VARCHAR(50) NOT NULL DEFAULT '';
|
||||
`
|
||||
|
||||
//
|
||||
// 012_create_table_org_secrets.sql
|
||||
//
|
||||
|
||||
var createTableOrgSecrets = `
|
||||
CREATE TABLE IF NOT EXISTS orgsecrets (
|
||||
secret_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
,secret_namespace VARCHAR(50)
|
||||
,secret_name VARCHAR(200)
|
||||
,secret_type VARCHAR(50)
|
||||
,secret_data BYTEA
|
||||
,secret_pull_request BOOLEAN
|
||||
,secret_pull_request_push BOOLEAN
|
||||
,UNIQUE(secret_namespace, secret_name)
|
||||
);
|
||||
`
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
-- name: create-table-org-secrets
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orgsecrets (
|
||||
secret_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
,secret_namespace VARCHAR(50)
|
||||
,secret_name VARCHAR(200)
|
||||
,secret_type VARCHAR(50)
|
||||
,secret_data BYTEA
|
||||
,secret_pull_request BOOLEAN
|
||||
,secret_pull_request_push BOOLEAN
|
||||
,UNIQUE(secret_namespace, secret_name)
|
||||
);
|
|
@ -116,6 +116,10 @@ var migrations = []struct {
|
|||
name: "alter-table-builds-add-column-cron",
|
||||
stmt: alterTableBuildsAddColumnCron,
|
||||
},
|
||||
{
|
||||
name: "create-table-org-secrets",
|
||||
stmt: createTableOrgSecrets,
|
||||
},
|
||||
}
|
||||
|
||||
// Migrate performs the database migration. If the migration fails
|
||||
|
@ -536,3 +540,20 @@ CREATE TABLE IF NOT EXISTS nodes (
|
|||
var alterTableBuildsAddColumnCron = `
|
||||
ALTER TABLE builds ADD COLUMN build_cron TEXT NOT NULL DEFAULT '';
|
||||
`
|
||||
|
||||
//
|
||||
// 012_create_table_org_secrets.sql
|
||||
//
|
||||
|
||||
var createTableOrgSecrets = `
|
||||
CREATE TABLE IF NOT EXISTS orgsecrets (
|
||||
secret_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
,secret_namespace TEXT COLLATE NOCASE
|
||||
,secret_name TEXT COLLATE NOCASE
|
||||
,secret_type TEXT
|
||||
,secret_data BLOB
|
||||
,secret_pull_request BOOLEAN
|
||||
,secret_pull_request_push BOOLEAN
|
||||
,UNIQUE(secret_namespace, secret_name)
|
||||
);
|
||||
`
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
-- name: create-table-org-secrets
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orgsecrets (
|
||||
secret_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
,secret_namespace TEXT COLLATE NOCASE
|
||||
,secret_name TEXT COLLATE NOCASE
|
||||
,secret_type TEXT
|
||||
,secret_data BLOB
|
||||
,secret_pull_request BOOLEAN
|
||||
,secret_pull_request_push BOOLEAN
|
||||
,UNIQUE(secret_namespace, secret_name)
|
||||
);
|
Loading…
Reference in a new issue