瀏覽代碼

Merge pull request #3408 from MSamman/extend-auth-api

etcdserver: extend auth api
Xiang Li 10 年之前
父節點
當前提交
ea3dbfed60
共有 3 個文件被更改,包括 326 次插入93 次删除
  1. 91 15
      Documentation/auth_api.md
  2. 80 10
      etcdserver/etcdhttp/client_auth.go
  3. 155 68
      etcdserver/etcdhttp/client_auth_test.go

+ 91 - 15
Documentation/auth_api.md

@@ -124,7 +124,7 @@ The User JSON object is formed as follows:
 
 Password is only passed when necessary.
 
-**Get a list of users**
+**Get a List of Users**
 
 GET/HEAD  /v2/auth/users
 
@@ -137,7 +137,36 @@ GET/HEAD  /v2/auth/users
         Content-type: application/json
     200 Body:
         {
-          "users": ["alice", "bob", "eve"]
+          "users": [
+            {
+              "user": "alice",
+              "roles": [
+                {
+                  "role": "root",
+                  "permissions": {
+                    "kv": {
+                      "read": ["*"],
+                      "write": ["*"]
+                    }
+                  }
+                }
+              ]
+            },
+            {
+              "user": "bob",
+              "roles": [
+                {
+                  "role": "guest",
+                  "permissions": {
+                    "kv": {
+                      "read": ["*"],
+                      "write": ["*"]
+                    }
+                  }
+                }
+              ]
+            }
+          ]
         }
 
 **Get User Details**
@@ -155,7 +184,26 @@ GET/HEAD  /v2/auth/users/alice
     200 Body:
         {
           "user" : "alice",
-          "roles" : ["fleet", "etcd"]
+          "roles" : [
+            {
+              "role": "fleet",
+              "permissions" : {
+                "kv" : {
+                  "read": [ "/fleet/" ],
+                  "write": [ "/fleet/" ]
+                }
+              }
+            },
+            {
+              "role": "etcd",
+              "permissions" : {
+                "kv" : {
+                  "read": [ "*" ],
+                  "write": [ "*" ]
+                }
+              }
+            }
+          ]
         }
 
 **Create Or Update A User**
@@ -213,43 +261,71 @@ A full role structure may look like this. A Permission List structure is used fo
 }
 ```
 
-**Get a list of Roles**
+**Get Role Details**
 
-GET/HEAD  /v2/auth/roles
+GET/HEAD  /v2/auth/roles/fleet
 
     Sent Headers:
         Authorization: Basic <BasicAuthString>
     Possible Status Codes:
         200 OK
         401 Unauthorized
+        404 Not Found
     200 Headers:
         Content-type: application/json
     200 Body:
         {
-          "roles": ["fleet", "etcd", "quay"]
+          "role" : "fleet",
+          "permissions" : {
+            "kv" : {
+              "read": [ "/fleet/" ],
+              "write": [ "/fleet/" ]
+            }
+          }
         }
 
-**Get Role Details**
+**Get a list of Roles**
 
-GET/HEAD  /v2/auth/roles/fleet
+GET/HEAD  /v2/auth/roles
 
     Sent Headers:
         Authorization: Basic <BasicAuthString>
     Possible Status Codes:
         200 OK
         401 Unauthorized
-        404 Not Found
     200 Headers:
         Content-type: application/json
     200 Body:
         {
-          "role" : "fleet",
-          "permissions" : {
-            "kv" : {
-              "read": [ "/fleet/" ],
-              "write": [ "/fleet/" ]
+          "roles": [
+            {
+              "role": "fleet",
+              "permissions": {
+                "kv": {
+                  "read": ["/fleet/"],
+                  "write": ["/fleet/"]
+                }
+              }
+            },
+            {
+              "role": "etcd",
+              "permissions": {
+                "kv": {
+                  "read": ["*"],
+                  "write": ["*"]
+                }
+              }
+            },
+            {
+              "role": "quay",
+              "permissions": {
+                "kv": {
+                  "read": ["*"],
+                  "write": ["*"]
+                }
+              }
             }
-          }
+          ]
         }
 
 **Create Or Update A Role**

+ 80 - 10
etcdserver/etcdhttp/client_auth.go

@@ -147,11 +147,10 @@ func (sh *authHandler) baseRoles(w http.ResponseWriter, r *http.Request) {
 		writeNoAuth(w)
 		return
 	}
+
 	w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
 	w.Header().Set("Content-Type", "application/json")
-	var rolesCollections struct {
-		Roles []string `json:"roles"`
-	}
+
 	roles, err := sh.sec.AllRoles()
 	if err != nil {
 		writeError(w, err)
@@ -161,10 +160,30 @@ func (sh *authHandler) baseRoles(w http.ResponseWriter, r *http.Request) {
 		roles = make([]string, 0)
 	}
 
-	rolesCollections.Roles = roles
+	err = r.ParseForm()
+	if err != nil {
+		writeError(w, err)
+		return
+	}
+
+	var rolesCollections struct {
+		Roles []auth.Role `json:"roles"`
+	}
+	for _, roleName := range roles {
+		var role auth.Role
+		role, err = sh.sec.GetRole(roleName)
+		if err != nil {
+			writeError(w, err)
+			return
+		}
+		rolesCollections.Roles = append(rolesCollections.Roles, role)
+	}
 	err = json.NewEncoder(w).Encode(rolesCollections)
+
 	if err != nil {
 		plog.Warningf("baseRoles error encoding on %s", r.URL)
+		writeError(w, err)
+		return
 	}
 }
 
@@ -259,6 +278,11 @@ func (sh *authHandler) forRole(w http.ResponseWriter, r *http.Request, role stri
 	}
 }
 
+type userWithRoles struct {
+	User  string      `json:"user"`
+	Roles []auth.Role `json:"roles,omitempty"`
+}
+
 func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
 	if !allowMethod(w, r.Method, "GET") {
 		return
@@ -269,9 +293,7 @@ func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
 	}
 	w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
 	w.Header().Set("Content-Type", "application/json")
-	var usersCollections struct {
-		Users []string `json:"users"`
-	}
+
 	users, err := sh.sec.AllUsers()
 	if err != nil {
 		writeError(w, err)
@@ -281,10 +303,42 @@ func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
 		users = make([]string, 0)
 	}
 
-	usersCollections.Users = users
+	err = r.ParseForm()
+	if err != nil {
+		writeError(w, err)
+		return
+	}
+
+	var usersCollections struct {
+		Users []userWithRoles `json:"users"`
+	}
+	for _, userName := range users {
+		var user auth.User
+		user, err = sh.sec.GetUser(userName)
+		if err != nil {
+			writeError(w, err)
+			return
+		}
+
+		uwr := userWithRoles{User: user.User}
+		for _, roleName := range user.Roles {
+			var role auth.Role
+			role, err = sh.sec.GetRole(roleName)
+			if err != nil {
+				writeError(w, err)
+				return
+			}
+			uwr.Roles = append(uwr.Roles, role)
+		}
+
+		usersCollections.Users = append(usersCollections.Users, uwr)
+	}
 	err = json.NewEncoder(w).Encode(usersCollections)
+
 	if err != nil {
 		plog.Warningf("baseUsers error encoding on %s", r.URL)
+		writeError(w, err)
+		return
 	}
 }
 
@@ -322,9 +376,25 @@ func (sh *authHandler) forUser(w http.ResponseWriter, r *http.Request, user stri
 			writeError(w, err)
 			return
 		}
-		u.Password = ""
 
-		err = json.NewEncoder(w).Encode(u)
+		err = r.ParseForm()
+		if err != nil {
+			writeError(w, err)
+			return
+		}
+
+		uwr := userWithRoles{User: u.User}
+		for _, roleName := range u.Roles {
+			var role auth.Role
+			role, err = sh.sec.GetRole(roleName)
+			if err != nil {
+				writeError(w, err)
+				return
+			}
+			uwr.Roles = append(uwr.Roles, role)
+		}
+		err = json.NewEncoder(w).Encode(uwr)
+
 		if err != nil {
 			plog.Warningf("forUser error encoding on %s", r.URL)
 			return

+ 155 - 68
etcdserver/etcdhttp/client_auth_test.go

@@ -37,16 +37,22 @@ func mustJSONRequest(t *testing.T, method string, p string, body string) *http.R
 }
 
 type mockAuthStore struct {
-	user    *auth.User
+	users   map[string]*auth.User
 	roles   map[string]*auth.Role
 	err     error
 	enabled bool
 }
 
-func (s *mockAuthStore) AllUsers() ([]string, error)            { return []string{"alice", "bob", "root"}, s.err }
-func (s *mockAuthStore) GetUser(name string) (auth.User, error) { return *s.user, s.err }
+func (s *mockAuthStore) AllUsers() ([]string, error) { return []string{"alice", "bob", "root"}, s.err }
+func (s *mockAuthStore) GetUser(name string) (auth.User, error) {
+	u, ok := s.users[name]
+	if !ok {
+		return auth.User{}, s.err
+	}
+	return *u, s.err
+}
 func (s *mockAuthStore) CreateOrUpdateUser(user auth.User) (out auth.User, created bool, err error) {
-	if s.user == nil {
+	if s.users == nil {
 		u, err := s.CreateUser(user)
 		return u, true, err
 	}
@@ -55,7 +61,9 @@ func (s *mockAuthStore) CreateOrUpdateUser(user auth.User) (out auth.User, creat
 }
 func (s *mockAuthStore) CreateUser(user auth.User) (auth.User, error) { return user, s.err }
 func (s *mockAuthStore) DeleteUser(name string) error                 { return s.err }
-func (s *mockAuthStore) UpdateUser(user auth.User) (auth.User, error) { return *s.user, s.err }
+func (s *mockAuthStore) UpdateUser(user auth.User) (auth.User, error) {
+	return *s.users[user.User], s.err
+}
 func (s *mockAuthStore) AllRoles() ([]string, error) {
 	return []string{"awesome", "guest", "root"}, s.err
 }
@@ -95,22 +103,64 @@ func TestAuthFlow(t *testing.T) {
 		},
 		// Users
 		{
-			req:   mustJSONRequest(t, "GET", "users", ""),
-			store: mockAuthStore{},
+			req: mustJSONRequest(t, "GET", "users", ""),
+			store: mockAuthStore{
+				users: map[string]*auth.User{
+					"alice": {
+						User:     "alice",
+						Roles:    []string{"alicerole", "guest"},
+						Password: "wheeee",
+					},
+					"bob": {
+						User:     "bob",
+						Roles:    []string{"guest"},
+						Password: "wheeee",
+					},
+					"root": {
+						User:     "root",
+						Roles:    []string{"root"},
+						Password: "wheeee",
+					},
+				},
+				roles: map[string]*auth.Role{
+					"alicerole": {
+						Role: "alicerole",
+					},
+					"guest": {
+						Role: "guest",
+					},
+					"root": {
+						Role: "root",
+					},
+				},
+			},
 			wcode: http.StatusOK,
-			wbody: `{"users":["alice","bob","root"]}`,
+			wbody: `{"users":[` +
+				`{"user":"alice","roles":[` +
+				`{"role":"alicerole","permissions":{"kv":{"read":null,"write":null}}},` +
+				`{"role":"guest","permissions":{"kv":{"read":null,"write":null}}}` +
+				`]},` +
+				`{"user":"bob","roles":[{"role":"guest","permissions":{"kv":{"read":null,"write":null}}}]},` +
+				`{"user":"root","roles":[{"role":"root","permissions":{"kv":{"read":null,"write":null}}}]}]}`,
 		},
 		{
 			req: mustJSONRequest(t, "GET", "users/alice", ""),
 			store: mockAuthStore{
-				user: &auth.User{
-					User:     "alice",
-					Roles:    []string{"alicerole", "guest"},
-					Password: "wheeee",
+				users: map[string]*auth.User{
+					"alice": {
+						User:     "alice",
+						Roles:    []string{"alicerole"},
+						Password: "wheeee",
+					},
+				},
+				roles: map[string]*auth.Role{
+					"alicerole": {
+						Role: "alicerole",
+					},
 				},
 			},
 			wcode: http.StatusOK,
-			wbody: `{"user":"alice","roles":["alicerole","guest"]}`,
+			wbody: `{"user":"alice","roles":[{"role":"alicerole","permissions":{"kv":{"read":null,"write":null}}}]}`,
 		},
 		{
 			req:   mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`),
@@ -127,10 +177,12 @@ func TestAuthFlow(t *testing.T) {
 		{
 			req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`),
 			store: mockAuthStore{
-				user: &auth.User{
-					User:     "alice",
-					Roles:    []string{"alicerole", "guest"},
-					Password: "wheeee",
+				users: map[string]*auth.User{
+					"alice": {
+						User:     "alice",
+						Roles:    []string{"alicerole", "guest"},
+						Password: "wheeee",
+					},
 				},
 			},
 			wcode: http.StatusOK,
@@ -139,10 +191,12 @@ func TestAuthFlow(t *testing.T) {
 		{
 			req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "grant": ["alicerole"]}`),
 			store: mockAuthStore{
-				user: &auth.User{
-					User:     "alice",
-					Roles:    []string{"alicerole", "guest"},
-					Password: "wheeee",
+				users: map[string]*auth.User{
+					"alice": {
+						User:     "alice",
+						Roles:    []string{"alicerole", "guest"},
+						Password: "wheeee",
+					},
 				},
 			},
 			wcode: http.StatusOK,
@@ -151,13 +205,12 @@ func TestAuthFlow(t *testing.T) {
 		{
 			req: mustJSONRequest(t, "GET", "users/alice", ``),
 			store: mockAuthStore{
-				user: &auth.User{},
-				err:  auth.Error{Status: http.StatusNotFound, Errmsg: "auth: User alice doesn't exist."},
+				users: map[string]*auth.User{},
+				err:   auth.Error{Status: http.StatusNotFound, Errmsg: "auth: User alice doesn't exist."},
 			},
 			wcode: http.StatusNotFound,
 			wbody: `{"message":"auth: User alice doesn't exist."}`,
 		},
-		// Roles
 		{
 			req: mustJSONRequest(t, "GET", "roles/manager", ""),
 			store: mockAuthStore{
@@ -195,10 +248,24 @@ func TestAuthFlow(t *testing.T) {
 			wbody: `{"role":"manager","permissions":{"kv":{"read":null,"write":null}}}`,
 		},
 		{
-			req:   mustJSONRequest(t, "GET", "roles", ""),
-			store: mockAuthStore{},
+			req: mustJSONRequest(t, "GET", "roles", ""),
+			store: mockAuthStore{
+				roles: map[string]*auth.Role{
+					"awesome": {
+						Role: "awesome",
+					},
+					"guest": {
+						Role: "guest",
+					},
+					"root": {
+						Role: "root",
+					},
+				},
+			},
 			wcode: http.StatusOK,
-			wbody: `{"roles":["awesome","guest","root"]}`,
+			wbody: `{"roles":[{"role":"awesome","permissions":{"kv":{"read":null,"write":null}}},` +
+				`{"role":"guest","permissions":{"kv":{"read":null,"write":null}}},` +
+				`{"role":"root","permissions":{"kv":{"read":null,"write":null}}}]}`,
 		},
 		{
 			req: mustJSONRequest(t, "GET", "enable", ""),
@@ -224,10 +291,12 @@ func TestAuthFlow(t *testing.T) {
 			})(),
 			store: mockAuthStore{
 				enabled: true,
-				user: &auth.User{
-					User:     "root",
-					Password: goodPassword,
-					Roles:    []string{"root"},
+				users: map[string]*auth.User{
+					"root": {
+						User:     "root",
+						Password: goodPassword,
+						Roles:    []string{"root"},
+					},
 				},
 				roles: map[string]*auth.Role{
 					"root": {
@@ -246,10 +315,12 @@ func TestAuthFlow(t *testing.T) {
 			})(),
 			store: mockAuthStore{
 				enabled: true,
-				user: &auth.User{
-					User:     "root",
-					Password: goodPassword,
-					Roles:    []string{"root"},
+				users: map[string]*auth.User{
+					"root": {
+						User:     "root",
+						Password: goodPassword,
+						Roles:    []string{"root"},
+					},
 				},
 				roles: map[string]*auth.Role{
 					"root": {
@@ -304,10 +375,12 @@ func TestPrefixAccess(t *testing.T) {
 			key: "/foo",
 			req: mustAuthRequest("GET", "root", "good"),
 			store: &mockAuthStore{
-				user: &auth.User{
-					User:     "root",
-					Password: goodPassword,
-					Roles:    []string{"root"},
+				users: map[string]*auth.User{
+					"root": {
+						User:     "root",
+						Password: goodPassword,
+						Roles:    []string{"root"},
+					},
 				},
 				roles: map[string]*auth.Role{
 					"root": {
@@ -324,10 +397,12 @@ func TestPrefixAccess(t *testing.T) {
 			key: "/foo",
 			req: mustAuthRequest("GET", "user", "good"),
 			store: &mockAuthStore{
-				user: &auth.User{
-					User:     "user",
-					Password: goodPassword,
-					Roles:    []string{"foorole"},
+				users: map[string]*auth.User{
+					"user": {
+						User:     "user",
+						Password: goodPassword,
+						Roles:    []string{"foorole"},
+					},
 				},
 				roles: map[string]*auth.Role{
 					"foorole": {
@@ -350,10 +425,12 @@ func TestPrefixAccess(t *testing.T) {
 			key: "/foo",
 			req: mustAuthRequest("GET", "user", "good"),
 			store: &mockAuthStore{
-				user: &auth.User{
-					User:     "user",
-					Password: goodPassword,
-					Roles:    []string{"foorole"},
+				users: map[string]*auth.User{
+					"user": {
+						User:     "user",
+						Password: goodPassword,
+						Roles:    []string{"foorole"},
+					},
 				},
 				roles: map[string]*auth.Role{
 					"foorole": {
@@ -376,10 +453,12 @@ func TestPrefixAccess(t *testing.T) {
 			key: "/foo",
 			req: mustAuthRequest("GET", "user", "bad"),
 			store: &mockAuthStore{
-				user: &auth.User{
-					User:     "user",
-					Password: goodPassword,
-					Roles:    []string{"foorole"},
+				users: map[string]*auth.User{
+					"user": {
+						User:     "user",
+						Password: goodPassword,
+						Roles:    []string{"foorole"},
+					},
 				},
 				roles: map[string]*auth.Role{
 					"foorole": {
@@ -402,7 +481,7 @@ func TestPrefixAccess(t *testing.T) {
 			key: "/foo",
 			req: mustAuthRequest("GET", "user", "good"),
 			store: &mockAuthStore{
-				user:    &auth.User{},
+				users:   map[string]*auth.User{},
 				err:     errors.New("Not the user"),
 				enabled: true,
 			},
@@ -414,10 +493,12 @@ func TestPrefixAccess(t *testing.T) {
 			key: "/foo",
 			req: mustJSONRequest(t, "GET", "somepath", ""),
 			store: &mockAuthStore{
-				user: &auth.User{
-					User:     "user",
-					Password: goodPassword,
-					Roles:    []string{"foorole"},
+				users: map[string]*auth.User{
+					"user": {
+						User:     "user",
+						Password: goodPassword,
+						Roles:    []string{"foorole"},
+					},
 				},
 				roles: map[string]*auth.Role{
 					"guest": {
@@ -440,10 +521,12 @@ func TestPrefixAccess(t *testing.T) {
 			key: "/bar",
 			req: mustJSONRequest(t, "GET", "somepath", ""),
 			store: &mockAuthStore{
-				user: &auth.User{
-					User:     "user",
-					Password: goodPassword,
-					Roles:    []string{"foorole"},
+				users: map[string]*auth.User{
+					"user": {
+						User:     "user",
+						Password: goodPassword,
+						Roles:    []string{"foorole"},
+					},
 				},
 				roles: map[string]*auth.Role{
 					"guest": {
@@ -467,10 +550,12 @@ func TestPrefixAccess(t *testing.T) {
 			key: "/foo",
 			req: mustAuthRequest("GET", "user", "good"),
 			store: &mockAuthStore{
-				user: &auth.User{
-					User:     "user",
-					Password: goodPassword,
-					Roles:    []string{"role1", "role2"},
+				users: map[string]*auth.User{
+					"user": {
+						User:     "user",
+						Password: goodPassword,
+						Roles:    []string{"role1", "role2"},
+					},
 				},
 				roles: map[string]*auth.Role{
 					"role1": {
@@ -501,10 +586,12 @@ func TestPrefixAccess(t *testing.T) {
 			})(),
 			store: &mockAuthStore{
 				enabled: true,
-				user: &auth.User{
-					User:     "root",
-					Password: goodPassword,
-					Roles:    []string{"root"},
+				users: map[string]*auth.User{
+					"root": {
+						User:     "root",
+						Password: goodPassword,
+						Roles:    []string{"root"},
+					},
 				},
 				roles: map[string]*auth.Role{
 					"guest": {