Browse Source

Merge pull request #6881 from mitake/auth-v3-cn

 authenticate clients based on certificate CommonName in v3 API
Xiang Li 9 years ago
parent
commit
89bb9048dd
6 changed files with 84 additions and 4 deletions
  1. 27 0
      auth/store.go
  2. 34 0
      e2e/ctl_v3_auth_test.go
  3. 7 0
      e2e/etcd_test.go
  4. 2 1
      etcdserver/api/v3rpc/maintenance.go
  5. 1 1
      etcdserver/server.go
  6. 13 2
      etcdserver/v3_server.go

+ 27 - 0
auth/store.go

@@ -30,7 +30,9 @@ import (
 	"github.com/coreos/pkg/capnslog"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/net/context"
+	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/metadata"
+	"google.golang.org/grpc/peer"
 )
 
 var (
@@ -159,6 +161,9 @@ type AuthStore interface {
 
 	// AuthInfoFromCtx gets AuthInfo from gRPC's context
 	AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error)
+
+	// AuthInfoFromTLS gets AuthInfo from TLS info of gRPC's context
+	AuthInfoFromTLS(ctx context.Context) *AuthInfo
 }
 
 type authStore struct {
@@ -950,6 +955,28 @@ func (as *authStore) isValidSimpleToken(token string, ctx context.Context) bool
 	return false
 }
 
+func (as *authStore) AuthInfoFromTLS(ctx context.Context) *AuthInfo {
+	peer, ok := peer.FromContext(ctx)
+	if !ok || peer == nil || peer.AuthInfo == nil {
+		return nil
+	}
+
+	tlsInfo := peer.AuthInfo.(credentials.TLSInfo)
+	for _, chains := range tlsInfo.State.VerifiedChains {
+		for _, chain := range chains {
+			cn := chain.Subject.CommonName
+			plog.Debugf("found common name %s", cn)
+
+			return &AuthInfo{
+				Username: cn,
+				Revision: as.Revision(),
+			}
+		}
+	}
+
+	return nil
+}
+
 func (as *authStore) AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error) {
 	md, ok := metadata.FromContext(ctx)
 	if !ok {

+ 34 - 0
e2e/ctl_v3_auth_test.go

@@ -34,6 +34,7 @@ func TestCtlV3AuthMemberRemove(t *testing.T) {
 	testCtl(t, authTestMemberRemove, withQuorum(), withNoStrictReconfig())
 }
 func TestCtlV3AuthMemberUpdate(t *testing.T) { testCtl(t, authTestMemberUpdate) }
+func TestCtlV3AuthCertCN(t *testing.T)       { testCtl(t, authTestCertCN, withCfg(configClientTLSCertAuth)) }
 
 func authEnableTest(cx ctlCtx) {
 	if err := authEnable(cx); err != nil {
@@ -549,3 +550,36 @@ func authTestMemberUpdate(cx ctlCtx) {
 		cx.t.Fatal(err)
 	}
 }
+
+func authTestCertCN(cx ctlCtx) {
+	if err := ctlV3User(cx, []string{"add", "etcd", "--interactive=false"}, "User etcd created", []string{""}); err != nil {
+		cx.t.Fatal(err)
+	}
+	if err := spawnWithExpect(append(cx.PrefixArgs(), "role", "add", "test-role"), "Role test-role created"); err != nil {
+		cx.t.Fatal(err)
+	}
+	if err := ctlV3User(cx, []string{"grant-role", "etcd", "test-role"}, "Role test-role is granted to user etcd", nil); err != nil {
+		cx.t.Fatal(err)
+	}
+	cmd := append(cx.PrefixArgs(), "role", "grant-permission", "test-role", "readwrite", "foo")
+	if err := spawnWithExpect(cmd, "Role test-role updated"); err != nil {
+		cx.t.Fatal(err)
+	}
+
+	// grant a new key
+	if err := ctlV3RoleGrantPermission(cx, "test-role", grantingPerm{true, true, "hoo", "", false}); err != nil {
+		cx.t.Fatal(err)
+	}
+
+	// try a granted key
+	cx.user, cx.pass = "", ""
+	if err := ctlV3Put(cx, "hoo", "bar", ""); err != nil {
+		cx.t.Fatal(err)
+	}
+
+	// try a non granted key
+	cx.user, cx.pass = "", ""
+	if err := ctlV3PutFailPerm(cx, "baz", "bar"); err == nil {
+		cx.t.Fatal(err)
+	}
+}

+ 7 - 0
e2e/etcd_test.go

@@ -106,6 +106,13 @@ var (
 		isPeerTLS:    true,
 		initialToken: "new",
 	}
+	configClientTLSCertAuth = etcdProcessClusterConfig{
+		clusterSize:           1,
+		proxySize:             0,
+		clientTLS:             clientTLS,
+		initialToken:          "new",
+		clientCertAuthEnabled: true,
+	}
 )
 
 func configStandalone(cfg etcdProcessClusterConfig) *etcdProcessClusterConfig {

+ 2 - 1
etcdserver/api/v3rpc/maintenance.go

@@ -47,6 +47,7 @@ type RaftStatusGetter interface {
 }
 
 type AuthGetter interface {
+	AuthInfoFromCtx(ctx context.Context) (*auth.AuthInfo, error)
 	AuthStore() auth.AuthStore
 }
 
@@ -152,7 +153,7 @@ type authMaintenanceServer struct {
 }
 
 func (ams *authMaintenanceServer) isAuthenticated(ctx context.Context) error {
-	authInfo, err := ams.ag.AuthStore().AuthInfoFromCtx(ctx)
+	authInfo, err := ams.ag.AuthInfoFromCtx(ctx)
 	if err != nil {
 		return err
 	}

+ 1 - 1
etcdserver/server.go

@@ -1022,7 +1022,7 @@ func (s *EtcdServer) checkMembershipOperationPermission(ctx context.Context) err
 	// in the state machine layer
 	// However, both of membership change and role management requires the root privilege.
 	// So careful operation by admins can prevent the problem.
-	authInfo, err := s.AuthStore().AuthInfoFromCtx(ctx)
+	authInfo, err := s.AuthInfoFromCtx(ctx)
 	if err != nil {
 		return err
 	}

+ 13 - 2
etcdserver/v3_server.go

@@ -617,7 +617,7 @@ func (s *EtcdServer) RoleDelete(ctx context.Context, r *pb.AuthRoleDeleteRequest
 // doSerialize handles the auth logic, with permissions checked by "chk", for a serialized request "get". Returns a non-nil error on authentication failure.
 func (s *EtcdServer) doSerialize(ctx context.Context, chk func(*auth.AuthInfo) error, get func()) error {
 	for {
-		ai, err := s.AuthStore().AuthInfoFromCtx(ctx)
+		ai, err := s.AuthInfoFromCtx(ctx)
 		if err != nil {
 			return err
 		}
@@ -652,7 +652,7 @@ func (s *EtcdServer) processInternalRaftRequestOnce(ctx context.Context, r pb.In
 		ID: s.reqIDGen.Next(),
 	}
 
-	authInfo, err := s.AuthStore().AuthInfoFromCtx(ctx)
+	authInfo, err := s.AuthInfoFromCtx(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -802,3 +802,14 @@ func (s *EtcdServer) linearizableReadNotify(ctx context.Context) error {
 		return ErrStopped
 	}
 }
+
+func (s *EtcdServer) AuthInfoFromCtx(ctx context.Context) (*auth.AuthInfo, error) {
+	if s.Cfg.ClientCertAuthEnabled {
+		authInfo := s.AuthStore().AuthInfoFromTLS(ctx)
+		if authInfo != nil {
+			return authInfo, nil
+		}
+	}
+
+	return s.AuthStore().AuthInfoFromCtx(ctx)
+}