*: Add security/auth support to etcdctl and etcd/client

add godep for speakeasy and auth entry parsing
add security_user to client
add role to client
add role commands
add auth support to etcdclient and etcdctl(member/user)
add enable/disable to etcdctl
better error messages, read/write/readwrite
Bump go-etcd to include codec changes, add new dependency
verify the error for revoke/add if nothing changed, remove security-merging prefix
This commit is contained in:
Barak Michener
2015-04-03 17:32:30 -04:00
parent 97709b202d
commit a4d1a5a6e5
62 changed files with 45661 additions and 39 deletions

View File

@@ -76,6 +76,13 @@ type Config struct {
// If CheckRedirect is nil, the Client uses its default policy,
// which is to stop after 10 consecutive requests.
CheckRedirect CheckRedirectFunc
// Username specifies the user credential to add as an authorization header
Username string
// Password is the password for the specified user to add as an authorization header
// to the request.
Password string
}
func (cfg *Config) transport() CancelableTransport {
@@ -122,7 +129,15 @@ type Client interface {
}
func New(cfg Config) (Client, error) {
c := &httpClusterClient{clientFactory: newHTTPClientFactory(cfg.transport(), cfg.checkRedirect())}
c := &httpClusterClient{
clientFactory: newHTTPClientFactory(cfg.transport(), cfg.checkRedirect()),
}
if cfg.Username != "" {
c.credentials = &credentials{
username: cfg.Username,
password: cfg.Password,
}
}
if err := c.reset(cfg.Endpoints); err != nil {
return nil, err
}
@@ -145,6 +160,11 @@ func newHTTPClientFactory(tr CancelableTransport, cr CheckRedirectFunc) httpClie
}
}
type credentials struct {
username string
password string
}
type httpClientFactory func(url.URL) httpClient
type httpAction interface {
@@ -154,6 +174,7 @@ type httpAction interface {
type httpClusterClient struct {
clientFactory httpClientFactory
endpoints []url.URL
credentials *credentials
sync.RWMutex
}
@@ -177,10 +198,18 @@ func (c *httpClusterClient) reset(eps []string) error {
}
func (c *httpClusterClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
action := act
c.RLock()
leps := len(c.endpoints)
eps := make([]url.URL, leps)
n := copy(eps, c.endpoints)
if c.credentials != nil {
action = &authedAction{
act: act,
credentials: *c.credentials,
}
}
c.RUnlock()
if leps == 0 {
@@ -197,7 +226,7 @@ func (c *httpClusterClient) Do(ctx context.Context, act httpAction) (*http.Respo
for _, ep := range eps {
hc := c.clientFactory(ep)
resp, body, err = hc.Do(ctx, act)
resp, body, err = hc.Do(ctx, action)
if err != nil {
if err == context.DeadlineExceeded || err == context.Canceled {
return nil, nil, err
@@ -313,6 +342,17 @@ func (c *simpleHTTPClient) Do(ctx context.Context, act httpAction) (*http.Respon
return resp, body, err
}
type authedAction struct {
act httpAction
credentials credentials
}
func (a *authedAction) HTTPRequest(url url.URL) *http.Request {
r := a.act.HTTPRequest(url)
r.SetBasicAuth(a.credentials.username, a.credentials.password)
return r
}
type redirectFollowingHTTPClient struct {
client httpClient
checkRedirect CheckRedirectFunc

235
client/security_role.go Normal file
View File

@@ -0,0 +1,235 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
)
type Role struct {
Role string `json:"role"`
Permissions Permissions `json:"permissions"`
Grant *Permissions `json:"grant,omitempty"`
Revoke *Permissions `json:"revoke,omitempty"`
}
type Permissions struct {
KV rwPermission `json:"kv"`
}
type rwPermission struct {
Read []string `json:"read"`
Write []string `json:"write"`
}
type PermissionType int
const (
ReadPermission PermissionType = iota
WritePermission
ReadWritePermission
)
// NewSecurityRoleAPI constructs a new SecurityRoleAPI that uses HTTP to
// interact with etcd's role creation and modification features.
func NewSecurityRoleAPI(c Client) SecurityRoleAPI {
return &httpSecurityRoleAPI{
client: c,
}
}
type SecurityRoleAPI interface {
// Add a role.
AddRole(ctx context.Context, role string) error
// Remove a role.
RemoveRole(ctx context.Context, role string) error
// Get role details.
GetRole(ctx context.Context, role string) (*Role, error)
// Grant a role some permission prefixes for the KV store.
GrantRoleKV(ctx context.Context, role string, prefixes []string, permType PermissionType) (*Role, error)
// Revoke some some permission prefixes for a role on the KV store.
RevokeRoleKV(ctx context.Context, role string, prefixes []string, permType PermissionType) (*Role, error)
// List roles.
ListRoles(ctx context.Context) ([]string, error)
}
type httpSecurityRoleAPI struct {
client httpClient
}
type securityRoleAPIAction struct {
verb string
name string
role *Role
}
type securityRoleAPIList struct{}
func (list *securityRoleAPIList) HTTPRequest(ep url.URL) *http.Request {
u := v2SecurityURL(ep, "roles", "")
req, _ := http.NewRequest("GET", u.String(), nil)
req.Header.Set("Content-Type", "application/json")
return req
}
func (l *securityRoleAPIAction) HTTPRequest(ep url.URL) *http.Request {
u := v2SecurityURL(ep, "roles", l.name)
if l.role == nil {
req, _ := http.NewRequest(l.verb, u.String(), nil)
return req
}
b, err := json.Marshal(l.role)
if err != nil {
panic(err)
}
body := bytes.NewReader(b)
req, _ := http.NewRequest(l.verb, u.String(), body)
req.Header.Set("Content-Type", "application/json")
return req
}
func (r *httpSecurityRoleAPI) ListRoles(ctx context.Context) ([]string, error) {
resp, body, err := r.client.Do(ctx, &securityRoleAPIList{})
if err != nil {
return nil, err
}
if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
return nil, err
}
var userList struct {
Roles []string `json:"roles"`
}
err = json.Unmarshal(body, &userList)
if err != nil {
return nil, err
}
return userList.Roles, nil
}
func (r *httpSecurityRoleAPI) AddRole(ctx context.Context, rolename string) error {
role := &Role{
Role: rolename,
}
return r.addRemoveRole(ctx, &securityRoleAPIAction{
verb: "PUT",
name: rolename,
role: role,
})
}
func (r *httpSecurityRoleAPI) RemoveRole(ctx context.Context, rolename string) error {
return r.addRemoveRole(ctx, &securityRoleAPIAction{
verb: "DELETE",
name: rolename,
})
}
func (r *httpSecurityRoleAPI) addRemoveRole(ctx context.Context, req *securityRoleAPIAction) error {
resp, body, err := r.client.Do(ctx, req)
if err != nil {
return err
}
if err := assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
var sec securityError
err := json.Unmarshal(body, &sec)
if err != nil {
return err
}
return sec
}
return nil
}
func (r *httpSecurityRoleAPI) GetRole(ctx context.Context, rolename string) (*Role, error) {
return r.modRole(ctx, &securityRoleAPIAction{
verb: "GET",
name: rolename,
})
}
func buildRWPermission(prefixes []string, permType PermissionType) rwPermission {
var out rwPermission
switch permType {
case ReadPermission:
out.Read = prefixes
case WritePermission:
out.Write = prefixes
case ReadWritePermission:
out.Read = prefixes
out.Write = prefixes
}
return out
}
func (r *httpSecurityRoleAPI) GrantRoleKV(ctx context.Context, rolename string, prefixes []string, permType PermissionType) (*Role, error) {
rwp := buildRWPermission(prefixes, permType)
role := &Role{
Role: rolename,
Grant: &Permissions{
KV: rwp,
},
}
return r.modRole(ctx, &securityRoleAPIAction{
verb: "PUT",
name: rolename,
role: role,
})
}
func (r *httpSecurityRoleAPI) RevokeRoleKV(ctx context.Context, rolename string, prefixes []string, permType PermissionType) (*Role, error) {
rwp := buildRWPermission(prefixes, permType)
role := &Role{
Role: rolename,
Revoke: &Permissions{
KV: rwp,
},
}
return r.modRole(ctx, &securityRoleAPIAction{
verb: "PUT",
name: rolename,
role: role,
})
}
func (r *httpSecurityRoleAPI) modRole(ctx context.Context, req *securityRoleAPIAction) (*Role, error) {
resp, body, err := r.client.Do(ctx, req)
if err != nil {
return nil, err
}
if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
var sec securityError
err := json.Unmarshal(body, &sec)
if err != nil {
return nil, err
}
return nil, sec
}
var role Role
err = json.Unmarshal(body, &role)
if err != nil {
return nil, err
}
return &role, nil
}

297
client/security_user.go Normal file
View File

@@ -0,0 +1,297 @@
// Copyright 2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"path"
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
)
var (
defaultV2SecurityPrefix = "/v2/security"
)
type User struct {
User string `json:"user"`
Password string `json:"password,omitempty"`
Roles []string `json:"roles"`
Grant []string `json:"grant,omitempty"`
Revoke []string `json:"revoke,omitempty"`
}
func v2SecurityURL(ep url.URL, action string, name string) *url.URL {
if name != "" {
ep.Path = path.Join(ep.Path, defaultV2SecurityPrefix, action, name)
return &ep
}
ep.Path = path.Join(ep.Path, defaultV2SecurityPrefix, action)
return &ep
}
// NewSecurityAPI constructs a new SecurityAPI that uses HTTP to
// interact with etcd's general security features.
func NewSecurityAPI(c Client) SecurityAPI {
return &httpSecurityAPI{
client: c,
}
}
type SecurityAPI interface {
// Enable security.
Enable(ctx context.Context) error
// Disable security.
Disable(ctx context.Context) error
}
type httpSecurityAPI struct {
client httpClient
}
func (s *httpSecurityAPI) Enable(ctx context.Context) error {
return s.enableDisable(ctx, &securityAPIAction{"PUT"})
}
func (s *httpSecurityAPI) Disable(ctx context.Context) error {
return s.enableDisable(ctx, &securityAPIAction{"DELETE"})
}
func (s *httpSecurityAPI) enableDisable(ctx context.Context, req httpAction) error {
resp, body, err := s.client.Do(ctx, req)
if err != nil {
return err
}
if err := assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
var sec securityError
err := json.Unmarshal(body, &sec)
if err != nil {
return err
}
return sec
}
return nil
}
type securityAPIAction struct {
verb string
}
func (l *securityAPIAction) HTTPRequest(ep url.URL) *http.Request {
u := v2SecurityURL(ep, "enable", "")
req, _ := http.NewRequest(l.verb, u.String(), nil)
return req
}
type securityError struct {
Message string `json:"message"`
Code int `json:"-"`
}
func (e securityError) Error() string {
return e.Message
}
// NewSecurityUserAPI constructs a new SecurityUserAPI that uses HTTP to
// interact with etcd's user creation and modification features.
func NewSecurityUserAPI(c Client) SecurityUserAPI {
return &httpSecurityUserAPI{
client: c,
}
}
type SecurityUserAPI interface {
// Add a user.
AddUser(ctx context.Context, username string, password string) error
// Remove a user.
RemoveUser(ctx context.Context, username string) error
// Get user details.
GetUser(ctx context.Context, username string) (*User, error)
// Grant a user some permission roles.
GrantUser(ctx context.Context, username string, roles []string) (*User, error)
// Revoke some permission roles from a user.
RevokeUser(ctx context.Context, username string, roles []string) (*User, error)
// Change the user's password.
ChangePassword(ctx context.Context, username string, password string) (*User, error)
// List users.
ListUsers(ctx context.Context) ([]string, error)
}
type httpSecurityUserAPI struct {
client httpClient
}
type securityUserAPIAction struct {
verb string
username string
user *User
}
type securityUserAPIList struct{}
func (list *securityUserAPIList) HTTPRequest(ep url.URL) *http.Request {
u := v2SecurityURL(ep, "users", "")
req, _ := http.NewRequest("GET", u.String(), nil)
req.Header.Set("Content-Type", "application/json")
return req
}
func (l *securityUserAPIAction) HTTPRequest(ep url.URL) *http.Request {
u := v2SecurityURL(ep, "users", l.username)
if l.user == nil {
req, _ := http.NewRequest(l.verb, u.String(), nil)
return req
}
b, err := json.Marshal(l.user)
if err != nil {
panic(err)
}
body := bytes.NewReader(b)
req, _ := http.NewRequest(l.verb, u.String(), body)
req.Header.Set("Content-Type", "application/json")
return req
}
func (u *httpSecurityUserAPI) ListUsers(ctx context.Context) ([]string, error) {
resp, body, err := u.client.Do(ctx, &securityUserAPIList{})
if err != nil {
return nil, err
}
if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
var sec securityError
err := json.Unmarshal(body, &sec)
if err != nil {
return nil, err
}
return nil, sec
}
var userList struct {
Users []string `json:"users"`
}
err = json.Unmarshal(body, &userList)
if err != nil {
return nil, err
}
return userList.Users, nil
}
func (u *httpSecurityUserAPI) AddUser(ctx context.Context, username string, password string) error {
user := &User{
User: username,
Password: password,
}
return u.addRemoveUser(ctx, &securityUserAPIAction{
verb: "PUT",
username: username,
user: user,
})
}
func (u *httpSecurityUserAPI) RemoveUser(ctx context.Context, username string) error {
return u.addRemoveUser(ctx, &securityUserAPIAction{
verb: "DELETE",
username: username,
})
}
func (u *httpSecurityUserAPI) addRemoveUser(ctx context.Context, req *securityUserAPIAction) error {
resp, body, err := u.client.Do(ctx, req)
if err != nil {
return err
}
if err := assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
var sec securityError
err := json.Unmarshal(body, &sec)
if err != nil {
return err
}
return sec
}
return nil
}
func (u *httpSecurityUserAPI) GetUser(ctx context.Context, username string) (*User, error) {
return u.modUser(ctx, &securityUserAPIAction{
verb: "GET",
username: username,
})
}
func (u *httpSecurityUserAPI) GrantUser(ctx context.Context, username string, roles []string) (*User, error) {
user := &User{
User: username,
Grant: roles,
}
return u.modUser(ctx, &securityUserAPIAction{
verb: "PUT",
username: username,
user: user,
})
}
func (u *httpSecurityUserAPI) RevokeUser(ctx context.Context, username string, roles []string) (*User, error) {
user := &User{
User: username,
Revoke: roles,
}
return u.modUser(ctx, &securityUserAPIAction{
verb: "PUT",
username: username,
user: user,
})
}
func (u *httpSecurityUserAPI) ChangePassword(ctx context.Context, username string, password string) (*User, error) {
user := &User{
User: username,
Password: password,
}
return u.modUser(ctx, &securityUserAPIAction{
verb: "PUT",
username: username,
user: user,
})
}
func (u *httpSecurityUserAPI) modUser(ctx context.Context, req *securityUserAPIAction) (*User, error) {
resp, body, err := u.client.Do(ctx, req)
if err != nil {
return nil, err
}
if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
var sec securityError
err := json.Unmarshal(body, &sec)
if err != nil {
return nil, err
}
return nil, sec
}
var user User
err = json.Unmarshal(body, &user)
if err != nil {
return nil, err
}
return &user, nil
}