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

etcdserver: extend auth api
This commit is contained in:
Xiang Li 2015-09-21 11:51:19 -07:00
commit ea3dbfed60
3 changed files with 330 additions and 97 deletions

View File

@ -124,7 +124,7 @@ The User JSON object is formed as follows:
Password is only passed when necessary. Password is only passed when necessary.
**Get a list of users** **Get a List of Users**
GET/HEAD /v2/auth/users GET/HEAD /v2/auth/users
@ -137,7 +137,36 @@ GET/HEAD /v2/auth/users
Content-type: application/json Content-type: application/json
200 Body: 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** **Get User Details**
@ -155,7 +184,26 @@ GET/HEAD /v2/auth/users/alice
200 Body: 200 Body:
{ {
"user" : "alice", "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** **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 <BasicAuthString>
Possible Status Codes:
200 OK
401 Unauthorized
200 Headers:
Content-type: application/json
200 Body:
{
"roles": ["fleet", "etcd", "quay"]
}
**Get Role Details** **Get Role Details**
GET/HEAD /v2/auth/roles/fleet 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 <BasicAuthString>
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** **Create Or Update A Role**
PUT /v2/auth/roles/rkt PUT /v2/auth/roles/rkt

View File

@ -147,11 +147,10 @@ func (sh *authHandler) baseRoles(w http.ResponseWriter, r *http.Request) {
writeNoAuth(w) writeNoAuth(w)
return return
} }
w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String()) w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
var rolesCollections struct {
Roles []string `json:"roles"`
}
roles, err := sh.sec.AllRoles() roles, err := sh.sec.AllRoles()
if err != nil { if err != nil {
writeError(w, err) writeError(w, err)
@ -161,10 +160,30 @@ func (sh *authHandler) baseRoles(w http.ResponseWriter, r *http.Request) {
roles = make([]string, 0) 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) err = json.NewEncoder(w).Encode(rolesCollections)
if err != nil { if err != nil {
plog.Warningf("baseRoles error encoding on %s", r.URL) 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) { func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") { if !allowMethod(w, r.Method, "GET") {
return 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("X-Etcd-Cluster-ID", sh.cluster.ID().String())
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
var usersCollections struct {
Users []string `json:"users"`
}
users, err := sh.sec.AllUsers() users, err := sh.sec.AllUsers()
if err != nil { if err != nil {
writeError(w, err) writeError(w, err)
@ -281,10 +303,42 @@ func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
users = make([]string, 0) 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) err = json.NewEncoder(w).Encode(usersCollections)
if err != nil { if err != nil {
plog.Warningf("baseUsers error encoding on %s", r.URL) 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) writeError(w, err)
return 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 { if err != nil {
plog.Warningf("forUser error encoding on %s", r.URL) plog.Warningf("forUser error encoding on %s", r.URL)
return return

View File

@ -37,16 +37,22 @@ func mustJSONRequest(t *testing.T, method string, p string, body string) *http.R
} }
type mockAuthStore struct { type mockAuthStore struct {
user *auth.User users map[string]*auth.User
roles map[string]*auth.Role roles map[string]*auth.Role
err error err error
enabled bool enabled bool
} }
func (s *mockAuthStore) AllUsers() ([]string, error) { return []string{"alice", "bob", "root"}, s.err } 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) 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) { 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) u, err := s.CreateUser(user)
return u, true, err 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) CreateUser(user auth.User) (auth.User, error) { return user, s.err }
func (s *mockAuthStore) DeleteUser(name string) error { return 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) { func (s *mockAuthStore) AllRoles() ([]string, error) {
return []string{"awesome", "guest", "root"}, s.err return []string{"awesome", "guest", "root"}, s.err
} }
@ -96,21 +104,63 @@ func TestAuthFlow(t *testing.T) {
// Users // Users
{ {
req: mustJSONRequest(t, "GET", "users", ""), req: mustJSONRequest(t, "GET", "users", ""),
store: mockAuthStore{},
wcode: http.StatusOK,
wbody: `{"users":["alice","bob","root"]}`,
},
{
req: mustJSONRequest(t, "GET", "users/alice", ""),
store: mockAuthStore{ store: mockAuthStore{
user: &auth.User{ users: map[string]*auth.User{
"alice": {
User: "alice", User: "alice",
Roles: []string{"alicerole", "guest"}, Roles: []string{"alicerole", "guest"},
Password: "wheeee", 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, wcode: http.StatusOK,
wbody: `{"user":"alice","roles":["alicerole","guest"]}`, 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{
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":[{"role":"alicerole","permissions":{"kv":{"read":null,"write":null}}}]}`,
}, },
{ {
req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`), req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`),
@ -127,37 +177,40 @@ func TestAuthFlow(t *testing.T) {
{ {
req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`), req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`),
store: mockAuthStore{ store: mockAuthStore{
user: &auth.User{ users: map[string]*auth.User{
"alice": {
User: "alice", User: "alice",
Roles: []string{"alicerole", "guest"}, Roles: []string{"alicerole", "guest"},
Password: "wheeee", Password: "wheeee",
}, },
}, },
},
wcode: http.StatusOK, wcode: http.StatusOK,
wbody: `{"user":"alice","roles":["alicerole","guest"]}`, wbody: `{"user":"alice","roles":["alicerole","guest"]}`,
}, },
{ {
req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "grant": ["alicerole"]}`), req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "grant": ["alicerole"]}`),
store: mockAuthStore{ store: mockAuthStore{
user: &auth.User{ users: map[string]*auth.User{
"alice": {
User: "alice", User: "alice",
Roles: []string{"alicerole", "guest"}, Roles: []string{"alicerole", "guest"},
Password: "wheeee", Password: "wheeee",
}, },
}, },
},
wcode: http.StatusOK, wcode: http.StatusOK,
wbody: `{"user":"alice","roles":["alicerole","guest"]}`, wbody: `{"user":"alice","roles":["alicerole","guest"]}`,
}, },
{ {
req: mustJSONRequest(t, "GET", "users/alice", ``), req: mustJSONRequest(t, "GET", "users/alice", ``),
store: mockAuthStore{ store: mockAuthStore{
user: &auth.User{}, users: map[string]*auth.User{},
err: auth.Error{Status: http.StatusNotFound, Errmsg: "auth: User alice doesn't exist."}, err: auth.Error{Status: http.StatusNotFound, Errmsg: "auth: User alice doesn't exist."},
}, },
wcode: http.StatusNotFound, wcode: http.StatusNotFound,
wbody: `{"message":"auth: User alice doesn't exist."}`, wbody: `{"message":"auth: User alice doesn't exist."}`,
}, },
// Roles
{ {
req: mustJSONRequest(t, "GET", "roles/manager", ""), req: mustJSONRequest(t, "GET", "roles/manager", ""),
store: mockAuthStore{ store: mockAuthStore{
@ -196,9 +249,23 @@ func TestAuthFlow(t *testing.T) {
}, },
{ {
req: mustJSONRequest(t, "GET", "roles", ""), req: mustJSONRequest(t, "GET", "roles", ""),
store: mockAuthStore{}, store: mockAuthStore{
roles: map[string]*auth.Role{
"awesome": {
Role: "awesome",
},
"guest": {
Role: "guest",
},
"root": {
Role: "root",
},
},
},
wcode: http.StatusOK, 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", ""), req: mustJSONRequest(t, "GET", "enable", ""),
@ -224,11 +291,13 @@ func TestAuthFlow(t *testing.T) {
})(), })(),
store: mockAuthStore{ store: mockAuthStore{
enabled: true, enabled: true,
user: &auth.User{ users: map[string]*auth.User{
"root": {
User: "root", User: "root",
Password: goodPassword, Password: goodPassword,
Roles: []string{"root"}, Roles: []string{"root"},
}, },
},
roles: map[string]*auth.Role{ roles: map[string]*auth.Role{
"root": { "root": {
Role: "root", Role: "root",
@ -246,11 +315,13 @@ func TestAuthFlow(t *testing.T) {
})(), })(),
store: mockAuthStore{ store: mockAuthStore{
enabled: true, enabled: true,
user: &auth.User{ users: map[string]*auth.User{
"root": {
User: "root", User: "root",
Password: goodPassword, Password: goodPassword,
Roles: []string{"root"}, Roles: []string{"root"},
}, },
},
roles: map[string]*auth.Role{ roles: map[string]*auth.Role{
"root": { "root": {
Role: "guest", Role: "guest",
@ -304,11 +375,13 @@ func TestPrefixAccess(t *testing.T) {
key: "/foo", key: "/foo",
req: mustAuthRequest("GET", "root", "good"), req: mustAuthRequest("GET", "root", "good"),
store: &mockAuthStore{ store: &mockAuthStore{
user: &auth.User{ users: map[string]*auth.User{
"root": {
User: "root", User: "root",
Password: goodPassword, Password: goodPassword,
Roles: []string{"root"}, Roles: []string{"root"},
}, },
},
roles: map[string]*auth.Role{ roles: map[string]*auth.Role{
"root": { "root": {
Role: "root", Role: "root",
@ -324,11 +397,13 @@ func TestPrefixAccess(t *testing.T) {
key: "/foo", key: "/foo",
req: mustAuthRequest("GET", "user", "good"), req: mustAuthRequest("GET", "user", "good"),
store: &mockAuthStore{ store: &mockAuthStore{
user: &auth.User{ users: map[string]*auth.User{
"user": {
User: "user", User: "user",
Password: goodPassword, Password: goodPassword,
Roles: []string{"foorole"}, Roles: []string{"foorole"},
}, },
},
roles: map[string]*auth.Role{ roles: map[string]*auth.Role{
"foorole": { "foorole": {
Role: "foorole", Role: "foorole",
@ -350,11 +425,13 @@ func TestPrefixAccess(t *testing.T) {
key: "/foo", key: "/foo",
req: mustAuthRequest("GET", "user", "good"), req: mustAuthRequest("GET", "user", "good"),
store: &mockAuthStore{ store: &mockAuthStore{
user: &auth.User{ users: map[string]*auth.User{
"user": {
User: "user", User: "user",
Password: goodPassword, Password: goodPassword,
Roles: []string{"foorole"}, Roles: []string{"foorole"},
}, },
},
roles: map[string]*auth.Role{ roles: map[string]*auth.Role{
"foorole": { "foorole": {
Role: "foorole", Role: "foorole",
@ -376,11 +453,13 @@ func TestPrefixAccess(t *testing.T) {
key: "/foo", key: "/foo",
req: mustAuthRequest("GET", "user", "bad"), req: mustAuthRequest("GET", "user", "bad"),
store: &mockAuthStore{ store: &mockAuthStore{
user: &auth.User{ users: map[string]*auth.User{
"user": {
User: "user", User: "user",
Password: goodPassword, Password: goodPassword,
Roles: []string{"foorole"}, Roles: []string{"foorole"},
}, },
},
roles: map[string]*auth.Role{ roles: map[string]*auth.Role{
"foorole": { "foorole": {
Role: "foorole", Role: "foorole",
@ -402,7 +481,7 @@ func TestPrefixAccess(t *testing.T) {
key: "/foo", key: "/foo",
req: mustAuthRequest("GET", "user", "good"), req: mustAuthRequest("GET", "user", "good"),
store: &mockAuthStore{ store: &mockAuthStore{
user: &auth.User{}, users: map[string]*auth.User{},
err: errors.New("Not the user"), err: errors.New("Not the user"),
enabled: true, enabled: true,
}, },
@ -414,11 +493,13 @@ func TestPrefixAccess(t *testing.T) {
key: "/foo", key: "/foo",
req: mustJSONRequest(t, "GET", "somepath", ""), req: mustJSONRequest(t, "GET", "somepath", ""),
store: &mockAuthStore{ store: &mockAuthStore{
user: &auth.User{ users: map[string]*auth.User{
"user": {
User: "user", User: "user",
Password: goodPassword, Password: goodPassword,
Roles: []string{"foorole"}, Roles: []string{"foorole"},
}, },
},
roles: map[string]*auth.Role{ roles: map[string]*auth.Role{
"guest": { "guest": {
Role: "guest", Role: "guest",
@ -440,11 +521,13 @@ func TestPrefixAccess(t *testing.T) {
key: "/bar", key: "/bar",
req: mustJSONRequest(t, "GET", "somepath", ""), req: mustJSONRequest(t, "GET", "somepath", ""),
store: &mockAuthStore{ store: &mockAuthStore{
user: &auth.User{ users: map[string]*auth.User{
"user": {
User: "user", User: "user",
Password: goodPassword, Password: goodPassword,
Roles: []string{"foorole"}, Roles: []string{"foorole"},
}, },
},
roles: map[string]*auth.Role{ roles: map[string]*auth.Role{
"guest": { "guest": {
Role: "guest", Role: "guest",
@ -467,11 +550,13 @@ func TestPrefixAccess(t *testing.T) {
key: "/foo", key: "/foo",
req: mustAuthRequest("GET", "user", "good"), req: mustAuthRequest("GET", "user", "good"),
store: &mockAuthStore{ store: &mockAuthStore{
user: &auth.User{ users: map[string]*auth.User{
"user": {
User: "user", User: "user",
Password: goodPassword, Password: goodPassword,
Roles: []string{"role1", "role2"}, Roles: []string{"role1", "role2"},
}, },
},
roles: map[string]*auth.Role{ roles: map[string]*auth.Role{
"role1": { "role1": {
Role: "role1", Role: "role1",
@ -501,11 +586,13 @@ func TestPrefixAccess(t *testing.T) {
})(), })(),
store: &mockAuthStore{ store: &mockAuthStore{
enabled: true, enabled: true,
user: &auth.User{ users: map[string]*auth.User{
"root": {
User: "root", User: "root",
Password: goodPassword, Password: goodPassword,
Roles: []string{"root"}, Roles: []string{"root"},
}, },
},
roles: map[string]*auth.Role{ roles: map[string]*auth.Role{
"guest": { "guest": {
Role: "guest", Role: "guest",