From 6ae1f6c6e4c1793e0cde6876d0c7478e1c4a6480 Mon Sep 17 00:00:00 2001 From: Mohammad Samman Date: Sun, 30 Aug 2015 03:17:39 -0700 Subject: [PATCH] etcdserver: extend auth api allow recursive query on users and roles to get more detail Fixes #3278 --- Documentation/auth_api.md | 114 ++++++++++-- etcdserver/etcdhttp/client_auth.go | 90 ++++++++-- etcdserver/etcdhttp/client_auth_test.go | 223 ++++++++++++++++-------- 3 files changed, 330 insertions(+), 97 deletions(-) diff --git a/Documentation/auth_api.md b/Documentation/auth_api.md index 404ed0aed..b0892da9a 100644 --- a/Documentation/auth_api.md +++ b/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,22 +261,6 @@ A full role structure may look like this. A Permission List structure is used fo } ``` -**Get a list of Roles** - -GET/HEAD /v2/auth/roles - - Sent Headers: - Authorization: Basic - Possible Status Codes: - 200 OK - 401 Unauthorized - 200 Headers: - Content-type: application/json - 200 Body: - { - "roles": ["fleet", "etcd", "quay"] - } - **Get Role Details** GET/HEAD /v2/auth/roles/fleet @@ -252,6 +284,50 @@ GET/HEAD /v2/auth/roles/fleet } } +**Get a list of Roles** + +GET/HEAD /v2/auth/roles + + Sent Headers: + Authorization: Basic + Possible Status Codes: + 200 OK + 401 Unauthorized + 200 Headers: + Content-type: application/json + 200 Body: + { + "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** PUT /v2/auth/roles/rkt diff --git a/etcdserver/etcdhttp/client_auth.go b/etcdserver/etcdhttp/client_auth.go index f43d540e4..aa9def0d2 100644 --- a/etcdserver/etcdhttp/client_auth.go +++ b/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 diff --git a/etcdserver/etcdhttp/client_auth_test.go b/etcdserver/etcdhttp/client_auth_test.go index 1ee2e0fb1..97bea77e6 100644 --- a/etcdserver/etcdhttp/client_auth_test.go +++ b/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": {