support for per-organization secrets

This commit is contained in:
Brad Rydzewski 2019-04-14 17:06:16 -07:00
parent 727177da13
commit 96132e3d0a
34 changed files with 1819 additions and 6 deletions

View file

@ -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.

View file

@ -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,
)

View file

@ -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)

View file

@ -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,
}

View file

@ -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
View file

@ -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=

View file

@ -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())

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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
}

View 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)
}
}

View 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)
}
}

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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 }

View 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
}

View 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
`

View 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
}

View 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)
}
}
}

View file

@ -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)
);
`

View file

@ -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)
);

View file

@ -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)
);
`

View file

@ -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)
);

View file

@ -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)
);
`

View file

@ -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)
);