diff --git a/e2e/v3_curl_test.go b/e2e/v3_curl_test.go index af137c4a7..87ae535fa 100644 --- a/e2e/v3_curl_test.go +++ b/e2e/v3_curl_test.go @@ -15,9 +15,13 @@ package e2e import ( + "encoding/base64" "encoding/json" + "path" + "strconv" "testing" + epb "github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb" pb "github.com/coreos/etcd/etcdserver/etcdserverpb" "github.com/coreos/etcd/pkg/testutil" @@ -162,3 +166,204 @@ func TestV3CurlTxn(t *testing.T) { t.Fatalf("failed put with curl (%v)", err) } } + +func TestV3CurlAuthAlpha(t *testing.T) { testV3CurlAuth(t, "/v3alpha") } +func TestV3CurlAuthBeta(t *testing.T) { testV3CurlAuth(t, "/v3beta") } +func testV3CurlAuth(t *testing.T, pathPrefix string) { + defer testutil.AfterTest(t) + epc, err := newEtcdProcessCluster(&configNoTLS) + if err != nil { + t.Fatalf("could not start etcd process cluster (%v)", err) + } + defer func() { + if cerr := epc.Close(); err != nil { + t.Fatalf("error closing etcd processes (%v)", cerr) + } + }() + + // create root user + userreq, err := json.Marshal(&pb.AuthUserAddRequest{Name: string("root"), Password: string("toor")}) + testutil.AssertNil(t, err) + + if err = cURLPost(epc, cURLReq{endpoint: path.Join(pathPrefix, "/auth/user/add"), value: string(userreq), expected: "revision"}); err != nil { + t.Fatalf("failed add user with curl (%v)", err) + } + + // create root role + rolereq, err := json.Marshal(&pb.AuthRoleAddRequest{Name: string("root")}) + testutil.AssertNil(t, err) + + if err = cURLPost(epc, cURLReq{endpoint: path.Join(pathPrefix, "/auth/role/add"), value: string(rolereq), expected: "revision"}); err != nil { + t.Fatalf("failed create role with curl (%v)", err) + } + + // grant root role + grantrolereq, err := json.Marshal(&pb.AuthUserGrantRoleRequest{User: string("root"), Role: string("root")}) + testutil.AssertNil(t, err) + + if err = cURLPost(epc, cURLReq{endpoint: path.Join(pathPrefix, "/auth/user/grant"), value: string(grantrolereq), expected: "revision"}); err != nil { + t.Fatalf("failed grant role with curl (%v)", err) + } + + // enable auth + if err = cURLPost(epc, cURLReq{endpoint: path.Join(pathPrefix, "/auth/enable"), value: string("{}"), expected: "revision"}); err != nil { + t.Fatalf("failed enable auth with curl (%v)", err) + } + + // put "bar" into "foo" + putreq, err := json.Marshal(&pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}) + testutil.AssertNil(t, err) + + // fail put no auth + if err = cURLPost(epc, cURLReq{endpoint: path.Join(pathPrefix, "/kv/put"), value: string(putreq), expected: "error"}); err != nil { + t.Fatalf("failed no auth put with curl (%v)", err) + } + + // auth request + authreq, err := json.Marshal(&pb.AuthenticateRequest{Name: string("root"), Password: string("toor")}) + testutil.AssertNil(t, err) + + var ( + authHeader string + cmdArgs []string + lineFunc = func(txt string) bool { return true } + ) + + cmdArgs = cURLPrefixArgs(epc, "POST", cURLReq{endpoint: path.Join(pathPrefix, "/auth/authenticate"), value: string(authreq)}) + proc, err := spawnCmd(cmdArgs) + testutil.AssertNil(t, err) + + cURLRes, err := proc.ExpectFunc(lineFunc) + testutil.AssertNil(t, err) + + authRes := make(map[string]interface{}) + testutil.AssertNil(t, json.Unmarshal([]byte(cURLRes), &authRes)) + + token, ok := authRes["token"].(string) + if !ok { + t.Fatalf("failed invalid token in authenticate response with curl") + } + + authHeader = "Authorization : " + token + + // put with auth + if err = cURLPost(epc, cURLReq{endpoint: path.Join(pathPrefix, "/kv/put"), value: string(putreq), header: authHeader, expected: "revision"}); err != nil { + t.Fatalf("failed auth put with curl (%v)", err) + } +} + +func TestV3CurlCampaignAlpha(t *testing.T) { testV3CurlCampaign(t, "/v3alpha") } +func TestV3CurlCampaignBeta(t *testing.T) { testV3CurlCampaign(t, "/v3beta") } +func testV3CurlCampaign(t *testing.T, pathPrefix string) { + defer testutil.AfterTest(t) + + epc, err := newEtcdProcessCluster(&configNoTLS) + if err != nil { + t.Fatalf("could not start etcd process cluster (%v)", err) + } + defer func() { + if cerr := epc.Close(); err != nil { + t.Fatalf("error closing etcd processes (%v)", cerr) + } + }() + + cdata, err := json.Marshal(&epb.CampaignRequest{ + Name: []byte("/election-prefix"), + Value: []byte("v1"), + }) + if err != nil { + t.Fatal(err) + } + cargs := cURLPrefixArgs(epc, "POST", cURLReq{ + endpoint: path.Join(pathPrefix, "/election/campaign"), + value: string(cdata), + }) + lines, err := spawnWithExpectLines(cargs, `"leader":{"name":"`) + if err != nil { + t.Fatalf("failed post campaign request (%s) (%v)", pathPrefix, err) + } + if len(lines) != 1 { + t.Fatalf("len(lines) expected 1, got %+v", lines) + } + + var cresp campaignResponse + if err = json.Unmarshal([]byte(lines[0]), &cresp); err != nil { + t.Fatalf("failed to unmarshal campaign response %v", err) + } + ndata, err := base64.StdEncoding.DecodeString(cresp.Leader.Name) + if err != nil { + t.Fatalf("failed to decode leader key %v", err) + } + kdata, err := base64.StdEncoding.DecodeString(cresp.Leader.Key) + if err != nil { + t.Fatalf("failed to decode leader key %v", err) + } + + rev, _ := strconv.ParseInt(cresp.Leader.Rev, 10, 64) + lease, _ := strconv.ParseInt(cresp.Leader.Lease, 10, 64) + pdata, err := json.Marshal(&epb.ProclaimRequest{ + Leader: &epb.LeaderKey{ + Name: ndata, + Key: kdata, + Rev: rev, + Lease: lease, + }, + Value: []byte("v2"), + }) + if err != nil { + t.Fatal(err) + } + if err = cURLPost(epc, cURLReq{ + endpoint: path.Join(pathPrefix, "/election/proclaim"), + value: string(pdata), + expected: `"revision":`, + }); err != nil { + t.Fatalf("failed post proclaim request (%s) (%v)", pathPrefix, err) + } +} + +func TestV3CurlProclaimMissiongLeaderKeyNoTLS(t *testing.T) { + testCtl(t, testV3CurlProclaimMissiongLeaderKey, withCfg(configNoTLS)) +} + +func testV3CurlProclaimMissiongLeaderKey(cx ctlCtx) { + pdata, err := json.Marshal(&epb.ProclaimRequest{Value: []byte("v2")}) + if err != nil { + cx.t.Fatal(err) + } + if err != nil { + cx.t.Fatal(err) + } + if err = cURLPost(cx.epc, cURLReq{ + endpoint: path.Join("/v3beta", "/election/proclaim"), + value: string(pdata), + expected: `{"error":"\"leader\" field must be provided","code":2}`, + }); err != nil { + cx.t.Fatalf("failed post proclaim request (%s) (%v)", "/v3beta", err) + } +} + +func TestV3CurlResignMissiongLeaderKeyNoTLS(t *testing.T) { + testCtl(t, testV3CurlResignMissiongLeaderKey, withCfg(configNoTLS)) +} + +func testV3CurlResignMissiongLeaderKey(cx ctlCtx) { + if err := cURLPost(cx.epc, cURLReq{ + endpoint: path.Join("/v3beta", "/election/resign"), + value: `{}`, + expected: `{"error":"\"leader\" field must be provided","code":2}`, + }); err != nil { + cx.t.Fatalf("failed post resign request (%s) (%v)", "/v3beta", err) + } +} + +// to manually decode; JSON marshals integer fields with +// string types, so can't unmarshal with epb.CampaignResponse +type campaignResponse struct { + Leader struct { + Name string `json:"name,omitempty"` + Key string `json:"key,omitempty"` + Rev string `json:"rev,omitempty"` + Lease string `json:"lease,omitempty"` + } `json:"leader,omitempty"` +} diff --git a/etcdserver/api/v3election/election.go b/etcdserver/api/v3election/election.go index f9061c079..d1b2767be 100644 --- a/etcdserver/api/v3election/election.go +++ b/etcdserver/api/v3election/election.go @@ -15,6 +15,8 @@ package v3election import ( + "errors" + "golang.org/x/net/context" "github.com/coreos/etcd/clientv3" @@ -22,6 +24,10 @@ import ( epb "github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb" ) +// ErrMissingLeaderKey is returned when election API request +// is missing the "leader" field. +var ErrMissingLeaderKey = errors.New(`"leader" field must be provided`) + type electionServer struct { c *clientv3.Client } @@ -51,6 +57,9 @@ func (es *electionServer) Campaign(ctx context.Context, req *epb.CampaignRequest } func (es *electionServer) Proclaim(ctx context.Context, req *epb.ProclaimRequest) (*epb.ProclaimResponse, error) { + if req.Leader == nil { + return nil, ErrMissingLeaderKey + } s, err := es.session(ctx, req.Leader.Lease) if err != nil { return nil, err @@ -98,6 +107,9 @@ func (es *electionServer) Leader(ctx context.Context, req *epb.LeaderRequest) (* } func (es *electionServer) Resign(ctx context.Context, req *epb.ResignRequest) (*epb.ResignResponse, error) { + if req.Leader == nil { + return nil, ErrMissingLeaderKey + } s, err := es.session(ctx, req.Leader.Lease) if err != nil { return nil, err