ability to store per-repository secrets & sign yaml

This commit is contained in:
Brad Rydzewski 2016-03-31 12:01:32 -07:00
parent 8251663686
commit 7ffa88cc09
11 changed files with 408 additions and 0 deletions

48
api/secret.go Normal file
View file

@ -0,0 +1,48 @@
package api
import (
"github.com/drone/drone/model"
"github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/store"
"github.com/gin-gonic/gin"
)
func PostSecret(c *gin.Context) {
repo := session.Repo(c)
in := &model.Secret{}
err := c.Bind(in)
if err != nil {
c.String(400, "Invalid JSON input. %s", err.Error())
return
}
in.ID = 0
in.RepoID = repo.ID
err = store.SetSecret(c, in)
if err != nil {
c.String(500, "Unable to persist secret. %s", err.Error())
return
}
c.String(200, "")
}
func DeleteSecret(c *gin.Context) {
repo := session.Repo(c)
name := c.Param("secret")
secret, err := store.GetSecret(c, repo, name)
if err != nil {
c.String(404, "Cannot find secret %s.", name)
return
}
err = store.DeleteSecret(c, secret)
if err != nil {
c.String(500, "Unable to delete secret. %s", err.Error())
return
}
c.String(200, "")
}

40
api/sign.go Normal file
View file

@ -0,0 +1,40 @@
package api
import (
"io/ioutil"
"github.com/drone/drone/router/middleware/session"
"github.com/gin-gonic/gin"
"github.com/square/go-jose"
)
func Sign(c *gin.Context) {
repo := session.Repo(c)
in, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.String(400, "Unable to read request body. %s.", err.Error())
return
}
signer, err := jose.NewSigner(jose.HS256, []byte(repo.Hash))
if err != nil {
c.String(500, "Unable to create the signer. %s.", err.Error())
return
}
signed, err := signer.Sign(in)
if err != nil {
c.String(500, "Unable to sign input. %s", err.Error())
return
}
out, err := signed.CompactSerialize()
if err != nil {
c.String(500, "Unable to serialize signature. %s", err.Error())
return
}
c.String(200, out)
}

15
model/registry.go Normal file
View file

@ -0,0 +1,15 @@
package model
type Registry struct {
ID int64 `json:"id" meddler:"registry_id,pk"`
RepoID int64 `json:"-" meddler:"registry_repo_id"`
Addr string `json:"addr" meddler:"registry_addr"`
Username string `json:"username" meddler:"registry_username"`
Password string `json:"password" meddler:"registry_password"`
Email string `json:"email" meddler:"registry_email"`
Token string `json:"token" meddler:"registry_token"`
}
func (r *Registry) Validate() error {
return nil
}

27
model/secret.go Normal file
View file

@ -0,0 +1,27 @@
package model
type Secret struct {
// the id for this secret.
ID int64 `json:"id" meddler:"secret_id,pk"`
// the foreign key for this secret.
RepoID int64 `json:"-" meddler:"secret_repo_id"`
// the name of the secret which will be used as the
// environment variable name at runtime.
Name string `json:"name" meddler:"secret_name"`
// the value of the secret which will be provided to
// the runtime environment as a named environment variable.
Value string `json:"value" meddler:"secret_value"`
// the secret is restricted to this list of images.
Images []string `json:"image,omitempty" meddler:"secret_images,json"`
// the secret is restricted to this list of events.
Events []string `json:"event,omitempty" meddler:"secret_events,json"`
}
func (s *Secret) Validate() error {
return nil
}

View file

@ -50,6 +50,7 @@ func Load(middleware ...gin.HandlerFunc) http.Handler {
repo.GET("", web.ShowRepo) repo.GET("", web.ShowRepo)
repo.GET("/builds/:number", web.ShowBuild) repo.GET("/builds/:number", web.ShowBuild)
repo.GET("/builds/:number/:job", web.ShowBuild) repo.GET("/builds/:number/:job", web.ShowBuild)
repo_settings := repo.Group("/settings") repo_settings := repo.Group("/settings")
{ {
repo_settings.GET("", session.MustPush, web.ShowRepoConf) repo_settings.GET("", session.MustPush, web.ShowRepoConf)
@ -102,6 +103,10 @@ func Load(middleware ...gin.HandlerFunc) http.Handler {
repo.GET("/builds", api.GetBuilds) repo.GET("/builds", api.GetBuilds)
repo.GET("/builds/:number", api.GetBuild) repo.GET("/builds/:number", api.GetBuild)
repo.GET("/logs/:number/:job", api.GetBuildLogs) repo.GET("/logs/:number/:job", api.GetBuildLogs)
repo.POST("/sign", session.MustPush, api.Sign)
repo.POST("/secrets", session.MustPush, api.PostSecret)
repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret)
// requires authenticated user // requires authenticated user
repo.POST("/encrypt", session.MustUser(), api.PostSecure) repo.POST("/encrypt", session.MustUser(), api.PostSecure)

View file

@ -0,0 +1,32 @@
-- +migrate Up
CREATE TABLE secrets (
secret_id INTEGER PRIMARY KEY AUTO_INCREMENT
,secret_repo_id INTEGER
,secret_name VARCHAR(500)
,secret_value MEDIUMBLOB
,secret_images VARCHAR(2000)
,secret_events VARCHAR(2000)
,UNIQUE(secret_name, secret_repo_id)
);
CREATE TABLE registry (
registry_id INTEGER PRIMARY KEY AUTO_INCREMENT
,registry_repo_id INTEGER
,registry_addr VARCHAR(500)
,registry_email VARCHAR(500)
,registry_username VARCHAR(2000)
,registry_password VARCHAR(2000)
,registry_token VARCHAR(2000)
,UNIQUE(registry_addr, registry_repo_id)
);
CREATE INDEX ix_secrets_repo ON secrets (secret_repo_id);
CREATE INDEX ix_registry_repo ON registry (registry_repo_id);
-- +migrate Down
DROP INDEX ix_secrets_repo;
DROP INDEX ix_registry_repo;

View file

@ -0,0 +1,32 @@
-- +migrate Up
CREATE TABLE secrets (
secret_id SERIAL PRIMARY KEY
,secret_repo_id INTEGER
,secret_name VARCHAR(500)
,secret_value BYTEA
,secret_images VARCHAR(2000)
,secret_events VARCHAR(2000)
,UNIQUE(secret_name, secret_repo_id)
);
CREATE TABLE registry (
registry_id SERIAL PRIMARY KEY
,registry_repo_id INTEGER
,registry_addr VARCHAR(500)
,registry_email VARCHAR(500)
,registry_username VARCHAR(2000)
,registry_password VARCHAR(2000)
,registry_token VARCHAR(2000)
,UNIQUE(registry_addr, registry_repo_id)
);
CREATE INDEX ix_secrets_repo ON secrets (secret_repo_id);
CREATE INDEX ix_registry_repo ON registry (registry_repo_id);
-- +migrate Down
DROP INDEX ix_secrets_repo;
DROP INDEX ix_registry_repo;

View file

@ -0,0 +1,34 @@
-- +migrate Up
CREATE TABLE secrets (
secret_id INTEGER PRIMARY KEY AUTOINCREMENT
,secret_repo_id INTEGER
,secret_name TEXT
,secret_value TEXT
,secret_images TEXT
,secret_events TEXT
,UNIQUE(secret_name, secret_repo_id)
);
CREATE TABLE registry (
registry_id INTEGER PRIMARY KEY AUTOINCREMENT
,registry_repo_id INTEGER
,registry_addr TEXT
,registry_username TEXT
,registry_password TEXT
,registry_email TEXT
,registry_token TEXT
,UNIQUE(registry_addr, registry_repo_id)
);
CREATE INDEX ix_secrets_repo ON secrets (secret_repo_id);
CREATE INDEX ix_registry_repo ON registry (registry_repo_id);
-- +migrate Down
DROP INDEX ix_secrets_repo;
DROP INDEX ix_registry_repo;
DROP TABLE secrets;
DROP TABLE registry;

53
store/datastore/secret.go Normal file
View file

@ -0,0 +1,53 @@
package datastore
import (
"github.com/drone/drone/model"
"github.com/russross/meddler"
)
func (db *datastore) GetSecretList(repo *model.Repo) ([]*model.Secret, error) {
var secrets = []*model.Secret{}
var err = meddler.QueryAll(db, &secrets, rebind(secretListQuery), repo.ID)
return secrets, err
}
func (db *datastore) GetSecret(repo *model.Repo, name string) (*model.Secret, error) {
var secret = new(model.Secret)
var err = meddler.QueryRow(db, secret, rebind(secretNameQuery), repo.ID, name)
return secret, err
}
func (db *datastore) SetSecret(sec *model.Secret) error {
var got = new(model.Secret)
var err = meddler.QueryRow(db, got, rebind(secretNameQuery), sec.RepoID, sec.Name)
if err == nil && got.ID != 0 {
sec.ID = got.ID // update existing id
}
return meddler.Save(db, secretTable, sec)
}
func (db *datastore) DeleteSecret(sec *model.Secret) error {
_, err := db.Exec(secretDeleteStmt, sec.ID)
return err
}
const secretTable = "secrets"
const secretListQuery = `
SELECT *
FROM secrets
WHERE secret_repo_id = ?
`
const secretNameQuery = `
SELECT *
FROM secrets
WHERE secret_repo_id = ?
AND secret_name = ?
LIMIT 1;
`
const secretDeleteStmt = `
DELETE FROM secrets
WHERE secret_id = ?
`

View file

@ -0,0 +1,94 @@
package datastore
import (
"testing"
"github.com/drone/drone/model"
"github.com/franela/goblin"
)
func TestSecrets(t *testing.T) {
db := openTest()
defer db.Close()
s := From(db)
g := goblin.Goblin(t)
g.Describe("Secrets", func() {
// before each test be sure to purge the package
// table data from the database.
g.BeforeEach(func() {
db.Exec(rebind("DELETE FROM secrets"))
})
g.It("Should set and get a secret", func() {
secret := &model.Secret{
RepoID: 1,
Name: "foo",
Value: "bar",
Images: []string{"docker", "gcr"},
Events: []string{"push", "tag"},
}
err := s.SetSecret(secret)
g.Assert(err == nil).IsTrue()
g.Assert(secret.ID != 0).IsTrue()
got, err := s.GetSecret(&model.Repo{ID: 1}, secret.Name)
g.Assert(err == nil).IsTrue()
g.Assert(got.Name).Equal(secret.Name)
g.Assert(got.Value).Equal(secret.Value)
g.Assert(got.Images).Equal(secret.Images)
g.Assert(got.Events).Equal(secret.Events)
})
g.It("Should update a secret", func() {
secret := &model.Secret{
RepoID: 1,
Name: "foo",
Value: "bar",
}
s.SetSecret(secret)
secret.Value = "baz"
s.SetSecret(secret)
got, err := s.GetSecret(&model.Repo{ID: 1}, secret.Name)
g.Assert(err == nil).IsTrue()
g.Assert(got.Name).Equal(secret.Name)
g.Assert(got.Value).Equal(secret.Value)
})
g.It("Should list secrets", func() {
s.SetSecret(&model.Secret{
RepoID: 1,
Name: "foo",
Value: "bar",
})
s.SetSecret(&model.Secret{
RepoID: 1,
Name: "bar",
Value: "baz",
})
secrets, err := s.GetSecretList(&model.Repo{ID: 1})
g.Assert(err == nil).IsTrue()
g.Assert(len(secrets)).Equal(2)
})
g.It("Should delete a secret", func() {
secret := &model.Secret{
RepoID: 1,
Name: "foo",
Value: "bar",
}
s.SetSecret(secret)
_, err := s.GetSecret(&model.Repo{ID: 1}, secret.Name)
g.Assert(err == nil).IsTrue()
err = s.DeleteSecret(secret)
g.Assert(err == nil).IsTrue()
_, err = s.GetSecret(&model.Repo{ID: 1}, secret.Name)
g.Assert(err != nil).IsTrue("expect a no rows in result set error")
})
})
}

View file

@ -66,6 +66,18 @@ type Store interface {
// DeleteKey deletes a user key. // DeleteKey deletes a user key.
DeleteKey(*model.Key) error DeleteKey(*model.Key) error
// GetSecretList gets a list of repository secrets
GetSecretList(*model.Repo) ([]*model.Secret, error)
// GetSecret gets the named repository secret.
GetSecret(*model.Repo, string) (*model.Secret, error)
// SetSecret sets the named repository secret.
SetSecret(*model.Secret) error
// DeleteSecret deletes the named repository secret.
DeleteSecret(*model.Secret) error
// GetBuild gets a build by unique ID. // GetBuild gets a build by unique ID.
GetBuild(int64) (*model.Build, error) GetBuild(int64) (*model.Build, error)
@ -211,6 +223,22 @@ func DeleteKey(c context.Context, key *model.Key) error {
return FromContext(c).DeleteKey(key) return FromContext(c).DeleteKey(key)
} }
func GetSecretList(c context.Context, r *model.Repo) ([]*model.Secret, error) {
return FromContext(c).GetSecretList(r)
}
func GetSecret(c context.Context, r *model.Repo, name string) (*model.Secret, error) {
return FromContext(c).GetSecret(r, name)
}
func SetSecret(c context.Context, s *model.Secret) error {
return FromContext(c).SetSecret(s)
}
func DeleteSecret(c context.Context, s *model.Secret) error {
return FromContext(c).DeleteSecret(s)
}
func GetBuild(c context.Context, id int64) (*model.Build, error) { func GetBuild(c context.Context, id int64) (*model.Build, error) {
return FromContext(c).GetBuild(id) return FromContext(c).GetBuild(id)
} }