added code to manage secrets

This commit is contained in:
Brad Rydzewski 2016-04-23 04:27:28 -07:00
parent b9037b9d7c
commit f3709922b3
17 changed files with 435 additions and 24 deletions

View file

@ -35,7 +35,7 @@ func init() {
}
droneSec = fmt.Sprintf("%s.sec", strings.TrimSuffix(droneYml, filepath.Ext(droneYml)))
if os.Getenv("CANARY") == "true" {
droneSec = fmt.Sprintf("%s.sig", strings.TrimSuffix(droneYml, filepath.Ext(droneYml)))
droneSec = fmt.Sprintf("%s.sig", droneYml)
}
}
@ -291,7 +291,10 @@ func PostBuild(c *gin.Context) {
// get the previous build so that we can send
// on status change notifications
last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID)
secs, _ := store.GetSecretList(c, repo)
secs, err := store.GetSecretList(c, repo)
if err != nil {
log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err)
}
// IMPORTANT. PLEASE READ
//
@ -305,14 +308,24 @@ func PostBuild(c *gin.Context) {
var verified bool
signature, err := jose.ParseSigned(string(sec))
if err == nil && len(sec) != 0 {
if err != nil {
log.Debugf("cannot parse .drone.yml.sig file. %s", err)
} else if len(sec) == 0 {
log.Debugf("cannot parse .drone.yml.sig file. empty file")
} else {
signed = true
output, err := signature.Verify(repo.Hash)
if err == nil && string(output) == string(raw) {
output, err := signature.Verify([]byte(repo.Hash))
if err != nil {
log.Debugf("cannot verify .drone.yml.sig file. %s", err)
} else if string(output) != string(raw) {
log.Debugf("cannot verify .drone.yml.sig file. no match. %q <> %q", string(output), string(raw))
} else {
verified = true
}
}
log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified)
bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build))
for _, job := range jobs {
queue.Publish(c, &queue.Work{

View file

@ -3,11 +3,21 @@ package client
import (
"io"
"github.com/drone/drone/model"
"github.com/drone/drone/queue"
)
// Client is used to communicate with a Drone server.
type Client interface {
// Sign returns a cryptographic signature for the input string.
Sign(string, string, []byte) ([]byte, error)
// SecretPost create or updates a repository secret.
SecretPost(string, string, *model.Secret) error
// SecretDel deletes a named repository secret.
SecretDel(string, string, string) error
// Pull pulls work from the server queue.
Pull(os, arch string) (*queue.Work, error)

View file

@ -2,6 +2,7 @@ package client
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
@ -22,6 +23,24 @@ const (
pathWait = "%s/api/queue/wait/%d"
pathStream = "%s/api/queue/stream/%d"
pathPush = "%s/api/queue/status/%d"
pathSelf = "%s/api/user"
pathFeed = "%s/api/user/feed"
pathRepos = "%s/api/user/repos"
pathRepo = "%s/api/repos/%s/%s"
pathEncrypt = "%s/api/repos/%s/%s/encrypt"
pathBuilds = "%s/api/repos/%s/%s/builds"
pathBuild = "%s/api/repos/%s/%s/builds/%v"
pathJob = "%s/api/repos/%s/%s/builds/%d/%d"
pathLog = "%s/api/repos/%s/%s/logs/%d/%d"
pathKey = "%s/api/repos/%s/%s/key"
pathSign = "%s/api/repos/%s/%s/sign"
pathSecrets = "%s/api/repos/%s/%s/secrets"
pathSecret = "%s/api/repos/%s/%s/secrets/%s"
pathNodes = "%s/api/nodes"
pathNode = "%s/api/nodes/%d"
pathUsers = "%s/api/users"
pathUser = "%s/api/users/%s"
)
type client struct {
@ -34,14 +53,50 @@ func NewClient(uri string) Client {
return &client{http.DefaultClient, uri}
}
// NewClientToken returns a client at the specified url that
// authenticates all outbound requests with the given token.
// NewClientToken returns a client at the specified url that authenticates all
// outbound requests with the given token.
func NewClientToken(uri, token string) Client {
config := new(oauth2.Config)
auther := config.Client(oauth2.NoContext, &oauth2.Token{AccessToken: token})
return &client{auther, uri}
}
// NewClientTokenTLS returns a client at the specified url that authenticates
// all outbound requests with the given token and tls.Config if provided.
func NewClientTokenTLS(uri, token string, c *tls.Config) Client {
config := new(oauth2.Config)
auther := config.Client(oauth2.NoContext, &oauth2.Token{AccessToken: token})
if c != nil {
if trans, ok := auther.Transport.(*oauth2.Transport); ok {
trans.Base = &http.Transport{TLSClientConfig: c}
}
}
return &client{auther, uri}
}
// SecretPost create or updates a repository secret.
func (c *client) SecretPost(owner, name string, secret *model.Secret) error {
uri := fmt.Sprintf(pathSecrets, c.base, owner, name)
return c.post(uri, secret, nil)
}
// SecretDel deletes a named repository secret.
func (c *client) SecretDel(owner, name, secret string) error {
uri := fmt.Sprintf(pathSecret, c.base, owner, name, secret)
return c.delete(uri)
}
// Sign returns a cryptographic signature for the input string.
func (c *client) Sign(owner, name string, in []byte) ([]byte, error) {
uri := fmt.Sprintf(pathSign, c.base, owner, name)
rc, err := stream(c.client, uri, "POST", in, nil)
if err != nil {
return nil, err
}
defer rc.Close()
return ioutil.ReadAll(rc)
}
// Pull pulls work from the server queue.
func (c *client) Pull(os, arch string) (*queue.Work, error) {
out := new(queue.Work)

61
client/http.go Normal file
View file

@ -0,0 +1,61 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
)
// helper function to stream an http request
func stream(client *http.Client, rawurl, method string, in, out interface{}) (io.ReadCloser, error) {
uri, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
// if we are posting or putting data, we need to
// write it to the body of the request.
var buf io.ReadWriter
if in == nil {
// nothing
} else if rw, ok := in.(io.ReadWriter); ok {
buf = rw
} else if b, ok := in.([]byte); ok {
buf = new(bytes.Buffer)
buf.Write(b)
} else {
buf = new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(in)
if err != nil {
return nil, err
}
}
// creates a new http request to bitbucket.
req, err := http.NewRequest(method, uri.String(), buf)
if err != nil {
return nil, err
}
if in == nil {
// nothing
} else if _, ok := in.(io.ReadWriter); ok {
req.Header.Set("Content-Type", "plain/text")
} else {
req.Header.Set("Content-Type", "application/json")
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode > http.StatusPartialContent {
defer resp.Body.Close()
out, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf(string(out))
}
return resp.Body, nil
}

View file

@ -83,7 +83,16 @@ var AgentCmd = cli.Command{
EnvVar: "DRONE_PLUGIN_NETRC",
Name: "netrc-plugin",
Usage: "plugins that receive the netrc file",
Value: &cli.StringSlice{"git", "hg"},
Value: &cli.StringSlice{
"git",
"git:*",
"hg",
"hg:*",
"plugins/hg",
"plugins/hg:*",
"plugins/git",
"plugins/git:*",
},
},
cli.StringSliceFlag{
EnvVar: "DRONE_PLUGIN_PRIVILEGED",

View file

@ -66,25 +66,29 @@ func (r *pipeline) run() error {
secrets = append(secrets, &model.Secret{
Name: "DRONE_NETRC_USERNAME",
Value: w.Netrc.Login,
Images: []string{"git", "hg"}, // TODO(bradrydzewski) use the command line parameters here
Images: r.config.netrc, // TODO(bradrydzewski) use the command line parameters here
Events: []string{model.EventDeploy, model.EventPull, model.EventPush, model.EventTag},
})
secrets = append(secrets, &model.Secret{
Name: "DRONE_NETRC_PASSWORD",
Value: w.Netrc.Password,
Images: []string{"git", "hg"},
Images: r.config.netrc,
Events: []string{model.EventDeploy, model.EventPull, model.EventPush, model.EventTag},
})
secrets = append(secrets, &model.Secret{
Name: "DRONE_NETRC_MACHINE",
Value: w.Netrc.Machine,
Images: []string{"git", "hg"},
Images: r.config.netrc,
Events: []string{model.EventDeploy, model.EventPull, model.EventPush, model.EventTag},
})
}
for _, secret := range secrets {
fmt.Printf("SECRET %s %s\n", secret.Name, secret.Value)
}
trans := []compiler.Transform{
builtin.NewCloneOp("plugins/git:latest", true),
builtin.NewCloneOp("git", true),
builtin.NewCacheOp(
"plugins/cache:latest",
"/var/lib/drone/cache/"+w.Repo.FullName,

View file

@ -19,10 +19,25 @@ func main2() {
app.Name = "drone"
app.Version = version.Version
app.Usage = "command line utility"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "t, token",
Value: "",
Usage: "server auth token",
EnvVar: "DRONE_TOKEN",
},
cli.StringFlag{
Name: "s, server",
Value: "",
Usage: "server location",
EnvVar: "DRONE_SERVER",
},
}
app.Commands = []cli.Command{
agent.AgentCmd,
server.ServeCmd,
SignCmd,
SecretCmd,
}
app.Run(os.Args)

104
drone/secret.go Normal file
View file

@ -0,0 +1,104 @@
package main
import (
"fmt"
"log"
"github.com/drone/drone/model"
"github.com/codegangsta/cli"
)
// SecretCmd is the exported command for managing secrets.
var SecretCmd = cli.Command{
Name: "secret",
Usage: "manage secrets",
Subcommands: []cli.Command{
// Secret Add
{
Name: "add",
Usage: "add a secret",
ArgsUsage: "[repo] [key] [value]",
UsageText: "foo",
Action: func(c *cli.Context) {
if err := secretAdd(c); err != nil {
log.Fatalln(err)
}
},
Flags: []cli.Flag{
cli.StringSliceFlag{
Name: "event",
Usage: "inject the secret for these event types",
Value: &cli.StringSlice{
model.EventPush,
model.EventTag,
model.EventDeploy,
},
},
cli.StringSliceFlag{
Name: "image",
Usage: "inject the secret for these image types",
Value: &cli.StringSlice{},
},
},
},
// Secret Delete
{
Name: "rm",
Usage: "remove a secret",
Action: func(c *cli.Context) {
if err := secretDel(c); err != nil {
log.Fatalln(err)
}
},
},
},
}
func secretAdd(c *cli.Context) error {
repo := c.Args().First()
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
tail := c.Args().Tail()
if len(tail) != 2 {
cli.ShowSubcommandHelp(c)
return nil
}
secret := &model.Secret{}
secret.Name = tail[0]
secret.Value = tail[1]
secret.Images = c.StringSlice("image")
secret.Events = c.StringSlice("event")
if len(secret.Images) == 0 {
return fmt.Errorf("Please specify the --image parameter")
}
client, err := newClient(c)
if err != nil {
return err
}
return client.SecretPost(owner, name, secret)
}
func secretDel(c *cli.Context) error {
repo := c.Args().First()
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
secret := c.Args().Get(1)
client, err := newClient(c)
if err != nil {
return err
}
return client.SecretDel(owner, name, secret)
}

56
drone/sign.go Normal file
View file

@ -0,0 +1,56 @@
package main
import (
"io/ioutil"
"log"
"github.com/codegangsta/cli"
)
// SignCmd is the exported command for signing the yaml.
var SignCmd = cli.Command{
Name: "sign",
Usage: "creates a secure yaml file",
Action: func(c *cli.Context) {
if err := sign(c); err != nil {
log.Fatalln(err)
}
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "in",
Usage: "input file",
Value: ".drone.yml",
},
cli.StringFlag{
Name: "out",
Usage: "output file signature",
Value: ".drone.yml.sig",
},
},
}
func sign(c *cli.Context) error {
repo := c.Args().First()
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
in, err := readInput(c.String("in"))
if err != nil {
return err
}
client, err := newClient(c)
if err != nil {
return err
}
sig, err := client.Sign(owner, name, in)
if err != nil {
return err
}
return ioutil.WriteFile(c.String("out"), sig, 0664)
}

53
drone/util.go Normal file
View file

@ -0,0 +1,53 @@
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/drone/drone/client"
"github.com/codegangsta/cli"
"github.com/jackspirou/syscerts"
)
func newClient(c *cli.Context) (client.Client, error) {
var token = c.GlobalString("token")
var server = c.GlobalString("server")
// if no server url is provided we can default
// to the hosted Drone service.
if len(server) == 0 {
return nil, fmt.Errorf("Error: you must provide the Drone server address.")
}
if len(token) == 0 {
return nil, fmt.Errorf("Error: you must provide your Drone access token.")
}
// attempt to find system CA certs
certs := syscerts.SystemRootsPool()
tlsConfig := &tls.Config{RootCAs: certs}
// create the drone client with TLS options
return client.NewClientTokenTLS(server, token, tlsConfig), nil
}
func parseRepo(str string) (user, repo string, err error) {
var parts = strings.Split(str, "/")
if len(parts) != 2 {
err = fmt.Errorf("Error: Invalid or missing repository. eg octocat/hello-world.")
return
}
user = parts[0]
repo = parts[1]
return
}
func readInput(in string) ([]byte, error) {
if in == "-" {
return ioutil.ReadAll(os.Stdin)
}
return ioutil.ReadFile(in)
}

View file

@ -79,7 +79,7 @@ func argsToEnv(from map[string]interface{}, to map[string]string) error {
} else {
out, err = json.YAMLToJSON(out)
if err != nil {
println(err.Error())
// return err TODO(bradrydzewski) unit test coverage for possible errors
}
to[k] = string(out)
}

View file

@ -16,6 +16,6 @@ type Work struct {
Netrc *model.Netrc `json:"netrc"`
Keys *model.Key `json:"keys"`
System *model.System `json:"system"`
Secrets []*model.Secret `json:"secret"`
Secrets []*model.Secret `json:"secrets"`
User *model.User `json:"user"`
}

View file

@ -33,10 +33,7 @@ func TestRepos(t *testing.T) {
err1 := s.CreateRepo(&repo)
err2 := s.UpdateRepo(&repo)
getrepo, err3 := s.GetRepo(repo.ID)
if err3 != nil {
println("Get Repo Error")
println(err3.Error())
}
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsTrue()

7
stream/reader_test.go Normal file
View file

@ -0,0 +1,7 @@
package stream
import "testing"
func TetsReader(t *testing.T) {
t.Skip() //TODO(bradrydzewski) implement reader tests
}

View file

@ -0,0 +1,7 @@
package stream
import "testing"
func TetsStream(t *testing.T) {
t.Skip() //TODO(bradrydzewski) implement stream tests
}

7
stream/writer_test.go Normal file
View file

@ -0,0 +1,7 @@
package stream
import "testing"
func TetsWriter(t *testing.T) {
t.Skip() //TODO(bradrydzewski) implement writer tests
}

View file

@ -33,7 +33,7 @@ func init() {
}
droneSec = fmt.Sprintf("%s.sec", strings.TrimSuffix(droneYml, filepath.Ext(droneYml)))
if os.Getenv("CANARY") == "true" {
droneSec = fmt.Sprintf("%s.sig", strings.TrimSuffix(droneYml, filepath.Ext(droneYml)))
droneSec = fmt.Sprintf("%s.sig", droneYml)
}
}
@ -209,7 +209,10 @@ func PostHook(c *gin.Context) {
// get the previous build so that we can send
// on status change notifications
last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID)
secs, _ := store.GetSecretList(c, repo)
secs, err := store.GetSecretList(c, repo)
if err != nil {
log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err)
}
// IMPORTANT. PLEASE READ
//
@ -223,14 +226,24 @@ func PostHook(c *gin.Context) {
var verified bool
signature, err := jose.ParseSigned(string(sec))
if err == nil && len(sec) != 0 {
if err != nil {
log.Debugf("cannot parse .drone.yml.sig file. %s", err)
} else if len(sec) == 0 {
log.Debugf("cannot parse .drone.yml.sig file. empty file")
} else {
signed = true
output, err := signature.Verify(repo.Hash)
if err == nil && string(output) == string(raw) {
output, err := signature.Verify([]byte(repo.Hash))
if err != nil {
log.Debugf("cannot verify .drone.yml.sig file. %s", err)
} else if string(output) != string(raw) {
log.Debugf("cannot verify .drone.yml.sig file. no match")
} else {
verified = true
}
}
log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified)
bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build))
for _, job := range jobs {
queue.Publish(c, &queue.Work{