ability to store per-repository secrets & sign yaml
This commit is contained in:
parent
8251663686
commit
7ffa88cc09
11 changed files with 408 additions and 0 deletions
48
api/secret.go
Normal file
48
api/secret.go
Normal 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
40
api/sign.go
Normal 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
15
model/registry.go
Normal 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
27
model/secret.go
Normal 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
|
||||
}
|
|
@ -50,6 +50,7 @@ func Load(middleware ...gin.HandlerFunc) http.Handler {
|
|||
repo.GET("", web.ShowRepo)
|
||||
repo.GET("/builds/:number", web.ShowBuild)
|
||||
repo.GET("/builds/:number/:job", web.ShowBuild)
|
||||
|
||||
repo_settings := repo.Group("/settings")
|
||||
{
|
||||
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/:number", api.GetBuild)
|
||||
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
|
||||
repo.POST("/encrypt", session.MustUser(), api.PostSecure)
|
||||
|
|
32
store/datastore/ddl/mysql/3.sql
Normal file
32
store/datastore/ddl/mysql/3.sql
Normal 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;
|
32
store/datastore/ddl/postgres/3.sql
Normal file
32
store/datastore/ddl/postgres/3.sql
Normal 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;
|
34
store/datastore/ddl/sqlite3/3.sql
Normal file
34
store/datastore/ddl/sqlite3/3.sql
Normal 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
53
store/datastore/secret.go
Normal 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 = ?
|
||||
`
|
94
store/datastore/secret_test.go
Normal file
94
store/datastore/secret_test.go
Normal 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")
|
||||
})
|
||||
})
|
||||
}
|
|
@ -66,6 +66,18 @@ type Store interface {
|
|||
// DeleteKey deletes a user key.
|
||||
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(int64) (*model.Build, error)
|
||||
|
||||
|
@ -211,6 +223,22 @@ func DeleteKey(c context.Context, key *model.Key) error {
|
|||
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) {
|
||||
return FromContext(c).GetBuild(id)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue