mirror of
https://github.com/moul/sshportal.git
synced 2025-09-06 20:54:27 +08:00
2398 lines
72 KiB
Go
2398 lines
72 KiB
Go
package bastion // import "moul.io/sshportal/pkg/bastion"
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
shlex "github.com/anmitsu/go-shlex"
|
|
"github.com/asaskevich/govalidator"
|
|
"github.com/docker/docker/pkg/namesgenerator"
|
|
humanize "github.com/dustin/go-humanize"
|
|
"github.com/gliderlabs/ssh"
|
|
"github.com/mgutz/ansi"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/urfave/cli"
|
|
gossh "golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/terminal" // nolint:staticcheck
|
|
"moul.io/sshportal/pkg/crypto"
|
|
"moul.io/sshportal/pkg/dbmodels"
|
|
"moul.io/sshportal/pkg/utils"
|
|
)
|
|
|
|
var banner = `
|
|
|
|
__________ _____ __ __
|
|
/ __/ __/ // / _ \___ ____/ /____ _/ /
|
|
_\ \_\ \/ _ / ___/ _ \/ __/ __/ _ '/ /
|
|
/___/___/_//_/_/ \___/_/ \__/\_,_/_/
|
|
|
|
|
|
`
|
|
var startTime = time.Now()
|
|
|
|
const (
|
|
naMessage = "n/a"
|
|
)
|
|
|
|
func shell(s ssh.Session, version, gitSha, gitTag string) error {
|
|
var (
|
|
sshCommand = s.Command()
|
|
actx = s.Context().Value(authContextKey).(*authContext)
|
|
)
|
|
if len(sshCommand) == 0 {
|
|
if _, err := fmt.Fprint(s, banner); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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) {}
|
|
cli.HelpFlag = cli.BoolFlag{
|
|
Name: "help, h",
|
|
Hidden: true,
|
|
}
|
|
app := cli.NewApp()
|
|
app.Writer = s
|
|
app.HideVersion = true
|
|
|
|
dbmodels.InitValidator()
|
|
|
|
var (
|
|
myself = &actx.user
|
|
db = actx.db
|
|
)
|
|
|
|
app.Commands = []cli.Command{
|
|
{
|
|
Name: "acl",
|
|
Usage: "Manages ACLs",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "create",
|
|
Usage: "Creates a new ACL",
|
|
Description: "$> acl create -",
|
|
Flags: []cli.Flag{
|
|
cli.StringSliceFlag{Name: "hostgroup, hg", Usage: "Assigns `HOSTGROUPS` to the acl"},
|
|
cli.StringSliceFlag{Name: "usergroup, ug", Usage: "Assigns `USERGROUP` to the acl"},
|
|
cli.StringFlag{Name: "pattern", Usage: "Assigns a host pattern to the acl"},
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
|
cli.StringFlag{Name: "action", Usage: "Assigns the ACL action (allow,deny)", Value: string(dbmodels.ACLActionAllow)},
|
|
cli.UintFlag{Name: "weight, w", Usage: "Assigns the ACL weight (priority)"},
|
|
cli.StringFlag{Name: "inception, i", Usage: "Assigns inception date-time"},
|
|
cli.StringFlag{Name: "expiration, e", Usage: "Assigns expiration date-time"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
inception, err := parseOptionalTime(c.String("inception"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
expiration, err := parseOptionalTime(c.String("expiration"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
acl := dbmodels.ACL{
|
|
Comment: c.String("comment"),
|
|
HostPattern: c.String("pattern"),
|
|
UserGroups: []*dbmodels.UserGroup{},
|
|
HostGroups: []*dbmodels.HostGroup{},
|
|
Weight: c.Uint("weight"),
|
|
Inception: inception,
|
|
Expiration: expiration,
|
|
Action: c.String("action"),
|
|
}
|
|
if acl.Action != string(dbmodels.ACLActionAllow) && acl.Action != string(dbmodels.ACLActionDeny) {
|
|
return fmt.Errorf("invalid action %q, allowed values: allow, deny", acl.Action)
|
|
}
|
|
if _, err := govalidator.ValidateStruct(acl); err != nil {
|
|
return err
|
|
}
|
|
|
|
var userGroups []*dbmodels.UserGroup
|
|
if err := dbmodels.UserGroupsPreload(dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("usergroup"))).Find(&userGroups).Error; err != nil {
|
|
return err
|
|
}
|
|
acl.UserGroups = append(acl.UserGroups, userGroups...)
|
|
var hostGroups []*dbmodels.HostGroup
|
|
if err := dbmodels.HostGroupsPreload(dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("hostgroup"))).Find(&hostGroups).Error; err != nil {
|
|
return err
|
|
}
|
|
acl.HostGroups = append(acl.HostGroups, hostGroups...)
|
|
|
|
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",
|
|
ArgsUsage: "ACL...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var acls []dbmodels.ACL
|
|
if err := dbmodels.ACLsPreload(dbmodels.ACLsByIdentifiers(db, c.Args())).Find(&acls).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
enc := json.NewEncoder(s)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(acls)
|
|
},
|
|
}, {
|
|
Name: "ls",
|
|
Usage: "Lists ACLs",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "latest, l", Usage: "Show the latest ACL"},
|
|
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var acls []*dbmodels.ACL
|
|
query := db.Order("created_at desc").Preload("UserGroups").Preload("HostGroups")
|
|
if c.Bool("latest") {
|
|
var acl dbmodels.ACL
|
|
if err := query.First(&acl).Error; err != nil {
|
|
return err
|
|
}
|
|
acls = append(acls, &acl)
|
|
} else if err := query.Find(&acls).Error; err != nil {
|
|
return err
|
|
}
|
|
if c.Bool("quiet") {
|
|
for _, acl := range acls {
|
|
fmt.Fprintln(s, acl.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
table.SetHeader([]string{"ID", "Weight", "User groups", "Host groups", "Host pattern", "Action", "Inception", "Expiration", "Updated", "Created", "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)
|
|
}
|
|
|
|
inception := ""
|
|
if acl.Inception != nil {
|
|
inception = acl.Inception.Format("2006-01-02 15:04 MST")
|
|
}
|
|
expiration := ""
|
|
if acl.Expiration != nil {
|
|
expiration = acl.Expiration.Format("2006-01-02 15:04 MST")
|
|
}
|
|
|
|
table.Append([]string{
|
|
fmt.Sprintf("%d", acl.ID),
|
|
fmt.Sprintf("%d", acl.Weight),
|
|
strings.Join(userGroups, ", "),
|
|
strings.Join(hostGroups, ", "),
|
|
acl.HostPattern,
|
|
acl.Action,
|
|
inception,
|
|
expiration,
|
|
humanize.Time(acl.UpdatedAt),
|
|
humanize.Time(acl.CreatedAt),
|
|
acl.Comment,
|
|
})
|
|
}
|
|
table.Render()
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "rm",
|
|
Usage: "Removes one or more ACLs",
|
|
ArgsUsage: "ACL...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return dbmodels.ACLsByIdentifiers(db, c.Args()).Unscoped().Delete(&dbmodels.ACL{}).Error
|
|
},
|
|
}, {
|
|
Name: "update",
|
|
Usage: "Updates an existing acl",
|
|
ArgsUsage: "ACL...",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{Name: "action, a", Usage: "Update action"},
|
|
cli.StringFlag{Name: "pattern, p", Usage: "Update host-pattern"},
|
|
cli.UintFlag{Name: "weight, w", Usage: "Update weight"},
|
|
cli.StringFlag{Name: "inception, i", Usage: "Update inception date-time"},
|
|
cli.BoolFlag{Name: "unset-inception", Usage: "Unset inception date-time"},
|
|
cli.BoolFlag{Name: "unset-expiration", Usage: "Unset expiration date-time"},
|
|
cli.StringFlag{Name: "expiration, e", Usage: "Update expiration date-time"},
|
|
cli.StringFlag{Name: "comment, c", Usage: "Update comment"},
|
|
cli.StringSliceFlag{Name: "assign-usergroup, ug", Usage: "Assign the ACL to new `USERGROUPS`"},
|
|
cli.StringSliceFlag{Name: "unassign-usergroup", Usage: "Unassign the ACL from `USERGROUPS`"},
|
|
cli.StringSliceFlag{Name: "assign-hostgroup, hg", Usage: "Assign the ACL to new `HOSTGROUPS`"},
|
|
cli.StringSliceFlag{Name: "unassign-hostgroup", Usage: "Unassign the ACL from `HOSTGROUPS`"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var acls []*dbmodels.ACL
|
|
if err := dbmodels.ACLsByIdentifiers(db, c.Args()).Find(&acls).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
tx := db.Begin()
|
|
for _, acl := range acls {
|
|
model := tx.Model(acl)
|
|
inception, err := parseOptionalTime(c.String("inception"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
expiration, err := parseOptionalTime(c.String("expiration"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
update := dbmodels.ACL{
|
|
Action: c.String("action"),
|
|
HostPattern: c.String("pattern"),
|
|
Weight: c.Uint("weight"),
|
|
Inception: inception,
|
|
Expiration: expiration,
|
|
Comment: c.String("comment"),
|
|
}
|
|
if err := model.Updates(update).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
|
|
if c.Bool("unset-inception") {
|
|
if err := model.Update("inception", nil).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
if c.Bool("unset-expiration") {
|
|
if err := model.Update("expiration", nil).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// associations
|
|
var appendUserGroups []dbmodels.UserGroup
|
|
var deleteUserGroups []dbmodels.UserGroup
|
|
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("assign-usergroup")).Find(&appendUserGroups).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("unassign-usergroup")).Find(&deleteUserGroups).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := model.Association("UserGroups").Append(&appendUserGroups); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if len(deleteUserGroups) > 0 {
|
|
if err := model.Association("UserGroups").Delete(deleteUserGroups); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
var appendHostGroups []dbmodels.HostGroup
|
|
var deleteHostGroups []dbmodels.HostGroup
|
|
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("assign-hostgroup")).Find(&appendHostGroups).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("unassign-hostgroup")).Find(&deleteHostGroups).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := model.Association("HostGroups").Append(&appendHostGroups); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if len(deleteHostGroups) > 0 {
|
|
if err := model.Association("HostGroups").Delete(deleteHostGroups); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return tx.Commit().Error
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
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"},
|
|
cli.BoolFlag{Name: "decrypt", Usage: "decrypt sensitive data"},
|
|
cli.BoolFlag{Name: "ignore-events", Usage: "do not backup events data"},
|
|
},
|
|
Description: "ssh admin@portal config backup > sshportal.bkp",
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
config := dbmodels.Config{}
|
|
if err := dbmodels.HostsPreload(db).Find(&config.Hosts).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := dbmodels.SSHKeysPreload(db).Find(&config.SSHKeys).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, key := range config.SSHKeys {
|
|
crypto.SSHKeyDecrypt(actx.aesKey, key)
|
|
}
|
|
if !c.Bool("decrypt") {
|
|
for _, key := range config.SSHKeys {
|
|
if err := crypto.SSHKeyEncrypt(actx.aesKey, key); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := dbmodels.HostsPreload(db).Find(&config.Hosts).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, host := range config.Hosts {
|
|
crypto.HostDecrypt(actx.aesKey, host)
|
|
}
|
|
if !c.Bool("decrypt") {
|
|
for _, host := range config.Hosts {
|
|
if err := crypto.HostEncrypt(actx.aesKey, host); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := dbmodels.UserKeysPreload(db).Find(&config.UserKeys).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := dbmodels.UsersPreload(db).Find(&config.Users).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := dbmodels.UserGroupsPreload(db).Find(&config.UserGroups).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := dbmodels.HostGroupsPreload(db).Find(&config.HostGroups).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := dbmodels.ACLsPreload(db).Find(&config.ACLs).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := db.Find(&config.Settings).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := dbmodels.SessionsPreload(db).Find(&config.Sessions).Error; err != nil {
|
|
return err
|
|
}
|
|
if !c.Bool("ignore-events") {
|
|
if err := dbmodels.EventsPreload(db).Find(&config.Events).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
config.Date = time.Now()
|
|
enc := json.NewEncoder(s)
|
|
if c.Bool("indent") {
|
|
enc.SetIndent("", " ")
|
|
}
|
|
return enc.Encode(config)
|
|
},
|
|
}, {
|
|
Name: "restore",
|
|
Usage: "Restores a backup",
|
|
Description: "ssh admin@portal config restore < sshportal.bkp",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "confirm", Usage: "yes, I want to replace everything with this backup!"},
|
|
cli.BoolFlag{Name: "decrypt", Usage: "do not encrypt sensitive data"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
config := dbmodels.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))
|
|
fmt.Fprintf(s, "* %d Settings\n", len(config.Settings))
|
|
fmt.Fprintf(s, "* %d Sessions\n", len(config.Sessions))
|
|
fmt.Fprintf(s, "* %d Events\n", len(config.Events))
|
|
|
|
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: handle different migrations:
|
|
// 1. drop tables
|
|
// 2. apply migrations `1` to `<backup-migration-id>`
|
|
// 3. restore data
|
|
// 4. continues migrations
|
|
|
|
// FIXME: tell the administrator to restart the server
|
|
// if the master host key changed
|
|
|
|
// FIXME: do everything in a transaction
|
|
tableNames := []string{
|
|
"acls",
|
|
"events",
|
|
"host_group_acls",
|
|
"host_groups",
|
|
"host_host_groups",
|
|
"hosts",
|
|
"sessions",
|
|
"settings",
|
|
"ssh_keys",
|
|
"user_group_acls",
|
|
"user_groups",
|
|
"user_keys",
|
|
"user_roles",
|
|
"user_user_groups",
|
|
"user_user_roles",
|
|
"users",
|
|
// "migrations",
|
|
}
|
|
for _, tableName := range tableNames {
|
|
/* #nosec */
|
|
if err := tx.Exec(fmt.Sprintf("DELETE FROM %s", tableName)).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
for _, host := range config.Hosts {
|
|
host := host
|
|
crypto.HostDecrypt(actx.aesKey, host)
|
|
if !c.Bool("decrypt") {
|
|
if err := crypto.HostEncrypt(actx.aesKey, host); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := tx.FirstOrCreate(&host).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
for _, user := range config.Users {
|
|
user := user
|
|
if err := tx.FirstOrCreate(&user).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
for _, acl := range config.ACLs {
|
|
acl := acl
|
|
if err := tx.FirstOrCreate(&acl).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
for _, hostGroup := range config.HostGroups {
|
|
hostGroup := hostGroup
|
|
if err := tx.FirstOrCreate(&hostGroup).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
for _, userGroup := range config.UserGroups {
|
|
userGroup := userGroup
|
|
if err := tx.FirstOrCreate(&userGroup).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
for _, sshKey := range config.SSHKeys {
|
|
sshKey := sshKey
|
|
crypto.SSHKeyDecrypt(actx.aesKey, sshKey)
|
|
if !c.Bool("decrypt") {
|
|
if err := crypto.SSHKeyEncrypt(actx.aesKey, sshKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := tx.FirstOrCreate(&sshKey).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
for _, userKey := range config.UserKeys {
|
|
userKey := userKey
|
|
if err := tx.FirstOrCreate(&userKey).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
for _, setting := range config.Settings {
|
|
setting := setting
|
|
if err := tx.FirstOrCreate(&setting).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
for _, session := range config.Sessions {
|
|
session := session
|
|
if err := tx.FirstOrCreate(&session).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
for _, event := range config.Events {
|
|
event := event
|
|
if err := tx.FirstOrCreate(&event).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(s, "Import done.\n")
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
Name: "event",
|
|
Usage: "Manages events",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "inspect",
|
|
Usage: "Shows detailed information on one or more events",
|
|
ArgsUsage: "EVENT...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var events []*dbmodels.Event
|
|
if err := dbmodels.EventsPreload(dbmodels.EventsByIdentifiers(db, c.Args())).Find(&events).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, event := range events {
|
|
if len(event.Args) > 0 {
|
|
if err := json.Unmarshal(event.Args, &event.ArgsMap); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
enc := json.NewEncoder(s)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(events)
|
|
},
|
|
}, {
|
|
Name: "ls",
|
|
Usage: "Lists events",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "latest, l", Usage: "Show the latest event"},
|
|
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var events []dbmodels.Event
|
|
query := db.Order("created_at desc").Preload("Author")
|
|
if c.Bool("latest") {
|
|
var event dbmodels.Event
|
|
if err := query.First(&event).Error; err != nil {
|
|
return err
|
|
}
|
|
events = append(events, event)
|
|
} else if err := query.Find(&events).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.Bool("quiet") {
|
|
for _, event := range events {
|
|
fmt.Fprintln(s, event.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
table.SetHeader([]string{"ID", "Author", "Domain", "Action", "Entity", "Args", "Date"})
|
|
table.SetBorder(false)
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d events.", len(events)))
|
|
for _, event := range events {
|
|
author := ""
|
|
if event.Author != nil {
|
|
author = event.Author.Name
|
|
}
|
|
table.Append([]string{
|
|
fmt.Sprintf("%d", event.ID),
|
|
author,
|
|
event.Domain,
|
|
event.Action,
|
|
event.Entity,
|
|
wrapText(string(event.Args), 30),
|
|
humanize.Time(event.CreatedAt),
|
|
})
|
|
}
|
|
table.Render()
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
Name: "host",
|
|
Usage: "Manages hosts",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "create",
|
|
Usage: "Creates a new host",
|
|
ArgsUsage: "[scheme://]<user>[:<password>]@<host>[:<port>]",
|
|
Description: "$> host create bart@foo.org\n $> host create bob:marley@example.com:2222",
|
|
Flags: []cli.Flag{
|
|
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: "comment, c"},
|
|
cli.StringFlag{Name: "key, k", Usage: "`KEY` to use for authentication"},
|
|
cli.StringFlag{Name: "hop, o", Usage: "Hop to use for connecting to the server"},
|
|
cli.StringFlag{Name: "logging, l", Usage: "Logging mode (disabled, input, everything)"},
|
|
cli.StringSliceFlag{Name: "group, g", Usage: "Assigns the host to `HOSTGROUPS` (default: \"default\")"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() != 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
u, err := parseInputURL(c.Args().First())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
host := &dbmodels.Host{
|
|
URL: u.String(),
|
|
Comment: c.String("comment"),
|
|
}
|
|
if c.String("password") != "" {
|
|
host.Password = c.String("password")
|
|
}
|
|
matched, err := regexp.MatchString(`^([0-9]{1,3}.){3}.([0-9]{1,3})$`, host.Hostname())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if matched {
|
|
host.Name = host.Hostname()
|
|
} else {
|
|
host.Name = strings.Split(host.Hostname(), ".")[0]
|
|
}
|
|
|
|
if c.String("hop") != "" {
|
|
hop, err := dbmodels.HostByName(db, c.String("hop"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
host.Hop = hop
|
|
}
|
|
if c.String("name") != "" {
|
|
host.Name = c.String("name")
|
|
}
|
|
|
|
host.Logging = "everything" // default is everything
|
|
if c.String("logging") != "" {
|
|
host.Logging = c.String("logging")
|
|
}
|
|
// FIXME: check if name already exists
|
|
|
|
if _, err := govalidator.ValidateStruct(host); err != nil {
|
|
return err
|
|
}
|
|
|
|
inputKey := c.String("key")
|
|
if inputKey == "" && host.Password == "" {
|
|
inputKey = "default"
|
|
}
|
|
if inputKey != "" {
|
|
var key dbmodels.SSHKey
|
|
if err := dbmodels.SSHKeysByIdentifiers(db, []string{inputKey}).First(&key).Error; err != nil {
|
|
return err
|
|
}
|
|
host.SSHKeyID = key.ID
|
|
}
|
|
|
|
// host group
|
|
inputGroups := c.StringSlice("group")
|
|
if len(inputGroups) == 0 {
|
|
inputGroups = []string{"default"}
|
|
}
|
|
if err := dbmodels.HostGroupsByIdentifiers(db, inputGroups).Find(&host.Groups).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// encrypt
|
|
if err := crypto.HostEncrypt(actx.aesKey, host); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := db.Create(&host).Error; err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(s, "%d\n", host.ID)
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "inspect",
|
|
Usage: "Shows detailed information on one or more hosts",
|
|
ArgsUsage: "HOST...",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "decrypt", Usage: "Decrypt sensitive data"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin", "listhosts"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var hosts []*dbmodels.Host
|
|
if myself.HasRole("admin") {
|
|
if err := dbmodels.HostsByIdentifiers(db.Preload("Groups").Preload("SSHKey"), c.Args()).Find(&hosts).Error; err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := dbmodels.HostsByIdentifiers(db.Preload("Groups"), c.Args()).Find(&hosts).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if c.Bool("decrypt") {
|
|
for _, host := range hosts {
|
|
crypto.HostDecrypt(actx.aesKey, host)
|
|
}
|
|
}
|
|
|
|
enc := json.NewEncoder(s)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(hosts)
|
|
},
|
|
}, {
|
|
Name: "ls",
|
|
Usage: "Lists hosts",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "latest, l", Usage: "Show the latest host"},
|
|
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin", "listhosts"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var hosts []*dbmodels.Host
|
|
query := db.Order("created_at desc").Preload("Groups")
|
|
if c.Bool("latest") {
|
|
var host dbmodels.Host
|
|
if err := query.First(&host).Error; err != nil {
|
|
return err
|
|
}
|
|
hosts = append(hosts, &host)
|
|
} else if err := query.Find(&hosts).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.Bool("quiet") {
|
|
for _, host := range hosts {
|
|
fmt.Fprintln(s, host.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
table.SetHeader([]string{"ID", "Name", "URL", "Key", "Groups", "Updated", "Created", "Comment", "Hop", "Logging"})
|
|
table.SetBorder(false)
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d hosts.", len(hosts)))
|
|
for _, host := range hosts {
|
|
authKey := ""
|
|
if host.SSHKeyID > 0 {
|
|
var key dbmodels.SSHKey
|
|
if err := db.Model(host).Association("SSHKey").Find(&key); err != nil {
|
|
return err
|
|
}
|
|
authKey = key.Name
|
|
}
|
|
groupNames := []string{}
|
|
for _, hostGroup := range host.Groups {
|
|
groupNames = append(groupNames, hostGroup.Name)
|
|
}
|
|
var hop string
|
|
if host.HopID != 0 {
|
|
var hopHost dbmodels.Host
|
|
if err := db.Model(host).Association("HopID").Find(&hopHost); err != nil {
|
|
return err
|
|
}
|
|
hop = hopHost.Name
|
|
} else {
|
|
hop = ""
|
|
}
|
|
table.Append([]string{
|
|
fmt.Sprintf("%d", host.ID),
|
|
host.Name,
|
|
host.String(),
|
|
authKey,
|
|
strings.Join(groupNames, ", "),
|
|
humanize.Time(host.UpdatedAt),
|
|
humanize.Time(host.CreatedAt),
|
|
host.Comment,
|
|
hop,
|
|
host.Logging,
|
|
//FIXME: add some stats about last access time etc
|
|
})
|
|
}
|
|
table.Render()
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "rm",
|
|
Usage: "Removes one or more hosts",
|
|
ArgsUsage: "HOST...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return dbmodels.HostsByIdentifiers(db, c.Args()).Unscoped().Delete(&dbmodels.Host{}).Error
|
|
},
|
|
}, {
|
|
Name: "update",
|
|
Usage: "Updates an existing host",
|
|
ArgsUsage: "HOST...",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{Name: "name, n", Usage: "Rename the host"},
|
|
cli.StringFlag{Name: "url, u", Usage: "Update connection URL"},
|
|
cli.StringFlag{Name: "comment, c", Usage: "Update/set a host comment"},
|
|
cli.StringFlag{Name: "key, k", Usage: "Link a `KEY` to use for authentication"},
|
|
cli.StringFlag{Name: "hop, o", Usage: "Change the hop to use for connecting to the server"},
|
|
cli.StringFlag{Name: "logging, l", Usage: "Logging mode (disabled, input, everything)"},
|
|
cli.BoolFlag{Name: "unset-hop", Usage: "Remove the hop set for this host"},
|
|
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`"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var hosts []dbmodels.Host
|
|
if err := dbmodels.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 {
|
|
host := host
|
|
model := tx.Model(&host)
|
|
// simple fields
|
|
for _, fieldname := range []string{"name", "comment"} {
|
|
if c.String(fieldname) != "" {
|
|
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// url
|
|
if c.String("url") != "" {
|
|
u, err := parseInputURL(c.String("url"))
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := model.Update("url", u.String()).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// hop
|
|
if c.String("hop") != "" {
|
|
hop, err := dbmodels.HostByName(db, c.String("hop"))
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := model.Association("Hop").Replace(hop); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// logging
|
|
if logging := c.String("logging"); logging != "" {
|
|
if !dbmodels.IsValidHostLoggingMode(logging) {
|
|
return fmt.Errorf("invalid host logging mode: %q", logging)
|
|
}
|
|
if err := model.Update("logging", logging).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// remove the hop
|
|
if c.Bool("unset-hop") {
|
|
var hopHost dbmodels.Host
|
|
|
|
if err := db.Model(&host).Association("HopID").Find(&hopHost); err != nil {
|
|
return err
|
|
}
|
|
if err := model.Association("Hop").Clear(); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// associations
|
|
if c.String("key") != "" {
|
|
var key dbmodels.SSHKey
|
|
if err := dbmodels.SSHKeysByIdentifiers(db, []string{c.String("key")}).First(&key).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := model.Association("SSHKey").Replace(&key); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
var appendGroups []dbmodels.HostGroup
|
|
var deleteGroups []dbmodels.HostGroup
|
|
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := model.Association("Groups").Append(&appendGroups); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if len(deleteGroups) > 0 {
|
|
if err := model.Association("Groups").Delete(deleteGroups); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return tx.Commit().Error
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
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"},
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
hostGroup := dbmodels.HostGroup{
|
|
Name: c.String("name"),
|
|
Comment: c.String("comment"),
|
|
}
|
|
if hostGroup.Name == "" {
|
|
hostGroup.Name = namesgenerator.GetRandomName(0)
|
|
}
|
|
if _, err := govalidator.ValidateStruct(hostGroup); err != nil {
|
|
return err
|
|
}
|
|
// 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",
|
|
ArgsUsage: "HOSTGROUP...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var hostGroups []dbmodels.HostGroup
|
|
if err := dbmodels.HostGroupsPreload(dbmodels.HostGroupsByIdentifiers(db, c.Args())).Find(&hostGroups).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
enc := json.NewEncoder(s)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(hostGroups)
|
|
},
|
|
}, {
|
|
Name: "ls",
|
|
Usage: "Lists host groups",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "latest, l", Usage: "Show the latest host group"},
|
|
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var hostGroups []*dbmodels.HostGroup
|
|
query := db.Order("created_at desc").Preload("ACLs").Preload("Hosts")
|
|
if c.Bool("latest") {
|
|
var hostGroup dbmodels.HostGroup
|
|
if err := query.First(&hostGroup).Error; err != nil {
|
|
return err
|
|
}
|
|
hostGroups = append(hostGroups, &hostGroup)
|
|
} else if err := query.Find(&hostGroups).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.Bool("quiet") {
|
|
for _, hostGroup := range hostGroups {
|
|
fmt.Fprintln(s, hostGroup.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
table.SetHeader([]string{"ID", "Name", "Hosts", "ACLs", "Updated", "Created", "Comment"})
|
|
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,
|
|
fmt.Sprintf("%d", len(hostGroup.Hosts)),
|
|
fmt.Sprintf("%d", len(hostGroup.ACLs)),
|
|
humanize.Time(hostGroup.UpdatedAt),
|
|
humanize.Time(hostGroup.CreatedAt),
|
|
hostGroup.Comment,
|
|
})
|
|
}
|
|
table.Render()
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "rm",
|
|
Usage: "Removes one or more host groups",
|
|
ArgsUsage: "HOSTGROUP...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return dbmodels.HostGroupsByIdentifiers(db, c.Args()).Unscoped().Delete(&dbmodels.HostGroup{}).Error
|
|
},
|
|
}, {
|
|
Name: "update",
|
|
Usage: "Updates a host group",
|
|
ArgsUsage: "HOSTGROUP...",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{Name: "name", Usage: "Assigns a new name to the host group"},
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var hostgroups []*dbmodels.HostGroup
|
|
if err := dbmodels.HostGroupsByIdentifiers(db, c.Args()).Find(&hostgroups).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(hostgroups) > 1 && c.String("name") != "" {
|
|
return fmt.Errorf("cannot set --name when editing multiple hostgroups at once")
|
|
}
|
|
|
|
tx := db.Begin()
|
|
for _, hostgroup := range hostgroups {
|
|
model := tx.Model(hostgroup)
|
|
// simple fields
|
|
for _, fieldname := range []string{"name", "comment"} {
|
|
if c.String(fieldname) != "" {
|
|
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return tx.Commit().Error
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
Name: "info",
|
|
Usage: "Shows system-wide information",
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(s, "debug mode (server): %v\n", actx.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", actx.demo)
|
|
fmt.Fprintf(s, "DB Driver: %s\n", actx.dbDriver)
|
|
fmt.Fprintf(s, "DB Conn: %s\n", actx.dbURL)
|
|
fmt.Fprintf(s, "Bind Address: %s\n", actx.bindAddr)
|
|
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))
|
|
|
|
fmt.Fprintf(s, "User ID: %v\n", myself.ID)
|
|
fmt.Fprintf(s, "User email: %s\n", myself.Email)
|
|
fmt.Fprintf(s, "Version: %s\n", version)
|
|
fmt.Fprintf(s, "GIT SHA: %s\n", gitSha)
|
|
fmt.Fprintf(s, "GIT Tag: %s\n", gitTag)
|
|
|
|
// FIXME: gormigrate version
|
|
// FIXME: add info about current server (network, cpu, ram, OS)
|
|
// FIXME: add info about current user
|
|
// FIXME: add active connections
|
|
// FIXME: stats
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "key",
|
|
Usage: "Manages keys",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "create",
|
|
Usage: "Creates a new key",
|
|
Description: "$> key create\n $> key create --name=mykey",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{Name: "name", Usage: "Assigns a name to the key"},
|
|
cli.StringFlag{Name: "type", Value: "ed25519"},
|
|
cli.UintFlag{Name: "length", Value: 0},
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
name := namesgenerator.GetRandomName(0)
|
|
if c.String("name") != "" {
|
|
name = c.String("name")
|
|
}
|
|
|
|
length := c.Uint("length")
|
|
if length == 0 {
|
|
switch c.String("type") {
|
|
case "rsa":
|
|
// same default as ssh-keygen
|
|
length = 3072
|
|
case "ecdsa":
|
|
// same default as ssh-keygen
|
|
length = 256
|
|
case "ed25519":
|
|
// irrelevant for ed25519
|
|
// set it to 1 to enforce consistency
|
|
// and because 0 is invalid
|
|
length = 1
|
|
}
|
|
}
|
|
|
|
key, err := crypto.NewSSHKey(c.String("type"), length)
|
|
if actx.aesKey != "" {
|
|
if err2 := crypto.SSHKeyEncrypt(actx.aesKey, key); err2 != nil {
|
|
return err2
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key.Name = name
|
|
key.Comment = c.String("comment")
|
|
|
|
if _, err := govalidator.ValidateStruct(key); err != nil {
|
|
return err
|
|
}
|
|
// FIXME: check if name already exists
|
|
|
|
// save the key in database
|
|
if err := db.Create(&key).Error; err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(s, "%d\n", key.ID)
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "import",
|
|
Usage: "Imports an existing private key",
|
|
Description: "$> key import\n $> key import --name=mykey",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{Name: "name", Usage: "Assigns a name to the key"},
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var name string
|
|
if c.String("name") != "" {
|
|
name = c.String("name")
|
|
} else {
|
|
name = namesgenerator.GetRandomName(0)
|
|
}
|
|
|
|
var value string
|
|
term := terminal.NewTerminal(s, "Paste your key and end with a blank line> ")
|
|
for {
|
|
line, err := term.ReadLine()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if line != "" {
|
|
value += line + "\n"
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
key, err := crypto.ImportSSHKey(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
key.Name = name
|
|
key.Comment = c.String("comment")
|
|
|
|
if _, err := govalidator.ValidateStruct(key); err != nil {
|
|
return err
|
|
}
|
|
// FIXME: check if name already exists
|
|
|
|
// save the key in database
|
|
if err := db.Create(&key).Error; err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(s, "%d\n", key.ID)
|
|
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "inspect",
|
|
Usage: "Shows detailed information on one or more keys",
|
|
ArgsUsage: "KEY...",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "decrypt", Usage: "Decrypt sensitive data"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var keys []*dbmodels.SSHKey
|
|
if err := dbmodels.SSHKeysByIdentifiers(dbmodels.SSHKeysPreload(db), c.Args()).Find(&keys).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.Bool("decrypt") {
|
|
for _, key := range keys {
|
|
crypto.SSHKeyDecrypt(actx.aesKey, key)
|
|
}
|
|
}
|
|
|
|
enc := json.NewEncoder(s)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(keys)
|
|
},
|
|
}, {
|
|
Name: "ls",
|
|
Usage: "Lists keys",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "latest, l", Usage: "Show the latest key"},
|
|
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var sshKeys []*dbmodels.SSHKey
|
|
query := db.Order("created_at desc").Preload("Hosts")
|
|
if c.Bool("latest") {
|
|
var sshKey dbmodels.SSHKey
|
|
if err := query.First(&sshKey).Error; err != nil {
|
|
return err
|
|
}
|
|
sshKeys = append(sshKeys, &sshKey)
|
|
} else if err := query.Find(&sshKeys).Error; err != nil {
|
|
return err
|
|
}
|
|
if c.Bool("quiet") {
|
|
for _, sshKey := range sshKeys {
|
|
fmt.Fprintln(s, sshKey.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
table.SetHeader([]string{"ID", "Name", "Type", "Length", "Hosts", "Updated", "Created", "Comment"})
|
|
table.SetBorder(false)
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d keys.", len(sshKeys)))
|
|
for _, key := range sshKeys {
|
|
table.Append([]string{
|
|
fmt.Sprintf("%d", key.ID),
|
|
key.Name,
|
|
key.Type,
|
|
fmt.Sprintf("%d", key.Length),
|
|
fmt.Sprintf("%d", len(key.Hosts)),
|
|
humanize.Time(key.UpdatedAt),
|
|
humanize.Time(key.CreatedAt),
|
|
key.Comment,
|
|
//FIXME: add some stats
|
|
})
|
|
}
|
|
table.Render()
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "rm",
|
|
Usage: "Removes one or more keys",
|
|
ArgsUsage: "KEY...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return dbmodels.SSHKeysByIdentifiers(db, c.Args()).Unscoped().Delete(&dbmodels.SSHKey{}).Error
|
|
},
|
|
}, {
|
|
Name: "setup",
|
|
Usage: "Return shell command to install key on remote host",
|
|
ArgsUsage: "KEY",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() != 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
// not checking roles, everyone with an account can see how to enroll new hosts
|
|
|
|
var key dbmodels.SSHKey
|
|
if err := dbmodels.SSHKeysByIdentifiers(db, c.Args()).First(&key).Error; err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(s, "umask 077; mkdir -p .ssh; echo %s sshportal >> .ssh/authorized_keys\n", key.PubKey)
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "show",
|
|
Usage: "Shows standard information on a `KEY`",
|
|
ArgsUsage: "KEY",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() != 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
// not checking roles, everyone with an account can see how to enroll new hosts
|
|
|
|
var key dbmodels.SSHKey
|
|
if err := dbmodels.SSHKeysByIdentifiers(dbmodels.SSHKeysPreload(db), c.Args()).First(&key).Error; err != nil {
|
|
return err
|
|
}
|
|
crypto.SSHKeyDecrypt(actx.aesKey, &key)
|
|
|
|
type line struct {
|
|
key string
|
|
value string
|
|
}
|
|
type section struct {
|
|
name string
|
|
lines []line
|
|
}
|
|
var hosts []string
|
|
for _, host := range key.Hosts {
|
|
hosts = append(hosts, host.Name)
|
|
}
|
|
sections := []section{
|
|
{
|
|
name: "General",
|
|
lines: []line{
|
|
{"Name", key.Name},
|
|
{"Type", key.Type},
|
|
{"Length", fmt.Sprintf("%d", key.Length)},
|
|
{"Comment", key.Comment},
|
|
},
|
|
}, {
|
|
name: "Relationships",
|
|
lines: []line{
|
|
{"Linked hosts", fmt.Sprintf("%s (%d)", strings.Join(hosts, ", "), len(hosts))},
|
|
},
|
|
}, {
|
|
name: "Crypto",
|
|
lines: []line{
|
|
{"authorized_key format", key.PubKey},
|
|
{"Private Key", key.PrivKey},
|
|
},
|
|
}, {
|
|
name: "Help",
|
|
lines: []line{
|
|
{"inspect", fmt.Sprintf("ssh sshportal key inspect %s", key.Name)},
|
|
{"setup", fmt.Sprintf(`ssh user@example.com "$(ssh sshportal key setup %s)"`, key.Name)},
|
|
},
|
|
},
|
|
}
|
|
|
|
valueColor := ansi.ColorFunc("white")
|
|
titleColor := ansi.ColorFunc("magenta+bh")
|
|
keyColor := ansi.ColorFunc("red+bh")
|
|
for _, section := range sections {
|
|
fmt.Fprintf(s, "%s\n%s\n", titleColor(section.name), strings.Repeat("=", len(section.name)))
|
|
for _, line := range section.lines {
|
|
if strings.Contains(line.value, "\n") {
|
|
fmt.Fprintf(s, "%s:\n%s\n", keyColor(line.key), valueColor(line.value))
|
|
} else {
|
|
fmt.Fprintf(s, "%s: %s\n", keyColor(line.key), valueColor(line.value))
|
|
}
|
|
}
|
|
fmt.Fprintf(s, "\n")
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
Name: "user",
|
|
Usage: "Manages users",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "inspect",
|
|
Usage: "Shows detailed information on one or more users",
|
|
ArgsUsage: "USER...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var users []dbmodels.User
|
|
if err := dbmodels.UsersPreload(dbmodels.UsersByIdentifiers(db, c.Args())).Find(&users).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
enc := json.NewEncoder(s)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(users)
|
|
},
|
|
}, {
|
|
Name: "invite",
|
|
ArgsUsage: "<email>",
|
|
Usage: "Invites a new user",
|
|
Description: "$> user invite bob@example.com\n $> user invite --name=Robert bob@example.com",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{Name: "name", Usage: "Assigns a name to the user"},
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
|
cli.StringSliceFlag{Name: "group, g", Usage: "Names or IDs of `USERGROUPS` (default: \"default\")"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() != 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
email := c.Args().First()
|
|
valid := utils.ValidateEmail(email)
|
|
if !valid {
|
|
return errors.New("invalid email")
|
|
}
|
|
name := strings.Split(email, "@")[0]
|
|
if c.String("name") != "" {
|
|
name = c.String("name")
|
|
}
|
|
|
|
user := dbmodels.User{
|
|
Name: name,
|
|
Email: email,
|
|
Comment: c.String("comment"),
|
|
InviteToken: randStringBytes(16),
|
|
}
|
|
|
|
if _, err := govalidator.ValidateStruct(user); err != nil {
|
|
return err
|
|
}
|
|
|
|
// user group
|
|
inputGroups := c.StringSlice("group")
|
|
if len(inputGroups) == 0 {
|
|
inputGroups = []string{"default"}
|
|
}
|
|
if err := dbmodels.UserGroupsByIdentifiers(db, inputGroups).Find(&user.Groups).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// save the user in database
|
|
if err := db.Create(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
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)
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "ls",
|
|
Usage: "Lists users",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "latest, l", Usage: "Show the latest user"},
|
|
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var users []*dbmodels.User
|
|
query := db.Order("created_at desc").Preload("Groups").Preload("Roles").Preload("Keys")
|
|
if c.Bool("latest") {
|
|
var user dbmodels.User
|
|
if err := query.First(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
users = append(users, &user)
|
|
} else if err := query.Find(&users).Error; err != nil {
|
|
return err
|
|
}
|
|
if c.Bool("quiet") {
|
|
for _, user := range users {
|
|
fmt.Fprintln(s, user.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
table.SetHeader([]string{"ID", "Name", "Email", "Roles", "Keys", "Groups", "Updated", "Created", "Comment"})
|
|
table.SetBorder(false)
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d users.", len(users)))
|
|
for _, user := range users {
|
|
groupNames := []string{}
|
|
for _, userGroup := range user.Groups {
|
|
groupNames = append(groupNames, userGroup.Name)
|
|
}
|
|
roleNames := []string{}
|
|
for _, role := range user.Roles {
|
|
roleNames = append(roleNames, role.Name)
|
|
}
|
|
table.Append([]string{
|
|
fmt.Sprintf("%d", user.ID),
|
|
user.Name,
|
|
user.Email,
|
|
strings.Join(roleNames, ", "),
|
|
fmt.Sprintf("%d", len(user.Keys)),
|
|
strings.Join(groupNames, ", "),
|
|
humanize.Time(user.UpdatedAt),
|
|
humanize.Time(user.CreatedAt),
|
|
user.Comment,
|
|
})
|
|
}
|
|
table.Render()
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "rm",
|
|
Usage: "Removes one or more users",
|
|
ArgsUsage: "USER...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return dbmodels.UsersByIdentifiers(db, c.Args()).Unscoped().Delete(&dbmodels.User{}).Error
|
|
},
|
|
}, {
|
|
Name: "update",
|
|
Usage: "Updates an existing user",
|
|
ArgsUsage: "USER...",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{Name: "name, n", Usage: "Renames the user"},
|
|
cli.StringFlag{Name: "email, e", Usage: "Updates the email"},
|
|
cli.StringFlag{Name: "invite_token, i", Usage: "Updates the invite token"},
|
|
cli.BoolFlag{Name: "remove_invite, R", Usage: "Remove invite token"},
|
|
cli.StringSliceFlag{Name: "assign-role, r", Usage: "Assign the user to new `USERROLES`"},
|
|
cli.StringSliceFlag{Name: "unassign-role", Usage: "Unassign the user from `USERROLES`"},
|
|
cli.StringSliceFlag{Name: "assign-group, g", Usage: "Assign the user to new `USERGROUPS`"},
|
|
cli.StringSliceFlag{Name: "unassign-group", Usage: "Unassign the user from `USERGROUPS`"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// FIXME: check if unset-admin + user == myself
|
|
var users []*dbmodels.User
|
|
if err := dbmodels.UsersByIdentifiers(db, c.Args()).Find(&users).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.Bool("set-admin") && c.Bool("unset-admin") {
|
|
return fmt.Errorf("cannot use --set-admin and --unset-admin altogether")
|
|
}
|
|
|
|
if len(users) > 1 && c.String("email") != "" {
|
|
return fmt.Errorf("cannot set --email when editing multiple users at once")
|
|
}
|
|
|
|
tx := db.Begin()
|
|
for _, user := range users {
|
|
model := tx.Model(user)
|
|
// simple fields
|
|
for _, fieldname := range []string{"name", "email", "comment", "invite_token"} {
|
|
if c.String(fieldname) != "" {
|
|
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
// invite remove
|
|
if c.Bool("remove_invite") {
|
|
if err := model.Update("invite_token", "").Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// associations
|
|
var appendGroups []dbmodels.UserGroup
|
|
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
var deleteGroups []dbmodels.UserGroup
|
|
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := model.Association("Groups").Append(&appendGroups); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if len(deleteGroups) > 0 {
|
|
if err := model.Association("Groups").Delete(deleteGroups); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
var appendRoles []dbmodels.UserRole
|
|
if err := dbmodels.UserRolesByIdentifiers(db, c.StringSlice("assign-role")).Find(&appendRoles).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
var deleteRoles []dbmodels.UserRole
|
|
if err := dbmodels.UserRolesByIdentifiers(db, c.StringSlice("unassign-role")).Find(&deleteRoles).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if err := model.Association("Roles").Append(&appendRoles); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
if len(deleteRoles) > 0 {
|
|
if err := model.Association("Roles").Delete(deleteRoles); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return tx.Commit().Error
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
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"},
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
userGroup := dbmodels.UserGroup{
|
|
Name: c.String("name"),
|
|
Comment: c.String("comment"),
|
|
}
|
|
if userGroup.Name == "" {
|
|
userGroup.Name = namesgenerator.GetRandomName(0)
|
|
}
|
|
|
|
if _, err := govalidator.ValidateStruct(userGroup); err != nil {
|
|
return err
|
|
}
|
|
// FIXME: check if name already exists
|
|
// FIXME: add myself to the new group
|
|
|
|
userGroup.Users = []*dbmodels.User{myself}
|
|
|
|
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",
|
|
ArgsUsage: "USERGROUP...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var userGroups []dbmodels.UserGroup
|
|
if err := dbmodels.UserGroupsPreload(dbmodels.UserGroupsByIdentifiers(db, c.Args())).Find(&userGroups).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
enc := json.NewEncoder(s)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(userGroups)
|
|
},
|
|
}, {
|
|
Name: "ls",
|
|
Usage: "Lists user groups",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "latest, l", Usage: "Show the latest user group"},
|
|
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var userGroups []*dbmodels.UserGroup
|
|
query := db.Order("created_at desc").Preload("ACLs").Preload("Users")
|
|
if c.Bool("latest") {
|
|
var userGroup dbmodels.UserGroup
|
|
if err := query.First(&userGroup).Error; err != nil {
|
|
return err
|
|
}
|
|
userGroups = append(userGroups, &userGroup)
|
|
} else if err := query.Find(&userGroups).Error; err != nil {
|
|
return err
|
|
}
|
|
if c.Bool("quiet") {
|
|
for _, userGroup := range userGroups {
|
|
fmt.Fprintln(s, userGroup.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
table.SetHeader([]string{"ID", "Name", "Users", "ACLs", "Update", "Create", "Comment"})
|
|
table.SetBorder(false)
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d user groups.", len(userGroups)))
|
|
for _, userGroup := range userGroups {
|
|
table.Append([]string{
|
|
fmt.Sprintf("%d", userGroup.ID),
|
|
userGroup.Name,
|
|
fmt.Sprintf("%d", len(userGroup.Users)),
|
|
fmt.Sprintf("%d", len(userGroup.ACLs)),
|
|
humanize.Time(userGroup.UpdatedAt),
|
|
humanize.Time(userGroup.CreatedAt),
|
|
userGroup.Comment,
|
|
})
|
|
// FIXME: add more stats (amount of users, linked usergroups, ...)
|
|
}
|
|
table.Render()
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "rm",
|
|
Usage: "Removes one or more user groups",
|
|
ArgsUsage: "USERGROUP...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return dbmodels.UserGroupsByIdentifiers(db, c.Args()).Unscoped().Delete(&dbmodels.UserGroup{}).Error
|
|
},
|
|
}, {
|
|
Name: "update",
|
|
Usage: "Updates a user group",
|
|
ArgsUsage: "USERGROUP...",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{Name: "name", Usage: "Assigns a new name to the user group"},
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var usergroups []*dbmodels.UserGroup
|
|
if err := dbmodels.UserGroupsByIdentifiers(db, c.Args()).Find(&usergroups).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(usergroups) > 1 && c.String("name") != "" {
|
|
return fmt.Errorf("cannot set --name when editing multiple usergroups at once")
|
|
}
|
|
|
|
tx := db.Begin()
|
|
for _, usergroup := range usergroups {
|
|
model := tx.Model(usergroup)
|
|
// simple fields
|
|
for _, fieldname := range []string{"name", "comment"} {
|
|
if c.String(fieldname) != "" {
|
|
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return tx.Commit().Error
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
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{
|
|
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() != 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var user dbmodels.User
|
|
if err := dbmodels.UsersByIdentifiers(db, c.Args()).First(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
var reader *bufio.Reader
|
|
var term *terminal.Terminal
|
|
if len(sshCommand) == 0 { // interactive mode
|
|
term = terminal.NewTerminal(s, "Paste your key(s) and end with a blank line> ")
|
|
} else {
|
|
fmt.Fprintf(s, "Enter key(s):\n")
|
|
reader = bufio.NewReader(s)
|
|
}
|
|
|
|
for {
|
|
var text string
|
|
var errReadline error
|
|
if len(sshCommand) == 0 { // interactive mode
|
|
text, errReadline = term.ReadLine()
|
|
} else {
|
|
text, errReadline = reader.ReadString('\n')
|
|
}
|
|
if errReadline != nil && errReadline != io.EOF {
|
|
return errReadline
|
|
}
|
|
if text != "" && text != "\n" {
|
|
key, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(text))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userkey := dbmodels.UserKey{
|
|
User: &user,
|
|
Key: key.Marshal(),
|
|
Comment: comment,
|
|
AuthorizedKey: string(gossh.MarshalAuthorizedKey(key)),
|
|
}
|
|
if c.String("comment") != "" {
|
|
userkey.Comment = c.String("comment")
|
|
}
|
|
|
|
if _, err := govalidator.ValidateStruct(userkey); err != nil {
|
|
return err
|
|
}
|
|
|
|
// save the userkey in database
|
|
if err := db.Create(&userkey).Error; err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(s, "%d\n", userkey.ID)
|
|
if errReadline == io.EOF {
|
|
return nil
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "inspect",
|
|
Usage: "Shows detailed information on one or more userkeys",
|
|
ArgsUsage: "USERKEY...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var userKeys []dbmodels.UserKey
|
|
if err := dbmodels.UserKeysPreload(dbmodels.UserKeysByIdentifiers(db, c.Args())).Find(&userKeys).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
enc := json.NewEncoder(s)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(userKeys)
|
|
},
|
|
}, {
|
|
Name: "ls",
|
|
Usage: "Lists userkeys",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "latest, l", Usage: "Show the latest user key"},
|
|
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var userKeys []*dbmodels.UserKey
|
|
query := db.Order("created_at desc").Preload("User")
|
|
if c.Bool("latest") {
|
|
var userKey dbmodels.UserKey
|
|
if err := query.First(&userKey).Error; err != nil {
|
|
return err
|
|
}
|
|
userKeys = append(userKeys, &userKey)
|
|
} else if err := query.Find(&userKeys).Error; err != nil {
|
|
return err
|
|
}
|
|
if c.Bool("quiet") {
|
|
for _, userKey := range userKeys {
|
|
fmt.Fprintln(s, userKey.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
table.SetHeader([]string{"ID", "User", "Updated", "Created", "Comment"})
|
|
table.SetBorder(false)
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d userkeys.", len(userKeys)))
|
|
for _, userkey := range userKeys {
|
|
email := naMessage
|
|
if userkey.User != nil {
|
|
email = userkey.User.Email
|
|
}
|
|
table.Append([]string{
|
|
fmt.Sprintf("%d", userkey.ID),
|
|
email,
|
|
// FIXME: add fingerprint
|
|
humanize.Time(userkey.UpdatedAt),
|
|
humanize.Time(userkey.CreatedAt),
|
|
userkey.Comment,
|
|
})
|
|
}
|
|
table.Render()
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "rm",
|
|
Usage: "Removes one or more userkeys",
|
|
ArgsUsage: "USERKEY...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
if err := dbmodels.UserKeysByIdentifiers(db, c.Args()).Find(&dbmodels.UserKey{}).Error; err != nil {
|
|
var user dbmodels.User
|
|
if err := dbmodels.UsersByIdentifiers(db, c.Args()).First(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := dbmodels.UserKeysByUserID(db, []string{fmt.Sprint(user.ID)}).Find(&dbmodels.UserKey{}).Error; err != nil {
|
|
return err
|
|
}
|
|
return dbmodels.UserKeysByUserID(db, []string{fmt.Sprint(user.ID)}).Unscoped().Delete(&dbmodels.UserKey{}).Error
|
|
}
|
|
return dbmodels.UserKeysByIdentifiers(db, c.Args()).Unscoped().Delete(&dbmodels.UserKey{}).Error
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
Name: "session",
|
|
Usage: "Manages sessions",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "inspect",
|
|
Usage: "Shows detailed information on one or more sessions",
|
|
ArgsUsage: "SESSION...",
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return cli.ShowSubcommandHelp(c)
|
|
}
|
|
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var sessions []dbmodels.Session
|
|
if err := dbmodels.SessionsPreload(dbmodels.SessionsByIdentifiers(db, c.Args())).Find(&sessions).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
enc := json.NewEncoder(s)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(sessions)
|
|
},
|
|
}, {
|
|
Name: "ls",
|
|
Usage: "Lists sessions",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{Name: "latest, l", Usage: "Show the latest session"},
|
|
cli.BoolFlag{Name: "active, a", Usage: "Show only active session"},
|
|
cli.BoolFlag{Name: "quiet, q", Usage: "Only display IDs"},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
var sessions []*dbmodels.Session
|
|
|
|
limit, offset, status := 60000, -1, []string{string(dbmodels.SessionStatusActive), string(dbmodels.SessionStatusClosed), string(dbmodels.SessionStatusUnknown)}
|
|
if c.Bool("active") {
|
|
status = status[:1]
|
|
}
|
|
|
|
query := db.Order("created_at desc").Limit(limit).Offset(offset).Where("status in (?)", status).Preload("User").Preload("Host")
|
|
|
|
if c.Bool("latest") {
|
|
var session dbmodels.Session
|
|
if err := query.First(&session).Error; err != nil {
|
|
return err
|
|
}
|
|
sessions = append(sessions, &session)
|
|
} else {
|
|
if err := query.Find(&sessions).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
factor := 1
|
|
for len(sessions) >= limit*factor {
|
|
var additionnalSessions []*dbmodels.Session
|
|
|
|
offset = limit * factor
|
|
query := db.Order("created_at desc").Limit(limit).Offset(offset).Where("status in (?)", status).Preload("User").Preload("Host")
|
|
if err := query.Find(&additionnalSessions).Error; err != nil {
|
|
return err
|
|
}
|
|
sessions = append(sessions, additionnalSessions...)
|
|
factor++
|
|
}
|
|
}
|
|
if c.Bool("quiet") {
|
|
for _, session := range sessions {
|
|
fmt.Fprintln(s, session.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
table := tablewriter.NewWriter(s)
|
|
table.SetHeader([]string{"ID", "User", "Host", "Status", "Start", "Duration", "Error", "Comment"})
|
|
table.SetBorder(false)
|
|
table.SetCaption(true, fmt.Sprintf("Total: %d sessions.", len(sessions)))
|
|
for _, session := range sessions {
|
|
var duration string
|
|
if session.StoppedAt == nil || session.StoppedAt.IsZero() {
|
|
duration = humanize.RelTime(session.CreatedAt, time.Now(), "", "")
|
|
} else {
|
|
duration = humanize.RelTime(session.CreatedAt, *session.StoppedAt, "", "")
|
|
}
|
|
duration = strings.Replace(duration, "now", "1 second", 1)
|
|
hostname := naMessage
|
|
if session.Host != nil {
|
|
hostname = session.Host.Name
|
|
}
|
|
username := naMessage
|
|
if session.User != nil {
|
|
username = session.User.Name
|
|
}
|
|
table.Append([]string{
|
|
fmt.Sprintf("%d", session.ID),
|
|
username,
|
|
hostname,
|
|
session.Status,
|
|
humanize.Time(session.CreatedAt),
|
|
duration,
|
|
wrapText(session.ErrMsg, 30),
|
|
session.Comment,
|
|
})
|
|
}
|
|
table.Render()
|
|
return nil
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
Name: "version",
|
|
Usage: "Shows the SSHPortal version information",
|
|
Action: func(c *cli.Context) error {
|
|
fmt.Fprintf(s, "%s\n", version)
|
|
return nil
|
|
},
|
|
}, {
|
|
Name: "exit",
|
|
Usage: "Exit",
|
|
Action: func(c *cli.Context) error {
|
|
return cli.NewExitError("", 0)
|
|
},
|
|
},
|
|
}
|
|
|
|
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 {
|
|
fmt.Fprint(s, "syntax error.\n")
|
|
continue
|
|
}
|
|
if len(words) == 1 && strings.ToLower(words[0]) == "exit" {
|
|
return s.Exit(0)
|
|
}
|
|
if len(words) == 0 {
|
|
continue
|
|
}
|
|
dbmodels.NewEvent("shell", words[0]).SetAuthor(myself).SetArg("interactive", true).SetArg("args", words[1:]).Log(db)
|
|
if err := app.Run(append([]string{"config"}, words...)); err != nil {
|
|
if cliErr, ok := err.(*cli.ExitError); ok {
|
|
if cliErr.ExitCode() != 0 {
|
|
fmt.Fprintf(s, "error: %v\n", err)
|
|
}
|
|
// s.Exit(cliErr.ExitCode())
|
|
} else {
|
|
fmt.Fprintf(s, "error: %v\n", err)
|
|
}
|
|
}
|
|
}
|
|
} else { // oneshot mode
|
|
dbmodels.NewEvent("shell", sshCommand[0]).SetAuthor(myself).SetArg("interactive", false).SetArg("args", sshCommand[1:]).Log(db)
|
|
if err := app.Run(append([]string{"config"}, sshCommand...)); err != nil {
|
|
if errMsg := err.Error(); errMsg != "" {
|
|
fmt.Fprintf(s, "error: %s\n", errMsg)
|
|
}
|
|
if cliErr, ok := err.(*cli.ExitError); ok {
|
|
return s.Exit(cliErr.ExitCode())
|
|
}
|
|
return s.Exit(1)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func wrapText(in string, length int) string {
|
|
if len(in) <= length {
|
|
return in
|
|
}
|
|
return in[0:length-3] + "..."
|
|
}
|
|
|
|
func parseInputURL(input string) (*url.URL, error) {
|
|
if !strings.Contains(input, "://") {
|
|
input = "ssh://" + input
|
|
}
|
|
u, err := url.Parse(input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
func parseOptionalTime(input string) (*time.Time, error) {
|
|
if input != "" {
|
|
parsed, err := time.ParseInLocation("2006-01-02 15:04", input, time.Local)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &parsed, nil
|
|
}
|
|
return nil, nil
|
|
}
|