mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-26 11:51:02 +00:00
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("", 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)
|
||||||
|
|
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 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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue