2017-10-31 17:17:06 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2017-11-04 06:31:46 +08:00
|
|
|
"bufio"
|
2017-10-31 23:22:40 +08:00
|
|
|
"encoding/json"
|
2017-11-14 06:57:52 +08:00
|
|
|
"errors"
|
2017-10-31 17:17:06 +08:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2017-11-02 00:00:34 +08:00
|
|
|
"os"
|
|
|
|
"runtime"
|
2017-11-02 06:32:36 +08:00
|
|
|
"strings"
|
2017-11-02 00:11:50 +08:00
|
|
|
"time"
|
2017-10-31 17:17:06 +08:00
|
|
|
|
|
|
|
shlex "github.com/anmitsu/go-shlex"
|
2017-11-16 16:36:43 +08:00
|
|
|
"github.com/asaskevich/govalidator"
|
2017-10-31 17:17:06 +08:00
|
|
|
"github.com/gliderlabs/ssh"
|
2017-10-31 23:22:40 +08:00
|
|
|
"github.com/jinzhu/gorm"
|
2017-11-01 22:48:30 +08:00
|
|
|
"github.com/moby/moby/pkg/namesgenerator"
|
2017-10-31 23:22:40 +08:00
|
|
|
"github.com/olekukonko/tablewriter"
|
2017-10-31 17:17:06 +08:00
|
|
|
"github.com/urfave/cli"
|
2017-11-01 22:48:30 +08:00
|
|
|
"golang.org/x/crypto/ssh/terminal"
|
2017-10-31 17:17:06 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
var banner = `
|
|
|
|
|
|
|
|
__________ _____ __ __
|
|
|
|
/ __/ __/ // / _ \___ ____/ /____ _/ /
|
|
|
|
_\ \_\ \/ _ / ___/ _ \/ __/ __/ _ '/ /
|
|
|
|
/___/___/_//_/_/ \___/_/ \__/\_,_/_/
|
|
|
|
|
|
|
|
|
|
|
|
`
|
2017-11-02 00:11:50 +08:00
|
|
|
var startTime = time.Now()
|
2017-10-31 17:17:06 +08:00
|
|
|
|
2017-11-02 00:00:34 +08:00
|
|
|
func shell(globalContext *cli.Context, s ssh.Session, sshCommand []string, db *gorm.DB) error {
|
2017-10-31 17:17:06 +08:00
|
|
|
if len(sshCommand) == 0 {
|
|
|
|
io.WriteString(s, banner)
|
|
|
|
}
|
|
|
|
|
|
|
|
cli.AppHelpTemplate = `COMMANDS:
|
|
|
|
{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{if .VisibleFlags}}
|
|
|
|
GLOBAL OPTIONS:
|
|
|
|
{{range .VisibleFlags}}{{.}}
|
|
|
|
{{end}}{{end}}
|
|
|
|
`
|
|
|
|
cli.OsExiter = func(c int) {
|
2017-11-14 03:08:12 +08:00
|
|
|
return
|
2017-10-31 17:17:06 +08:00
|
|
|
}
|
2017-11-03 07:23:57 +08:00
|
|
|
cli.HelpFlag = cli.BoolFlag{
|
|
|
|
Name: "help, h",
|
|
|
|
Hidden: true,
|
|
|
|
}
|
2017-10-31 17:17:06 +08:00
|
|
|
app := cli.NewApp()
|
|
|
|
app.Writer = s
|
|
|
|
app.HideVersion = true
|
|
|
|
app.Commands = []cli.Command{
|
|
|
|
{
|
2017-11-11 07:25:27 +08:00
|
|
|
Name: "acl",
|
|
|
|
Usage: "Manages acls",
|
|
|
|
Subcommands: []cli.Command{
|
|
|
|
{
|
|
|
|
Name: "create",
|
|
|
|
Usage: "Creates a new ACL",
|
|
|
|
Description: "$> acl create -",
|
|
|
|
Flags: []cli.Flag{
|
2017-11-23 17:35:51 +08:00
|
|
|
cli.StringSliceFlag{Name: "hostgroup, hg", Usage: "Assigns `HOSTGROUPS` to the acl"},
|
|
|
|
cli.StringSliceFlag{Name: "usergroup, ug", Usage: "Assigns `HOSTGROUPS` to the acl"},
|
2017-11-11 07:25:27 +08:00
|
|
|
cli.StringFlag{Name: "pattern", Usage: "Assigns a host pattern to the acl"},
|
2017-11-23 17:35:51 +08:00
|
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
2017-11-11 07:25:27 +08:00
|
|
|
cli.StringFlag{Name: "action", Usage: "Assigns the ACL action (allow,deny)", Value: "allow"},
|
|
|
|
cli.UintFlag{Name: "weight, w", Usage: "Assigns the ACL weight (priority)"},
|
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
acl := ACL{
|
|
|
|
Comment: c.String("comment"),
|
|
|
|
HostPattern: c.String("pattern"),
|
2017-11-15 16:52:59 +08:00
|
|
|
UserGroups: []*UserGroup{},
|
|
|
|
HostGroups: []*HostGroup{},
|
2017-11-11 07:25:27 +08:00
|
|
|
Weight: c.Uint("weight"),
|
|
|
|
Action: c.String("action"),
|
|
|
|
}
|
|
|
|
if acl.Action != "allow" && acl.Action != "deny" {
|
|
|
|
return fmt.Errorf("invalid action %q, allowed values: allow, deny", acl.Action)
|
|
|
|
}
|
2017-11-16 16:36:43 +08:00
|
|
|
if _, err := govalidator.ValidateStruct(acl); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-11-11 07:25:27 +08:00
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
var userGroups []*UserGroup
|
|
|
|
if err := UserGroupsPreload(UserGroupsByIdentifiers(db, c.StringSlice("usergroup"))).Find(&userGroups).Error; err != nil {
|
|
|
|
return err
|
2017-11-11 07:25:27 +08:00
|
|
|
}
|
2017-11-23 16:58:32 +08:00
|
|
|
acl.UserGroups = append(acl.UserGroups, userGroups...)
|
|
|
|
var hostGroups []*HostGroup
|
|
|
|
if err := HostGroupsPreload(HostGroupsByIdentifiers(db, c.StringSlice("hostgroup"))).Find(&hostGroups).Error; err != nil {
|
|
|
|
return err
|
2017-11-11 07:25:27 +08:00
|
|
|
}
|
2017-11-23 16:58:32 +08:00
|
|
|
acl.HostGroups = append(acl.HostGroups, hostGroups...)
|
2017-11-11 07:25:27 +08:00
|
|
|
|
|
|
|
if len(acl.UserGroups) == 0 {
|
|
|
|
return fmt.Errorf("an ACL must have at least one user group")
|
|
|
|
}
|
|
|
|
if len(acl.HostGroups) == 0 && acl.HostPattern == "" {
|
|
|
|
return fmt.Errorf("an ACL must have at least one host group or host pattern")
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := db.Create(&acl).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Fprintf(s, "%d\n", acl.ID)
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "inspect",
|
|
|
|
Usage: "Shows detailed information on one or more acls",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "ACL...",
|
2017-11-11 07:25:27 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
var acls []ACL
|
|
|
|
if err := ACLsPreload(ACLsByIdentifiers(db, c.Args())).Find(&acls).Error; err != nil {
|
|
|
|
return err
|
2017-11-11 07:25:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
enc := json.NewEncoder(s)
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
return enc.Encode(acls)
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "ls",
|
|
|
|
Usage: "Lists acls",
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
var acls []ACL
|
|
|
|
if err := db.Preload("UserGroups").Preload("HostGroups").Find(&acls).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
|
|
table.SetHeader([]string{"ID", "User groups", "Host groups", "Host pattern", "Action", "Comment"})
|
|
|
|
table.SetBorder(false)
|
|
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d acls.", len(acls)))
|
|
|
|
for _, acl := range acls {
|
|
|
|
userGroups := []string{}
|
|
|
|
hostGroups := []string{}
|
|
|
|
for _, entity := range acl.UserGroups {
|
|
|
|
userGroups = append(userGroups, entity.Name)
|
|
|
|
}
|
|
|
|
for _, entity := range acl.HostGroups {
|
|
|
|
hostGroups = append(hostGroups, entity.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
table.Append([]string{
|
|
|
|
fmt.Sprintf("%d", acl.ID),
|
|
|
|
strings.Join(userGroups, ", "),
|
|
|
|
strings.Join(hostGroups, ", "),
|
|
|
|
acl.HostPattern,
|
|
|
|
acl.Action,
|
|
|
|
acl.Comment,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
table.Render()
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "rm",
|
|
|
|
Usage: "Removes one or more acls",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "ACL...",
|
2017-11-11 07:25:27 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
return ACLsByIdentifiers(db, c.Args()).Delete(&ACL{}).Error
|
2017-11-11 07:25:27 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2017-11-14 03:22:45 +08:00
|
|
|
}, {
|
|
|
|
Name: "config",
|
|
|
|
Usage: "Manages global configuration",
|
|
|
|
Subcommands: []cli.Command{
|
|
|
|
{
|
|
|
|
Name: "backup",
|
|
|
|
Usage: "Dumps a backup",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
cli.BoolFlag{Name: "indent", Usage: "uses indented JSON"},
|
|
|
|
},
|
2017-11-14 06:57:52 +08:00
|
|
|
Description: "ssh admin@portal config backup > sshportal.bkp",
|
2017-11-14 03:22:45 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
config := Config{}
|
|
|
|
if err := db.Find(&config.Hosts).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := db.Find(&config.SSHKeys).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := db.Find(&config.Hosts).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := db.Find(&config.UserKeys).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := db.Find(&config.Users).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := db.Find(&config.UserGroups).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := db.Find(&config.HostGroups).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := db.Find(&config.ACLs).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-11-19 07:53:32 +08:00
|
|
|
if err := db.Find(&config.Settings).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-11-14 06:57:52 +08:00
|
|
|
config.Date = time.Now()
|
2017-11-14 03:22:45 +08:00
|
|
|
enc := json.NewEncoder(s)
|
|
|
|
if c.Bool("indent") {
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
}
|
|
|
|
return enc.Encode(config)
|
|
|
|
},
|
2017-11-14 06:57:52 +08:00
|
|
|
}, {
|
|
|
|
Name: "restore",
|
|
|
|
Usage: "Restores a backup",
|
|
|
|
Description: "ssh admin@portal config restore < sshportal.bkp",
|
|
|
|
Flags: []cli.Flag{
|
2017-11-23 17:35:51 +08:00
|
|
|
cli.BoolFlag{Name: "confirm", Usage: "yes, I want to replace everything with this backup!"},
|
2017-11-14 06:57:52 +08:00
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
config := Config{}
|
|
|
|
|
|
|
|
dec := json.NewDecoder(s)
|
|
|
|
if err := dec.Decode(&config); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Fprintf(s, "Loaded backup file (date=%v)\n", config.Date)
|
|
|
|
fmt.Fprintf(s, "* %d ACLs\n", len(config.ACLs))
|
|
|
|
fmt.Fprintf(s, "* %d HostGroups\n", len(config.HostGroups))
|
|
|
|
fmt.Fprintf(s, "* %d Hosts\n", len(config.Hosts))
|
|
|
|
fmt.Fprintf(s, "* %d Keys\n", len(config.SSHKeys))
|
|
|
|
fmt.Fprintf(s, "* %d UserGroups\n", len(config.UserGroups))
|
|
|
|
fmt.Fprintf(s, "* %d Userkeys\n", len(config.UserKeys))
|
|
|
|
fmt.Fprintf(s, "* %d Users\n", len(config.Users))
|
2017-11-19 07:53:32 +08:00
|
|
|
fmt.Fprintf(s, "* %d Settings\n", len(config.Settings))
|
2017-11-14 06:57:52 +08:00
|
|
|
|
|
|
|
if !c.Bool("confirm") {
|
|
|
|
fmt.Fprintf(s, "restore will erase and replace everything in the database.\nIf you are ok, add the '--confirm' to the restore command\n")
|
|
|
|
return errors.New("")
|
|
|
|
}
|
|
|
|
|
|
|
|
tx := db.Begin()
|
|
|
|
|
|
|
|
// FIXME: do everything in a transaction
|
2017-11-19 07:53:32 +08:00
|
|
|
for _, tableName := range []string{"hosts", "users", "acls", "host_groups", "user_groups", "ssh_keys", "user_keys", "settings"} {
|
2017-11-14 06:57:52 +08:00
|
|
|
if err := tx.Exec(fmt.Sprintf("DELETE FROM %s;", tableName)).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, host := range config.Hosts {
|
|
|
|
if err := tx.Create(&host).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, user := range config.Users {
|
|
|
|
if err := tx.Create(&user).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, acl := range config.ACLs {
|
|
|
|
if err := tx.Create(&acl).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, hostGroup := range config.HostGroups {
|
|
|
|
if err := tx.Create(&hostGroup).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, userGroup := range config.UserGroups {
|
|
|
|
if err := tx.Create(&userGroup).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, sshKey := range config.SSHKeys {
|
|
|
|
if err := tx.Create(&sshKey).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, userKey := range config.UserKeys {
|
|
|
|
if err := tx.Create(&userKey).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2017-11-19 07:53:32 +08:00
|
|
|
for _, setting := range config.Settings {
|
|
|
|
if err := tx.Create(&setting).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2017-11-14 06:57:52 +08:00
|
|
|
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Fprintf(s, "Import done.\n")
|
|
|
|
return nil
|
|
|
|
},
|
2017-11-14 03:22:45 +08:00
|
|
|
},
|
|
|
|
},
|
2017-11-11 07:25:27 +08:00
|
|
|
}, {
|
2017-10-31 17:17:06 +08:00
|
|
|
Name: "host",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Manages hosts",
|
2017-10-31 17:17:06 +08:00
|
|
|
Subcommands: []cli.Command{
|
|
|
|
{
|
2017-10-31 23:22:40 +08:00
|
|
|
Name: "create",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Creates a new host",
|
2017-10-31 23:22:40 +08:00
|
|
|
ArgsUsage: "<user>[:<password>]@<host>[:<port>]",
|
2017-11-02 06:32:36 +08:00
|
|
|
Description: "$> host create bart@foo.org\n $> host create bob:marley@example.com:2222",
|
2017-10-31 23:22:40 +08:00
|
|
|
Flags: []cli.Flag{
|
2017-11-15 16:52:59 +08:00
|
|
|
cli.StringFlag{Name: "name, n", Usage: "Assigns a name to the host"},
|
|
|
|
cli.StringFlag{Name: "password, p", Usage: "If present, sshportal will use password-based authentication"},
|
|
|
|
cli.StringFlag{Name: "fingerprint, f", Usage: "SSH host key fingerprint"},
|
|
|
|
cli.StringFlag{Name: "comment, c"},
|
2017-11-23 16:58:32 +08:00
|
|
|
cli.StringFlag{Name: "key, k", Usage: "`KEY` to use for authentication"},
|
2017-11-23 17:35:51 +08:00
|
|
|
cli.StringSliceFlag{Name: "group, g", Usage: "Assigns the host to `HOSTGROUPS` (default: \"default\")"},
|
2017-10-31 23:22:40 +08:00
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() != 1 {
|
2017-11-03 07:23:57 +08:00
|
|
|
return cli.ShowSubcommandHelp(c)
|
2017-10-31 23:22:40 +08:00
|
|
|
}
|
|
|
|
host, err := NewHostFromURL(c.Args().First())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if c.String("password") != "" {
|
|
|
|
host.Password = c.String("password")
|
|
|
|
}
|
|
|
|
host.Fingerprint = c.String("fingerprint")
|
2017-11-02 06:32:36 +08:00
|
|
|
host.Name = strings.Split(host.Hostname(), ".")[0]
|
|
|
|
|
2017-10-31 23:22:40 +08:00
|
|
|
if c.String("name") != "" {
|
|
|
|
host.Name = c.String("name")
|
|
|
|
}
|
2017-11-02 06:32:36 +08:00
|
|
|
// FIXME: check if name already exists
|
2017-11-01 23:41:39 +08:00
|
|
|
host.Comment = c.String("comment")
|
2017-11-16 16:36:43 +08:00
|
|
|
|
|
|
|
if _, err := govalidator.ValidateStruct(host); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-11-02 06:32:36 +08:00
|
|
|
inputKey := c.String("key")
|
|
|
|
if inputKey == "" && host.Password == "" {
|
|
|
|
inputKey = "default"
|
|
|
|
}
|
|
|
|
if inputKey != "" {
|
2017-11-23 16:58:32 +08:00
|
|
|
var key SSHKey
|
|
|
|
if err := SSHKeysByIdentifiers(db, []string{inputKey}).First(&key).Error; err != nil {
|
2017-11-02 06:32:36 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
host.SSHKeyID = key.ID
|
|
|
|
}
|
2017-11-01 23:41:39 +08:00
|
|
|
|
2017-11-08 14:57:30 +08:00
|
|
|
// host group
|
2017-11-15 16:52:59 +08:00
|
|
|
inputGroups := c.StringSlice("group")
|
|
|
|
if len(inputGroups) == 0 {
|
|
|
|
inputGroups = []string{"default"}
|
|
|
|
}
|
2017-11-23 16:58:32 +08:00
|
|
|
if err := HostGroupsByIdentifiers(db, inputGroups).Find(&host.Groups).Error; err != nil {
|
2017-11-08 14:57:30 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-10-31 23:22:40 +08:00
|
|
|
if err := db.Create(&host).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Fprintf(s, "%d\n", host.ID)
|
|
|
|
return nil
|
|
|
|
},
|
2017-11-03 07:15:15 +08:00
|
|
|
}, {
|
2017-10-31 23:22:40 +08:00
|
|
|
Name: "inspect",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Shows detailed information on one or more hosts",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "HOST...",
|
2017-10-31 23:22:40 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
2017-11-03 07:23:57 +08:00
|
|
|
return cli.ShowSubcommandHelp(c)
|
2017-10-31 23:22:40 +08:00
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
var hosts []Host
|
|
|
|
if err := HostsPreload(HostsByIdentifiers(db, c.Args())).Find(&hosts).Error; err != nil {
|
|
|
|
return err
|
2017-10-31 23:22:40 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
enc := json.NewEncoder(s)
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
return enc.Encode(hosts)
|
|
|
|
},
|
2017-11-03 07:15:15 +08:00
|
|
|
}, {
|
2017-10-31 23:22:40 +08:00
|
|
|
Name: "ls",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Lists hosts",
|
2017-10-31 23:22:40 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-11-15 16:52:59 +08:00
|
|
|
var hosts []*Host
|
2017-11-11 01:20:22 +08:00
|
|
|
if err := db.Preload("Groups").Find(&hosts).Error; err != nil {
|
2017-10-31 23:22:40 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
2017-11-11 01:20:22 +08:00
|
|
|
table.SetHeader([]string{"ID", "Name", "URL", "Key", "Pass", "Groups", "Comment"})
|
2017-10-31 23:22:40 +08:00
|
|
|
table.SetBorder(false)
|
|
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d hosts.", len(hosts)))
|
|
|
|
for _, host := range hosts {
|
2017-11-02 06:32:36 +08:00
|
|
|
authKey, authPass := "", ""
|
2017-11-01 23:41:39 +08:00
|
|
|
if host.Password != "" {
|
2017-11-23 17:05:13 +08:00
|
|
|
authPass = "yes"
|
2017-11-02 06:32:36 +08:00
|
|
|
}
|
|
|
|
if host.SSHKeyID > 0 {
|
|
|
|
var key SSHKey
|
|
|
|
db.Model(&host).Related(&key)
|
|
|
|
authKey = key.Name
|
2017-11-01 23:41:39 +08:00
|
|
|
}
|
2017-11-23 17:05:13 +08:00
|
|
|
groupNames := []string{}
|
|
|
|
for _, hostGroup := range host.Groups {
|
|
|
|
groupNames = append(groupNames, hostGroup.Name)
|
|
|
|
}
|
2017-10-31 23:22:40 +08:00
|
|
|
table.Append([]string{
|
|
|
|
fmt.Sprintf("%d", host.ID),
|
|
|
|
host.Name,
|
|
|
|
host.URL(),
|
2017-11-02 06:32:36 +08:00
|
|
|
authKey,
|
|
|
|
authPass,
|
2017-11-23 17:05:13 +08:00
|
|
|
strings.Join(groupNames, ", "),
|
2017-11-01 23:41:39 +08:00
|
|
|
host.Comment,
|
2017-10-31 23:22:40 +08:00
|
|
|
//FIXME: add some stats about last access time etc
|
2017-11-01 23:41:39 +08:00
|
|
|
//FIXME: add creation date
|
2017-10-31 23:22:40 +08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
table.Render()
|
|
|
|
return nil
|
|
|
|
},
|
2017-11-03 07:15:15 +08:00
|
|
|
}, {
|
2017-10-31 23:22:40 +08:00
|
|
|
Name: "rm",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Removes one or more hosts",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "HOST...",
|
2017-10-31 23:22:40 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
2017-11-03 07:23:57 +08:00
|
|
|
return cli.ShowSubcommandHelp(c)
|
2017-10-31 23:22:40 +08:00
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
return HostsByIdentifiers(db, c.Args()).Delete(&Host{}).Error
|
2017-10-31 23:22:40 +08:00
|
|
|
},
|
2017-11-23 17:15:28 +08:00
|
|
|
}, {
|
|
|
|
Name: "update",
|
|
|
|
Usage: "Updates an existing host",
|
|
|
|
ArgsUsage: "HOST",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
cli.StringFlag{Name: "name, n", Usage: "Rename the host"},
|
|
|
|
cli.StringFlag{Name: "password, p", Usage: "Update/set a password, use \"none\" to unset"},
|
|
|
|
cli.StringFlag{Name: "fingerprint, f", Usage: "Update/set a host fingerprint, use \"none\" to unset"},
|
|
|
|
cli.StringFlag{Name: "comment, c", Usage: "Update/set a host comment"},
|
|
|
|
cli.StringFlag{Name: "key, k", Usage: "Link a `KEY` to use for authentication"},
|
2017-11-23 17:35:51 +08:00
|
|
|
cli.StringSliceFlag{Name: "assign-group, g", Usage: "Assign the host to a new `HOSTGROUPS`"},
|
|
|
|
cli.StringSliceFlag{Name: "unassign-group", Usage: "Unassign the host from a `HOSTGROUPS`"},
|
2017-11-23 17:15:28 +08:00
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
var hosts []Host
|
|
|
|
if err := HostsByIdentifiers(db, c.Args()).Find(&hosts).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(hosts) > 1 && c.String("name") != "" {
|
|
|
|
return fmt.Errorf("cannot set --name when editing multiple hosts at once")
|
|
|
|
}
|
|
|
|
|
|
|
|
tx := db.Begin()
|
|
|
|
for _, host := range hosts {
|
|
|
|
model := tx.Model(&host)
|
|
|
|
// simple fields
|
|
|
|
for _, fieldname := range []string{"name", "comment", "password", "fingerprint"} {
|
|
|
|
if c.String(fieldname) != "" {
|
|
|
|
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// associations
|
|
|
|
if c.String("key") != "" {
|
|
|
|
var key SSHKey
|
|
|
|
if err := SSHKeysByIdentifiers(db, []string{c.String("key")}).First(&key).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := model.Association("SSHKey").Replace(&key).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var appendGroups []HostGroup
|
|
|
|
var deleteGroups []HostGroup
|
|
|
|
if err := HostGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := HostGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := model.Association("Groups").Append(&appendGroups).Delete(deleteGroups).Error; err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return tx.Commit().Error
|
|
|
|
},
|
2017-10-31 17:17:06 +08:00
|
|
|
},
|
|
|
|
},
|
2017-11-10 22:59:24 +08:00
|
|
|
}, {
|
|
|
|
Name: "hostgroup",
|
|
|
|
Usage: "Manages host groups",
|
|
|
|
Subcommands: []cli.Command{
|
|
|
|
{
|
|
|
|
Name: "create",
|
|
|
|
Usage: "Creates a new host group",
|
|
|
|
Description: "$> hostgroup create --name=prod",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
cli.StringFlag{Name: "name", Usage: "Assigns a name to the host group"},
|
2017-11-23 17:35:51 +08:00
|
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
2017-11-10 22:59:24 +08:00
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
hostGroup := HostGroup{
|
2017-11-16 16:36:43 +08:00
|
|
|
Name: c.String("name"),
|
|
|
|
Comment: c.String("comment"),
|
2017-11-10 22:59:24 +08:00
|
|
|
}
|
|
|
|
if hostGroup.Name == "" {
|
|
|
|
hostGroup.Name = namesgenerator.GetRandomName(0)
|
|
|
|
}
|
2017-11-16 16:36:43 +08:00
|
|
|
if _, err := govalidator.ValidateStruct(hostGroup); err != nil {
|
|
|
|
return err
|
2017-11-10 22:59:24 +08:00
|
|
|
}
|
|
|
|
// FIXME: check if name already exists
|
|
|
|
|
|
|
|
if err := db.Create(&hostGroup).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Fprintf(s, "%d\n", hostGroup.ID)
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "inspect",
|
|
|
|
Usage: "Shows detailed information on one or more host groups",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "HOSTGROUP...",
|
2017-11-10 22:59:24 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
var hostGroups []HostGroup
|
|
|
|
if err := HostGroupsPreload(HostGroupsByIdentifiers(db, c.Args())).Find(&hostGroups).Error; err != nil {
|
|
|
|
return err
|
2017-11-10 22:59:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
enc := json.NewEncoder(s)
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
return enc.Encode(hostGroups)
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "ls",
|
|
|
|
Usage: "Lists host groups",
|
|
|
|
Action: func(c *cli.Context) error {
|
2017-11-15 16:52:59 +08:00
|
|
|
var hostGroups []*HostGroup
|
2017-11-11 07:27:52 +08:00
|
|
|
if err := db.Preload("ACLs").Preload("Hosts").Find(&hostGroups).Error; err != nil {
|
2017-11-10 22:59:24 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
2017-11-11 07:27:52 +08:00
|
|
|
table.SetHeader([]string{"ID", "Name", "Hosts", "ACLs", "Comment"})
|
2017-11-10 22:59:24 +08:00
|
|
|
table.SetBorder(false)
|
|
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d host groups.", len(hostGroups)))
|
|
|
|
for _, hostGroup := range hostGroups {
|
|
|
|
// FIXME: add more stats (amount of hosts, linked usergroups, ...)
|
|
|
|
table.Append([]string{
|
|
|
|
fmt.Sprintf("%d", hostGroup.ID),
|
|
|
|
hostGroup.Name,
|
2017-11-11 01:20:22 +08:00
|
|
|
fmt.Sprintf("%d", len(hostGroup.Hosts)),
|
2017-11-11 07:27:52 +08:00
|
|
|
fmt.Sprintf("%d", len(hostGroup.ACLs)),
|
2017-11-10 22:59:24 +08:00
|
|
|
hostGroup.Comment,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
table.Render()
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "rm",
|
|
|
|
Usage: "Removes one or more host groups",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "HOSTGROUP...",
|
2017-11-10 22:59:24 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
return HostGroupsByIdentifiers(db, c.Args()).Delete(&HostGroup{}).Error
|
2017-11-10 22:59:24 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2017-10-31 17:17:06 +08:00
|
|
|
}, {
|
2017-11-02 00:00:34 +08:00
|
|
|
Name: "info",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Shows system-wide information",
|
2017-11-02 00:00:34 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
fmt.Fprintf(s, "Debug mode (server): %v\n", globalContext.Bool("debug"))
|
|
|
|
hostname, _ := os.Hostname()
|
|
|
|
fmt.Fprintf(s, "Hostname: %s\n", hostname)
|
|
|
|
fmt.Fprintf(s, "CPUs: %d\n", runtime.NumCPU())
|
|
|
|
fmt.Fprintf(s, "Demo mode: %v\n", globalContext.Bool("demo"))
|
|
|
|
fmt.Fprintf(s, "DB Driver: %s\n", globalContext.String("db-driver"))
|
|
|
|
fmt.Fprintf(s, "DB Conn: %s\n", globalContext.String("db-conn"))
|
|
|
|
fmt.Fprintf(s, "Bind Address: %s\n", globalContext.String("bind-address"))
|
2017-11-02 00:11:50 +08:00
|
|
|
fmt.Fprintf(s, "System Time: %v\n", time.Now().Format(time.RFC3339Nano))
|
|
|
|
fmt.Fprintf(s, "OS Type: %s\n", runtime.GOOS)
|
|
|
|
fmt.Fprintf(s, "OS Architecture: %s\n", runtime.GOARCH)
|
|
|
|
fmt.Fprintf(s, "Go routines: %d\n", runtime.NumGoroutine())
|
|
|
|
fmt.Fprintf(s, "Go version (build): %v\n", runtime.Version())
|
|
|
|
fmt.Fprintf(s, "Uptime: %v\n", time.Since(startTime))
|
2017-11-11 01:06:10 +08:00
|
|
|
|
|
|
|
myself := s.Context().Value(userContextKey).(User)
|
|
|
|
fmt.Fprintf(s, "User email: %v\n", myself.ID)
|
|
|
|
fmt.Fprintf(s, "User email: %s\n", myself.Email)
|
2017-11-14 07:38:23 +08:00
|
|
|
fmt.Fprintf(s, "Version: %s\n", VERSION)
|
|
|
|
fmt.Fprintf(s, "GIT SHA: %s\n", GIT_SHA)
|
|
|
|
fmt.Fprintf(s, "GIT Branch: %s\n", GIT_BRANCH)
|
|
|
|
fmt.Fprintf(s, "GIT Tag: %s\n", GIT_TAG)
|
|
|
|
|
2017-11-02 00:00:34 +08:00
|
|
|
// FIXME: add info about current server (network, cpu, ram, OS)
|
|
|
|
// FIXME: add info about current user
|
|
|
|
// FIXME: add active connections
|
|
|
|
// FIXME: stats
|
|
|
|
return nil
|
|
|
|
},
|
2017-10-31 17:17:06 +08:00
|
|
|
}, {
|
|
|
|
Name: "key",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Manages keys",
|
2017-10-31 17:17:06 +08:00
|
|
|
Subcommands: []cli.Command{
|
|
|
|
{
|
2017-11-01 22:48:30 +08:00
|
|
|
Name: "create",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Creates a new key",
|
2017-11-01 22:48:30 +08:00
|
|
|
Description: "$> key create\n $> key create --name=mykey",
|
|
|
|
Flags: []cli.Flag{
|
2017-11-03 07:15:15 +08:00
|
|
|
cli.StringFlag{Name: "name", Usage: "Assigns a name to the key"},
|
2017-11-01 22:48:30 +08:00
|
|
|
cli.StringFlag{Name: "type", Value: "rsa"},
|
|
|
|
cli.UintFlag{Name: "length", Value: 2048},
|
2017-11-23 17:35:51 +08:00
|
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
2017-11-01 22:48:30 +08:00
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
2017-11-01 23:41:39 +08:00
|
|
|
name := namesgenerator.GetRandomName(0)
|
2017-11-01 22:48:30 +08:00
|
|
|
if c.String("name") != "" {
|
2017-11-01 23:41:39 +08:00
|
|
|
name = c.String("name")
|
2017-11-01 22:48:30 +08:00
|
|
|
}
|
|
|
|
|
2017-11-01 23:41:39 +08:00
|
|
|
key, err := NewSSHKey(c.String("type"), c.Uint("length"))
|
2017-11-01 22:48:30 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-11-01 23:41:39 +08:00
|
|
|
key.Name = name
|
|
|
|
key.Comment = c.String("comment")
|
2017-11-01 22:48:30 +08:00
|
|
|
|
2017-11-16 16:36:43 +08:00
|
|
|
if _, err := govalidator.ValidateStruct(key); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// FIXME: check if name already exists
|
|
|
|
|
2017-11-01 22:48:30 +08:00
|
|
|
// save the key in database
|
|
|
|
if err := db.Create(&key).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Fprintf(s, "%d\n", key.ID)
|
|
|
|
return nil
|
|
|
|
},
|
2017-11-03 07:15:15 +08:00
|
|
|
}, {
|
2017-11-01 22:48:30 +08:00
|
|
|
Name: "inspect",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Shows detailed information on one or more keys",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "KEY...",
|
2017-11-01 22:48:30 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
2017-11-03 07:23:57 +08:00
|
|
|
return cli.ShowSubcommandHelp(c)
|
2017-11-01 22:48:30 +08:00
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
var keys []SSHKey
|
|
|
|
if err := SSHKeysByIdentifiers(db, c.Args()).Find(&keys).Error; err != nil {
|
|
|
|
return err
|
2017-11-01 22:48:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
enc := json.NewEncoder(s)
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
return enc.Encode(keys)
|
|
|
|
},
|
2017-11-03 07:15:15 +08:00
|
|
|
}, {
|
2017-11-01 22:48:30 +08:00
|
|
|
Name: "ls",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Lists keys",
|
2017-11-01 22:48:30 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
var keys []SSHKey
|
2017-11-11 01:20:22 +08:00
|
|
|
if err := db.Preload("Hosts").Find(&keys).Error; err != nil {
|
2017-11-01 22:48:30 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
2017-11-11 01:20:22 +08:00
|
|
|
table.SetHeader([]string{"ID", "Name", "Type", "Length", "Hosts", "Comment"})
|
2017-11-01 22:48:30 +08:00
|
|
|
table.SetBorder(false)
|
|
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d keys.", len(keys)))
|
|
|
|
for _, key := range keys {
|
|
|
|
table.Append([]string{
|
|
|
|
fmt.Sprintf("%d", key.ID),
|
|
|
|
key.Name,
|
|
|
|
key.Type,
|
|
|
|
fmt.Sprintf("%d", key.Length),
|
2017-11-01 23:41:39 +08:00
|
|
|
//key.Fingerprint,
|
2017-11-11 01:20:22 +08:00
|
|
|
fmt.Sprintf("%d", len(key.Hosts)),
|
2017-11-01 23:41:39 +08:00
|
|
|
key.Comment,
|
2017-11-01 22:48:30 +08:00
|
|
|
//FIXME: add some stats
|
2017-11-01 23:41:39 +08:00
|
|
|
//FIXME: add creation date
|
2017-11-01 22:48:30 +08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
table.Render()
|
|
|
|
return nil
|
|
|
|
},
|
2017-11-03 07:15:15 +08:00
|
|
|
}, {
|
2017-11-01 22:48:30 +08:00
|
|
|
Name: "rm",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Removes one or more keys",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "KEY...",
|
2017-11-01 22:48:30 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
2017-11-03 07:23:57 +08:00
|
|
|
return cli.ShowSubcommandHelp(c)
|
2017-11-01 22:48:30 +08:00
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
return SSHKeysByIdentifiers(db, c.Args()).Delete(&SSHKey{}).Error
|
2017-11-01 22:48:30 +08:00
|
|
|
},
|
2017-10-31 17:17:06 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "user",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Manages users",
|
2017-10-31 17:17:06 +08:00
|
|
|
Subcommands: []cli.Command{
|
|
|
|
{
|
2017-11-08 02:40:14 +08:00
|
|
|
Name: "inspect",
|
|
|
|
Usage: "Shows detailed information on one or more users",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "USER...",
|
2017-11-08 02:40:14 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
var users []User
|
|
|
|
if err := UsersPreload(UsersByIdentifiers(db, c.Args())).Find(&users).Error; err != nil {
|
|
|
|
return err
|
2017-11-08 02:40:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
enc := json.NewEncoder(s)
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
return enc.Encode(users)
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "invite",
|
2017-11-03 07:15:15 +08:00
|
|
|
ArgsUsage: "<email>",
|
2017-11-08 02:40:14 +08:00
|
|
|
Usage: "Invites a new user",
|
|
|
|
Description: "$> user invite bob@example.com\n $> user invite --name=Robert bob@example.com",
|
2017-11-03 07:15:15 +08:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
cli.StringFlag{Name: "name", Usage: "Assigns a name to the user"},
|
2017-11-23 17:35:51 +08:00
|
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
|
|
|
cli.StringSliceFlag{Name: "group, g", Usage: "Names or IDs of `USERGROUPS` (default: \"default\")"},
|
2017-11-03 07:15:15 +08:00
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() != 1 {
|
2017-11-03 07:23:57 +08:00
|
|
|
return cli.ShowSubcommandHelp(c)
|
2017-11-03 07:15:15 +08:00
|
|
|
}
|
|
|
|
|
2017-11-08 02:40:14 +08:00
|
|
|
// FIXME: validate email
|
|
|
|
|
2017-11-03 07:15:15 +08:00
|
|
|
email := c.Args().First()
|
|
|
|
name := strings.Split(email, "@")[0]
|
|
|
|
if c.String("name") != "" {
|
|
|
|
name = c.String("name")
|
|
|
|
}
|
|
|
|
|
|
|
|
user := User{
|
2017-11-08 02:40:14 +08:00
|
|
|
Name: name,
|
|
|
|
Email: email,
|
|
|
|
Comment: c.String("comment"),
|
|
|
|
InviteToken: RandStringBytes(16),
|
2017-11-03 07:15:15 +08:00
|
|
|
}
|
|
|
|
|
2017-11-16 16:36:43 +08:00
|
|
|
if _, err := govalidator.ValidateStruct(user); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-11-08 14:57:30 +08:00
|
|
|
// user group
|
2017-11-15 16:52:59 +08:00
|
|
|
inputGroups := c.StringSlice("group")
|
|
|
|
if len(inputGroups) == 0 {
|
|
|
|
inputGroups = []string{"default"}
|
|
|
|
}
|
2017-11-23 16:58:32 +08:00
|
|
|
if err := UserGroupsByIdentifiers(db, inputGroups).Find(&user.Groups).Error; err != nil {
|
2017-11-08 14:57:30 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-11-03 07:15:15 +08:00
|
|
|
// save the user in database
|
|
|
|
if err := db.Create(&user).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-11-08 02:40:14 +08:00
|
|
|
fmt.Fprintf(s, "User %d created.\nTo associate this account with a key, use the following SSH user: 'invite-%s'.\n", user.ID, user.InviteToken)
|
2017-11-03 07:15:15 +08:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "ls",
|
|
|
|
Usage: "Lists users",
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
var users []User
|
2017-11-11 01:20:22 +08:00
|
|
|
if err := db.Preload("Groups").Preload("Keys").Find(&users).Error; err != nil {
|
2017-11-03 07:15:15 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
2017-11-11 01:20:22 +08:00
|
|
|
table.SetHeader([]string{"ID", "Name", "Email", "Keys", "Groups", "Comment"})
|
2017-11-03 07:15:15 +08:00
|
|
|
table.SetBorder(false)
|
|
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d users.", len(users)))
|
|
|
|
for _, user := range users {
|
2017-11-23 17:05:13 +08:00
|
|
|
groupNames := []string{}
|
|
|
|
for _, userGroup := range user.Groups {
|
|
|
|
groupNames = append(groupNames, userGroup.Name)
|
|
|
|
}
|
2017-11-03 07:15:15 +08:00
|
|
|
table.Append([]string{
|
|
|
|
fmt.Sprintf("%d", user.ID),
|
|
|
|
user.Name,
|
|
|
|
user.Email,
|
2017-11-11 01:20:22 +08:00
|
|
|
fmt.Sprintf("%d", len(user.Keys)),
|
2017-11-23 17:05:13 +08:00
|
|
|
strings.Join(groupNames, ", "),
|
2017-11-03 07:15:15 +08:00
|
|
|
user.Comment,
|
|
|
|
//FIXME: add some stats about last access time etc
|
|
|
|
//FIXME: add creation date
|
|
|
|
})
|
|
|
|
}
|
|
|
|
table.Render()
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "rm",
|
|
|
|
Usage: "Removes one or more users",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "USER...",
|
2017-11-03 07:15:15 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
2017-11-03 07:23:57 +08:00
|
|
|
return cli.ShowSubcommandHelp(c)
|
2017-11-03 07:15:15 +08:00
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
return UsersByIdentifiers(db, c.Args()).Delete(&User{}).Error
|
2017-11-03 07:15:15 +08:00
|
|
|
},
|
2017-10-31 17:17:06 +08:00
|
|
|
},
|
|
|
|
},
|
2017-11-11 01:06:10 +08:00
|
|
|
}, {
|
|
|
|
Name: "usergroup",
|
|
|
|
Usage: "Manages user groups",
|
|
|
|
Subcommands: []cli.Command{
|
|
|
|
{
|
|
|
|
Name: "create",
|
|
|
|
Usage: "Creates a new user group",
|
|
|
|
Description: "$> usergroup create --name=prod",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
cli.StringFlag{Name: "name", Usage: "Assigns a name to the user group"},
|
2017-11-23 17:35:51 +08:00
|
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
2017-11-11 01:06:10 +08:00
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
userGroup := UserGroup{
|
2017-11-16 16:36:43 +08:00
|
|
|
Name: c.String("name"),
|
|
|
|
Comment: c.String("comment"),
|
2017-11-11 01:06:10 +08:00
|
|
|
}
|
|
|
|
if userGroup.Name == "" {
|
|
|
|
userGroup.Name = namesgenerator.GetRandomName(0)
|
|
|
|
}
|
2017-11-16 16:36:43 +08:00
|
|
|
|
|
|
|
if _, err := govalidator.ValidateStruct(userGroup); err != nil {
|
|
|
|
return err
|
2017-11-11 01:06:10 +08:00
|
|
|
}
|
|
|
|
// FIXME: check if name already exists
|
|
|
|
|
|
|
|
// add myself to the new group
|
|
|
|
myself := s.Context().Value(userContextKey).(User)
|
|
|
|
// FIXME: use foreign key with ID to avoid updating the user with the context
|
2017-11-15 16:52:59 +08:00
|
|
|
userGroup.Users = []*User{&myself}
|
2017-11-11 01:06:10 +08:00
|
|
|
|
|
|
|
if err := db.Create(&userGroup).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Fprintf(s, "%d\n", userGroup.ID)
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "inspect",
|
|
|
|
Usage: "Shows detailed information on one or more user groups",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "USERGROUP...",
|
2017-11-11 01:06:10 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
var userGroups []UserGroup
|
|
|
|
if err := UserGroupsPreload(UserGroupsByIdentifiers(db, c.Args())).Find(&userGroups).Error; err != nil {
|
|
|
|
return err
|
2017-11-11 01:06:10 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
enc := json.NewEncoder(s)
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
return enc.Encode(userGroups)
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "ls",
|
|
|
|
Usage: "Lists user groups",
|
|
|
|
Action: func(c *cli.Context) error {
|
2017-11-15 16:52:59 +08:00
|
|
|
var userGroups []*UserGroup
|
2017-11-11 07:27:52 +08:00
|
|
|
if err := db.Preload("ACLs").Preload("Users").Find(&userGroups).Error; err != nil {
|
2017-11-11 01:06:10 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
2017-11-11 07:27:52 +08:00
|
|
|
table.SetHeader([]string{"ID", "Name", "Users", "ACLs", "Comment"})
|
2017-11-11 01:06:10 +08:00
|
|
|
table.SetBorder(false)
|
|
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d user groups.", len(userGroups)))
|
|
|
|
for _, userGroup := range userGroups {
|
|
|
|
// FIXME: add more stats (amount of users, linked usergroups, ...)
|
|
|
|
table.Append([]string{
|
|
|
|
fmt.Sprintf("%d", userGroup.ID),
|
|
|
|
userGroup.Name,
|
2017-11-11 01:20:22 +08:00
|
|
|
fmt.Sprintf("%d", len(userGroup.Users)),
|
2017-11-11 07:27:52 +08:00
|
|
|
fmt.Sprintf("%d", len(userGroup.ACLs)),
|
2017-11-11 01:06:10 +08:00
|
|
|
userGroup.Comment,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
table.Render()
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "rm",
|
|
|
|
Usage: "Removes one or more user groups",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "USERGROUP...",
|
2017-11-11 01:06:10 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
return UserGroupsByIdentifiers(db, c.Args()).Delete(&UserGroup{}).Error
|
2017-11-11 01:06:10 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2017-11-04 06:31:46 +08:00
|
|
|
}, {
|
|
|
|
Name: "userkey",
|
|
|
|
Usage: "Manages userkeys",
|
|
|
|
Subcommands: []cli.Command{
|
|
|
|
{
|
|
|
|
Name: "create",
|
|
|
|
ArgsUsage: "<user ID or email>",
|
|
|
|
Usage: "Creates a new userkey",
|
|
|
|
Description: "$> userkey create bob\n $> user create --name=mykey bob",
|
|
|
|
Flags: []cli.Flag{
|
2017-11-23 17:35:51 +08:00
|
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
2017-11-04 06:31:46 +08:00
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() != 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
var user User
|
|
|
|
if err := UsersByIdentifiers(db, c.Args()).First(&user).Error; err != nil {
|
2017-11-04 06:31:46 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Fprintf(s, "Enter key:\n")
|
|
|
|
reader := bufio.NewReader(s)
|
|
|
|
text, _ := reader.ReadString('\n')
|
|
|
|
|
|
|
|
key, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(text))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
userkey := UserKey{
|
2017-11-23 16:58:32 +08:00
|
|
|
User: &user,
|
2017-11-04 06:31:46 +08:00
|
|
|
Key: key.Marshal(),
|
|
|
|
Comment: comment,
|
|
|
|
}
|
|
|
|
if c.String("comment") != "" {
|
|
|
|
userkey.Comment = c.String("comment")
|
|
|
|
}
|
|
|
|
|
2017-11-16 16:36:43 +08:00
|
|
|
if _, err := govalidator.ValidateStruct(userkey); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-11-04 06:31:46 +08:00
|
|
|
// save the userkey in database
|
|
|
|
if err := db.Create(&userkey).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Fprintf(s, "%d\n", userkey.ID)
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "inspect",
|
|
|
|
Usage: "Shows detailed information on one or more userkeys",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "USERKEY...",
|
2017-11-04 06:31:46 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
var userKeys []UserKey
|
|
|
|
if err := UserKeysPreload(UserKeysByIdentifiers(db, c.Args())).Find(&userKeys).Error; err != nil {
|
|
|
|
return err
|
2017-11-04 06:31:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
enc := json.NewEncoder(s)
|
|
|
|
enc.SetIndent("", " ")
|
2017-11-23 16:58:32 +08:00
|
|
|
return enc.Encode(userKeys)
|
2017-11-04 06:31:46 +08:00
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "ls",
|
|
|
|
Usage: "Lists userkeys",
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
var userkeys []UserKey
|
|
|
|
if err := db.Preload("User").Find(&userkeys).Error; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
|
|
table.SetHeader([]string{"ID", "User", "Comment"})
|
|
|
|
table.SetBorder(false)
|
|
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d userkeys.", len(userkeys)))
|
|
|
|
for _, userkey := range userkeys {
|
|
|
|
table.Append([]string{
|
|
|
|
fmt.Sprintf("%d", userkey.ID),
|
|
|
|
userkey.User.Email,
|
|
|
|
// FIXME: add fingerprint
|
|
|
|
userkey.Comment,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
table.Render()
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "rm",
|
|
|
|
Usage: "Removes one or more userkeys",
|
2017-11-23 17:35:51 +08:00
|
|
|
ArgsUsage: "USERKEY...",
|
2017-11-04 06:31:46 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
if c.NArg() < 1 {
|
|
|
|
return cli.ShowSubcommandHelp(c)
|
|
|
|
}
|
|
|
|
|
2017-11-23 16:58:32 +08:00
|
|
|
return UserKeysByIdentifiers(db, c.Args()).Delete(&UserKey{}).Error
|
2017-11-04 06:31:46 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2017-11-02 05:09:08 +08:00
|
|
|
}, {
|
|
|
|
Name: "version",
|
2017-11-03 07:15:15 +08:00
|
|
|
Usage: "Shows the SSHPortal version information",
|
2017-11-02 05:09:08 +08:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-11-14 07:38:23 +08:00
|
|
|
fmt.Fprintf(s, "%s\n", VERSION)
|
2017-11-02 05:09:08 +08:00
|
|
|
return nil
|
|
|
|
},
|
2017-11-14 03:08:12 +08:00
|
|
|
}, {
|
|
|
|
Name: "exit",
|
|
|
|
Usage: "Exit",
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
return cli.NewExitError("", 0)
|
|
|
|
},
|
2017-10-31 17:17:06 +08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(sshCommand) == 0 { // interactive mode
|
|
|
|
term := terminal.NewTerminal(s, "config> ")
|
|
|
|
for {
|
|
|
|
line, err := term.ReadLine()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
words, err := shlex.Split(line, true)
|
|
|
|
if err != nil {
|
|
|
|
io.WriteString(s, "syntax error.\n")
|
|
|
|
continue
|
|
|
|
}
|
2017-11-14 03:08:12 +08:00
|
|
|
if len(words) == 1 && strings.ToLower(words[0]) == "exit" {
|
|
|
|
s.Exit(0)
|
|
|
|
return nil
|
|
|
|
}
|
2017-10-31 23:22:40 +08:00
|
|
|
if err := app.Run(append([]string{"config"}, words...)); err != nil {
|
2017-11-14 03:08:12 +08:00
|
|
|
if cliErr, ok := err.(*cli.ExitError); ok {
|
|
|
|
if cliErr.ExitCode() != 0 {
|
|
|
|
io.WriteString(s, fmt.Sprintf("error: %v\n", err))
|
|
|
|
}
|
|
|
|
//s.Exit(cliErr.ExitCode())
|
|
|
|
} else {
|
|
|
|
io.WriteString(s, fmt.Sprintf("error: %v\n", err))
|
|
|
|
}
|
2017-10-31 23:22:40 +08:00
|
|
|
}
|
2017-10-31 17:17:06 +08:00
|
|
|
}
|
|
|
|
} else { // oneshot mode
|
2017-10-31 23:22:40 +08:00
|
|
|
if err := app.Run(append([]string{"config"}, sshCommand...)); err != nil {
|
2017-11-14 03:08:12 +08:00
|
|
|
if errMsg := err.Error(); errMsg != "" {
|
|
|
|
io.WriteString(s, fmt.Sprintf("error: %s\n", errMsg))
|
|
|
|
}
|
|
|
|
if cliErr, ok := err.(*cli.ExitError); ok {
|
|
|
|
s.Exit(cliErr.ExitCode())
|
|
|
|
} else {
|
|
|
|
s.Exit(1)
|
|
|
|
}
|
2017-10-31 23:22:40 +08:00
|
|
|
}
|
2017-10-31 17:17:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|