*: 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

@@ -20,6 +20,7 @@ import (
"os"
"strings"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/bgentry/speakeasy"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd"
)
@@ -36,6 +37,31 @@ func dumpCURL(client *etcd.Client) {
}
}
func getUsernamePasswordFromFlag(usernameFlag string) (username string, password string, err error) {
colon := strings.Index(usernameFlag, ":")
if colon == -1 {
username = usernameFlag
// Prompt for the password.
password, err = speakeasy.Ask("Password: ")
if err != nil {
return "", "", err
}
} else {
username = usernameFlag[:colon]
password = usernameFlag[colon+1:]
}
return username, password, nil
}
func prepAuth(client *etcd.Client, usernameFlag string) error {
username, password, err := getUsernamePasswordFromFlag(usernameFlag)
if err != nil {
return err
}
client.SetCredentials(username, password)
return nil
}
// rawhandle wraps the command function handlers and sets up the
// environment but performs no output formatting.
func rawhandle(c *cli.Context, fn handlerFunc) (*etcd.Response, error) {
@@ -52,6 +78,14 @@ func rawhandle(c *cli.Context, fn handlerFunc) (*etcd.Response, error) {
client := etcd.NewClient(endpoints)
client.SetTransport(tr)
username := c.GlobalString("username")
if username != "" {
err := prepAuth(client, username)
if err != nil {
return nil, err
}
}
if c.GlobalBool("debug") {
go dumpCURL(client)
}

View File

@@ -49,28 +49,8 @@ func NewMemberCommand() cli.Command {
}
func mustNewMembersAPI(c *cli.Context) client.MembersAPI {
eps, err := getEndpoints(c)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
tr, err := getTransport(c)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
cfg := client.Config{
Transport: tr,
Endpoints: eps,
}
hc, err := client.New(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
hc := mustNewClient(c)
if !c.GlobalBool("no-sync") {
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)

View File

@@ -0,0 +1,241 @@
// 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 command
import (
"fmt"
"os"
"reflect"
"strings"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
"github.com/coreos/etcd/client"
)
func NewRoleCommands() cli.Command {
return cli.Command{
Name: "role",
Usage: "role add, grant and revoke subcommands",
Subcommands: []cli.Command{
cli.Command{
Name: "add",
Usage: "add a new role for the etcd cluster",
Action: actionRoleAdd,
},
cli.Command{
Name: "get",
Usage: "get details for a role",
Action: actionRoleGet,
},
cli.Command{
Name: "list",
Usage: "list all roles",
Action: actionRoleList,
},
cli.Command{
Name: "remove",
Usage: "remove a role from the etcd cluster",
Action: actionRoleRemove,
},
cli.Command{
Name: "grant",
Usage: "grant path matches to an etcd role",
Flags: []cli.Flag{
cli.StringFlag{Name: "path", Value: "", Usage: "Path granted for the role to access"},
cli.BoolFlag{Name: "read", Usage: "Grant read-only access"},
cli.BoolFlag{Name: "write", Usage: "Grant write-only access"},
cli.BoolFlag{Name: "readwrite", Usage: "Grant read-write access"},
},
Action: actionRoleGrant,
},
cli.Command{
Name: "revoke",
Usage: "revoke path matches for an etcd role",
Flags: []cli.Flag{
cli.StringFlag{Name: "path", Value: "", Usage: "Path revoked for the role to access"},
cli.BoolFlag{Name: "read", Usage: "Revoke read access"},
cli.BoolFlag{Name: "write", Usage: "Revoke write access"},
cli.BoolFlag{Name: "readwrite", Usage: "Revoke read-write access"},
},
Action: actionRoleRevoke,
},
},
}
}
func mustNewSecurityRoleAPI(c *cli.Context) client.SecurityRoleAPI {
hc := mustNewClient(c)
if c.GlobalBool("debug") {
fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(hc.Endpoints(), ", "))
}
return client.NewSecurityRoleAPI(hc)
}
func actionRoleList(c *cli.Context) {
if len(c.Args()) != 0 {
fmt.Fprintln(os.Stderr, "No arguments accepted")
os.Exit(1)
}
r := mustNewSecurityRoleAPI(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
roles, err := r.ListRoles(ctx)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
for _, role := range roles {
fmt.Printf("%s\n", role)
}
}
func actionRoleAdd(c *cli.Context) {
api, role := mustRoleAPIAndName(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
currentRole, err := api.GetRole(ctx, role)
cancel()
if currentRole != nil {
fmt.Fprintf(os.Stderr, "Role %s already exists\n", role)
os.Exit(1)
}
ctx, cancel = context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
err = api.AddRole(ctx, role)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("Role %s created\n", role)
}
func actionRoleRemove(c *cli.Context) {
api, role := mustRoleAPIAndName(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
err := api.RemoveRole(ctx, role)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("Role %s removed\n", role)
}
func actionRoleGrant(c *cli.Context) {
roleGrantRevoke(c, true)
}
func actionRoleRevoke(c *cli.Context) {
roleGrantRevoke(c, false)
}
func roleGrantRevoke(c *cli.Context, grant bool) {
path := c.String("path")
if path == "" {
fmt.Fprintln(os.Stderr, "No path specified; please use `-path`")
os.Exit(1)
}
read := c.Bool("read")
write := c.Bool("write")
rw := c.Bool("readwrite")
permcount := 0
for _, v := range []bool{read, write, rw} {
if v {
permcount++
}
}
if permcount != 1 {
fmt.Fprintln(os.Stderr, "Please specify exactly one of -read, -write or -readwrite")
os.Exit(1)
}
var permType client.PermissionType
switch {
case read:
permType = client.ReadPermission
case write:
permType = client.WritePermission
case rw:
permType = client.ReadWritePermission
}
api, role := mustRoleAPIAndName(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
currentRole, err := api.GetRole(ctx, role)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
ctx, cancel = context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
var newRole *client.Role
if grant {
newRole, err = api.GrantRoleKV(ctx, role, []string{path}, permType)
} else {
newRole, err = api.RevokeRoleKV(ctx, role, []string{path}, permType)
}
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if reflect.DeepEqual(newRole, currentRole) {
if grant {
fmt.Printf("Role unchanged; already granted")
} else {
fmt.Printf("Role unchanged; already revoked")
}
}
fmt.Printf("Role %s updated\n", role)
}
func actionRoleGet(c *cli.Context) {
api, rolename := mustRoleAPIAndName(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
role, err := api.GetRole(ctx, rolename)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("Role: %s\n", role.Role)
fmt.Printf("KV Read:\n")
for _, v := range role.Permissions.KV.Read {
fmt.Printf("\t%s\n", v)
}
fmt.Printf("KV Write:\n")
for _, v := range role.Permissions.KV.Write {
fmt.Printf("\t%s\n", v)
}
}
func mustRoleAPIAndName(c *cli.Context) (client.SecurityRoleAPI, string) {
args := c.Args()
if len(args) != 1 {
fmt.Fprintln(os.Stderr, "Please provide a role name")
os.Exit(1)
}
name := args[0]
api := mustNewSecurityRoleAPI(c)
return api, name
}

View File

@@ -0,0 +1,87 @@
// 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 command
import (
"fmt"
"os"
"strings"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
"github.com/coreos/etcd/client"
)
func NewSecurityCommands() cli.Command {
return cli.Command{
Name: "security",
Usage: "overall security controls",
Subcommands: []cli.Command{
cli.Command{
Name: "enable",
Usage: "enable security access controls",
Action: actionSecurityEnable,
},
cli.Command{
Name: "disable",
Usage: "disable security access controls",
Action: actionSecurityDisable,
},
},
}
}
func actionSecurityEnable(c *cli.Context) {
securityEnableDisable(c, true)
}
func actionSecurityDisable(c *cli.Context) {
securityEnableDisable(c, false)
}
func mustNewSecurityAPI(c *cli.Context) client.SecurityAPI {
hc := mustNewClient(c)
if c.GlobalBool("debug") {
fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(hc.Endpoints(), ", "))
}
return client.NewSecurityAPI(hc)
}
func securityEnableDisable(c *cli.Context, enable bool) {
if len(c.Args()) != 0 {
fmt.Fprintln(os.Stderr, "No arguments accepted")
os.Exit(1)
}
s := mustNewSecurityAPI(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
var err error
if enable {
err = s.Enable(ctx)
} else {
err = s.Disable(ctx)
}
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if enable {
fmt.Println("Security Enabled")
} else {
fmt.Println("Security Disabled")
}
}

View File

@@ -0,0 +1,242 @@
// 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 command
import (
"fmt"
"os"
"reflect"
"sort"
"strings"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/bgentry/speakeasy"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
"github.com/coreos/etcd/client"
)
func NewUserCommands() cli.Command {
return cli.Command{
Name: "user",
Usage: "user add, grant and revoke subcommands",
Subcommands: []cli.Command{
cli.Command{
Name: "add",
Usage: "add a new user for the etcd cluster",
Action: actionUserAdd,
},
cli.Command{
Name: "get",
Usage: "get details for a user",
Action: actionUserGet,
},
cli.Command{
Name: "list",
Usage: "list all current users",
Action: actionUserList,
},
cli.Command{
Name: "remove",
Usage: "remove a user for the etcd cluster",
Action: actionUserRemove,
},
cli.Command{
Name: "grant",
Usage: "grant roles to an etcd user",
Flags: []cli.Flag{cli.StringSliceFlag{Name: "roles", Value: new(cli.StringSlice), Usage: "List of roles to grant or revoke"}},
Action: actionUserGrant,
},
cli.Command{
Name: "revoke",
Usage: "revoke roles for an etcd user",
Flags: []cli.Flag{cli.StringSliceFlag{Name: "roles", Value: new(cli.StringSlice), Usage: "List of roles to grant or revoke"}},
Action: actionUserRevoke,
},
cli.Command{
Name: "passwd",
Usage: "change password for a user",
Action: actionUserPasswd,
},
},
}
}
func mustNewSecurityUserAPI(c *cli.Context) client.SecurityUserAPI {
hc := mustNewClient(c)
if c.GlobalBool("debug") {
fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(hc.Endpoints(), ", "))
}
return client.NewSecurityUserAPI(hc)
}
func actionUserList(c *cli.Context) {
if len(c.Args()) != 0 {
fmt.Fprintln(os.Stderr, "No arguments accepted")
os.Exit(1)
}
u := mustNewSecurityUserAPI(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
users, err := u.ListUsers(ctx)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
for _, user := range users {
fmt.Printf("%s\n", user)
}
}
func actionUserAdd(c *cli.Context) {
api, user := mustUserAPIAndName(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
currentUser, err := api.GetUser(ctx, user)
cancel()
if currentUser != nil {
fmt.Fprintf(os.Stderr, "User %s already exists\n", user)
os.Exit(1)
}
pass, err := speakeasy.Ask("New password: ")
if err != nil {
fmt.Fprintln(os.Stderr, "Error reading password:", err)
os.Exit(1)
}
ctx, cancel = context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
err = api.AddUser(ctx, user, pass)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("User %s created\n", user)
}
func actionUserRemove(c *cli.Context) {
api, user := mustUserAPIAndName(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
err := api.RemoveUser(ctx, user)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("User %s removed\n", user)
}
func actionUserPasswd(c *cli.Context) {
api, user := mustUserAPIAndName(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
currentUser, err := api.GetUser(ctx, user)
cancel()
if currentUser == nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
pass, err := speakeasy.Ask("New password: ")
if err != nil {
fmt.Fprintln(os.Stderr, "Error reading password:", err)
os.Exit(1)
}
ctx, cancel = context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
_, err = api.ChangePassword(ctx, user, pass)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("Password updated\n")
}
func actionUserGrant(c *cli.Context) {
userGrantRevoke(c, true)
}
func actionUserRevoke(c *cli.Context) {
userGrantRevoke(c, false)
}
func userGrantRevoke(c *cli.Context, grant bool) {
roles := c.StringSlice("roles")
if len(roles) == 0 {
fmt.Fprintln(os.Stderr, "No roles specified; please use `-roles`")
os.Exit(1)
}
api, user := mustUserAPIAndName(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
currentUser, err := api.GetUser(ctx, user)
cancel()
if currentUser == nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
ctx, cancel = context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
var newUser *client.User
if grant {
newUser, err = api.GrantUser(ctx, user, roles)
} else {
newUser, err = api.RevokeUser(ctx, user, roles)
}
cancel()
sort.Strings(newUser.Roles)
sort.Strings(currentUser.Roles)
if reflect.DeepEqual(newUser.Roles, currentUser.Roles) {
if grant {
fmt.Printf("User unchanged; roles already granted")
} else {
fmt.Printf("User unchanged; roles already revoked")
}
}
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("User %s updated\n", user)
}
func actionUserGet(c *cli.Context) {
api, username := mustUserAPIAndName(c)
ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
user, err := api.GetUser(ctx, username)
cancel()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("User: %s\n", user.User)
fmt.Printf("Roles: %s\n", strings.Join(user.Roles, " "))
}
func mustUserAPIAndName(c *cli.Context) (client.SecurityUserAPI, string) {
args := c.Args()
if len(args) != 1 {
fmt.Fprintln(os.Stderr, "Please provide a username")
os.Exit(1)
}
api := mustNewSecurityUserAPI(c)
username := args[0]
return api, username
}

View File

@@ -16,6 +16,7 @@ package command
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
@@ -24,6 +25,7 @@ import (
"strings"
"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/coreos/etcd/client"
"github.com/coreos/etcd/pkg/transport"
)
@@ -112,3 +114,40 @@ func getTransport(c *cli.Context) (*http.Transport, error) {
}
return transport.NewTransport(tls)
}
func mustNewClient(c *cli.Context) client.Client {
eps, err := getEndpoints(c)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
tr, err := getTransport(c)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
cfg := client.Config{
Transport: tr,
Endpoints: eps,
}
uFlag := c.GlobalString("username")
if uFlag != "" {
username, password, err := getUsernamePasswordFromFlag(uFlag)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
cfg.Username = username
cfg.Password = password
}
hc, err := client.New(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
return hc
}

View File

@@ -35,6 +35,7 @@ func main() {
cli.StringFlag{Name: "cert-file", Value: "", Usage: "identify HTTPS client using this SSL certificate file"},
cli.StringFlag{Name: "key-file", Value: "", Usage: "identify HTTPS client using this SSL key file"},
cli.StringFlag{Name: "ca-file", Value: "", Usage: "verify certificates of HTTPS-enabled servers using this CA bundle"},
cli.StringFlag{Name: "username, u", Value: "", Usage: "provide username[:password] and prompt if password is not supplied."},
}
app.Commands = []cli.Command{
command.NewBackupCommand(),
@@ -53,6 +54,9 @@ func main() {
command.NewExecWatchCommand(),
command.NewMemberCommand(),
command.NewImportSnapCommand(),
command.NewUserCommands(),
command.NewRoleCommands(),
command.NewSecurityCommands(),
}
app.Run(os.Args)