diff --git a/api/secret.go b/api/secret.go new file mode 100644 index 000000000..cc3384552 --- /dev/null +++ b/api/secret.go @@ -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, "") +} diff --git a/api/sign.go b/api/sign.go new file mode 100644 index 000000000..95d13c5ad --- /dev/null +++ b/api/sign.go @@ -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) +} diff --git a/model/registry.go b/model/registry.go new file mode 100644 index 000000000..eec2408d7 --- /dev/null +++ b/model/registry.go @@ -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 +} diff --git a/model/secret.go b/model/secret.go new file mode 100644 index 000000000..6fe7d625e --- /dev/null +++ b/model/secret.go @@ -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 +} diff --git a/router/router.go b/router/router.go index 43fe0a0de..d3eebbf30 100644 --- a/router/router.go +++ b/router/router.go @@ -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) diff --git a/store/datastore/ddl/mysql/3.sql b/store/datastore/ddl/mysql/3.sql new file mode 100644 index 000000000..5e859adc9 --- /dev/null +++ b/store/datastore/ddl/mysql/3.sql @@ -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; diff --git a/store/datastore/ddl/postgres/3.sql b/store/datastore/ddl/postgres/3.sql new file mode 100644 index 000000000..ac847e4f9 --- /dev/null +++ b/store/datastore/ddl/postgres/3.sql @@ -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; diff --git a/store/datastore/ddl/sqlite3/3.sql b/store/datastore/ddl/sqlite3/3.sql new file mode 100644 index 000000000..88b3e1eb3 --- /dev/null +++ b/store/datastore/ddl/sqlite3/3.sql @@ -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; diff --git a/store/datastore/secret.go b/store/datastore/secret.go new file mode 100644 index 000000000..528a859b0 --- /dev/null +++ b/store/datastore/secret.go @@ -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 = ? +` diff --git a/store/datastore/secret_test.go b/store/datastore/secret_test.go new file mode 100644 index 000000000..5de32f71e --- /dev/null +++ b/store/datastore/secret_test.go @@ -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") + }) + }) +} diff --git a/store/store.go b/store/store.go index 457177248..1ee916ec3 100644 --- a/store/store.go +++ b/store/store.go @@ -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) }