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.
**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 <BasicAuthString>
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 <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**
PUT /v2/auth/roles/rkt

View File

@ -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

View File

@ -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": {