Support assign multiple groups to hosts and users (#2)

This commit is contained in:
Manfred Touron 2017-11-15 09:52:59 +01:00
parent d6a7a6702f
commit f97c9f2878
5 changed files with 77 additions and 69 deletions

View file

@ -2,7 +2,7 @@
## master (unreleased) ## master (unreleased)
* No entry * Support adding multiple `--group` links on `host create` and `user create`
## v1.1.0 (2017-11-15) ## v1.1.0 (2017-11-15)

View file

@ -140,9 +140,9 @@ You can enter in interactive mode using this syntax: `ssh admin@portal.example.o
# acl management # acl management
acl help acl help
acl create [-h] [--hostgroup=<value>...] [--usergroup=<value>...] [--pattern=<value>] [--comment=<value>] [--action=<value>] [--weight=value] acl create [-h] [--hostgroup=<value>...] [--usergroup=<value>...] [--pattern=<value>] [--comment=<value>] [--action=<value>] [--weight=value]
acl inspect [-h] <id> [<id> [<id>...]] acl inspect [-h] <id>...
acl ls [-h] acl ls [-h]
acl rm [-h] <id> [<id> [<id>...]] acl rm [-h] <id>...
# config management # config management
config help config help
@ -151,38 +151,38 @@ config restore [-h] [--confirm]
# host management # host management
host help host help
host create [-h] [--name=<value>] [--password=<value>] [--fingerprint=<value>] [--comment=<value>] [--key=<value>] [--group=<value>] <user>[:<password>]@<host>[:<port>] host create [-h] [--name=<value>] [--password=<value>] [--fingerprint=<value>] [--comment=<value>] [--key=<value>] [--group=<value>...] <user>[:<password>]@<host>[:<port>]
host inspect [-h] <id or name> [<id or name> [<id or name>...]] host inspect [-h] <id or name>...
host ls [-h] host ls [-h]
host rm [-h] <id or name> [<id or name> [<id or name>...]] host rm [-h] <id or name>...
# hostgroup management # hostgroup management
hostgroup help hostgroup help
hostgroup create [-h] [--name=<value>] [--comment=<value>] hostgroup create [-h] [--name=<value>] [--comment=<value>]
hostgroup inspect [-h] <id or name> [<id or name> [<id or name>...]] hostgroup inspect [-h] <id or name>...
hostgroup ls [-h] hostgroup ls [-h]
hostgroup rm [-h] <id or name> [<id or name> [<id or name>...]] hostgroup rm [-h] <id or name>...
# key management # key management
key help key help
key create [-h] [--name=<value>] [--type=<value>] [--length=<value>] [--comment=<value>] key create [-h] [--name=<value>] [--type=<value>] [--length=<value>] [--comment=<value>]
key inspect [-h] <id or name> [<id or name> [<id or name>...]] key inspect [-h] <id or name>...
key ls [-h] key ls [-h]
key rm [-h] <id or name> [<id or name> [<id or name>...]] key rm [-h] <id or name>...
# user management # user management
user help user help
user invite [-h] [--name=<value>] [--comment=<value>] [--group=<value>] <email> user invite [-h] [--name=<value>] [--comment=<value>] [--group=<value>...] <email>
user inspect [-h] <id or email> [<id or email> [<id or email>...]] user inspect [-h] <id or email>...
user ls [-h] user ls [-h]
user rm [-h] <id or email> [<id or email> [<id or email>...]] user rm [-h] <id or email>...
# usergroup management # usergroup management
usergroup help usergroup help
hostgroup create [-h] [--name=<value>] [--comment=<value>] hostgroup create [-h] [--name=<value>] [--comment=<value>]
usergroup inspect [-h] <id or name> [<id or name> [<id or name>...]] usergroup inspect [-h] <id or name>...
usergroup ls [-h] usergroup ls [-h]
usergroup rm [-h] <id or name> [<id or name> [<id or name>...]] usergroup rm [-h] <id or name>...
# other # other
exit [-h] exit [-h]

6
acl.go
View file

@ -2,7 +2,7 @@ package main
import "sort" import "sort"
type ByWeight []ACL type ByWeight []*ACL
func (a ByWeight) Len() int { return len(a) } func (a ByWeight) Len() int { return len(a) }
func (a ByWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
@ -10,7 +10,7 @@ func (a ByWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
func CheckACLs(user User, host Host) (string, error) { func CheckACLs(user User, host Host) (string, error) {
// shared ACLs between user and host // shared ACLs between user and host
aclMap := map[uint]ACL{} aclMap := map[uint]*ACL{}
for _, userGroup := range user.Groups { for _, userGroup := range user.Groups {
for _, userGroupACL := range userGroup.ACLs { for _, userGroupACL := range userGroup.ACLs {
for _, hostGroup := range host.Groups { for _, hostGroup := range host.Groups {
@ -30,7 +30,7 @@ func CheckACLs(user User, host Host) (string, error) {
} }
// transofrm map to slice and sort it // transofrm map to slice and sort it
acls := []ACL{} acls := []*ACL{}
for _, acl := range aclMap { for _, acl := range aclMap {
acls = append(acls, acl) acls = append(acls, acl)
} }

64
db.go
View file

@ -12,14 +12,14 @@ import (
) )
type Config struct { type Config struct {
SSHKeys []SSHKey `json:"keys"` SSHKeys []*SSHKey `json:"keys"`
Hosts []Host `json:"hosts"` Hosts []*Host `json:"hosts"`
UserKeys []UserKey `json:"user_keys"` UserKeys []*UserKey `json:"user_keys"`
Users []User `json:"users"` Users []*User `json:"users"`
UserGroups []UserGroup `json:"user_groups"` UserGroups []*UserGroup `json:"user_groups"`
HostGroups []HostGroup `json:"host_groups"` HostGroups []*HostGroup `json:"host_groups"`
ACLs []ACL `json:"acls"` ACLs []*ACL `json:"acls"`
Date time.Time `json:"date"` Date time.Time `json:"date"`
} }
type SSHKey struct { type SSHKey struct {
@ -29,9 +29,9 @@ type SSHKey struct {
Type string Type string
Length uint Length uint
Fingerprint string Fingerprint string
PrivKey string `sql:"size:10000;"` PrivKey string `sql:"size:10000;"`
PubKey string `sql:"size:10000;"` PubKey string `sql:"size:10000;"`
Hosts []Host Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
Comment string Comment string
} }
@ -42,10 +42,10 @@ type Host struct {
Addr string Addr string
User string User string
Password string Password string
SSHKey *SSHKey SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"` SSHKeyID uint `gorm:"index"`
Groups []HostGroup `gorm:"many2many:host_host_groups;"` Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string // FIXME: replace with hostkey ? Fingerprint string // FIXME: replace with hostkey ?
Comment string Comment string
} }
@ -53,7 +53,7 @@ type UserKey struct {
gorm.Model gorm.Model
Key []byte `sql:"size:10000;"` Key []byte `sql:"size:10000;"`
UserID uint UserID uint
User *User User *User `gorm:"ForeignKey:UserID"`
Comment string Comment string
} }
@ -61,10 +61,10 @@ type User struct {
// FIXME: use uuid for ID // FIXME: use uuid for ID
gorm.Model gorm.Model
IsAdmin bool IsAdmin bool
Email string // FIXME: govalidator: email Email string // FIXME: govalidator: email
Name string // FIXME: govalidator: min length 3, alphanum Name string // FIXME: govalidator: min length 3, alphanum
Keys []UserKey Keys []*UserKey `gorm:"ForeignKey:UserID"`
Groups []UserGroup `gorm:"many2many:user_user_groups;"` Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
Comment string Comment string
InviteToken string InviteToken string
} }
@ -72,23 +72,23 @@ type User struct {
type UserGroup struct { type UserGroup struct {
gorm.Model gorm.Model
Name string Name string
Users []User `gorm:"many2many:user_user_groups;"` Users []*User `gorm:"many2many:user_user_groups;"`
ACLs []ACL `gorm:"many2many:user_group_acls;"` ACLs []*ACL `gorm:"many2many:user_group_acls;"`
Comment string Comment string
} }
type HostGroup struct { type HostGroup struct {
gorm.Model gorm.Model
Name string Name string
Hosts []Host `gorm:"many2many:host_host_groups;"` Hosts []*Host `gorm:"many2many:host_host_groups;"`
ACLs []ACL `gorm:"many2many:host_group_acls;"` ACLs []*ACL `gorm:"many2many:host_group_acls;"`
Comment string Comment string
} }
type ACL struct { type ACL struct {
gorm.Model gorm.Model
HostGroups []HostGroup `gorm:"many2many:host_group_acls;"` HostGroups []*HostGroup `gorm:"many2many:host_group_acls;"`
UserGroups []UserGroup `gorm:"many2many:user_group_acls;"` UserGroups []*UserGroup `gorm:"many2many:user_group_acls;"`
HostPattern string HostPattern string
Action string Action string
Weight uint Weight uint
@ -165,8 +165,8 @@ func dbInit(db *gorm.DB) error {
var defaultHostGroup HostGroup var defaultHostGroup HostGroup
db.Where("name = ?", "default").First(&defaultHostGroup) db.Where("name = ?", "default").First(&defaultHostGroup)
acl := ACL{ acl := ACL{
UserGroups: []UserGroup{defaultUserGroup}, UserGroups: []*UserGroup{&defaultUserGroup},
HostGroups: []HostGroup{defaultHostGroup}, HostGroups: []*HostGroup{&defaultHostGroup},
Action: "allow", Action: "allow",
//HostPattern: "", //HostPattern: "",
//Weight: 0, //Weight: 0,
@ -189,7 +189,7 @@ func dbInit(db *gorm.DB) error {
Comment: "created by sshportal", Comment: "created by sshportal",
IsAdmin: true, IsAdmin: true,
InviteToken: RandStringBytes(16), InviteToken: RandStringBytes(16),
Groups: []UserGroup{defaultUserGroup}, Groups: []*UserGroup{&defaultUserGroup},
} }
db.Create(&user) db.Create(&user)
log.Printf("Admin user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken) log.Printf("Admin user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken)
@ -225,9 +225,9 @@ func dbDemo(db *gorm.DB) error {
} }
var ( var (
host1 = Host{Name: "sdf", Addr: "sdf.org:22", User: "new", SSHKeyID: key.ID, Groups: []HostGroup{*hostGroup}} host1 = Host{Name: "sdf", Addr: "sdf.org:22", User: "new", SSHKeyID: key.ID, Groups: []*HostGroup{hostGroup}}
host2 = Host{Name: "whoami", Addr: "whoami.filippo.io:22", User: "test", SSHKeyID: key.ID, Groups: []HostGroup{*hostGroup}} host2 = Host{Name: "whoami", Addr: "whoami.filippo.io:22", User: "test", SSHKeyID: key.ID, Groups: []*HostGroup{hostGroup}}
host3 = Host{Name: "ssh-chat", Addr: "chat.shazow.net:22", User: "test", SSHKeyID: key.ID, Fingerprint: "MD5:e5:d5:d1:75:90:38:42:f6:c7:03:d7:d0:56:7d:6a:db", Groups: []HostGroup{*hostGroup}} host3 = Host{Name: "ssh-chat", Addr: "chat.shazow.net:22", User: "test", SSHKeyID: key.ID, Fingerprint: "MD5:e5:d5:d1:75:90:38:42:f6:c7:03:d7:d0:56:7d:6a:db", Groups: []*HostGroup{hostGroup}}
) )
// FIXME: check if hosts exist to avoid `UNIQUE constraint` error // FIXME: check if hosts exist to avoid `UNIQUE constraint` error

View file

@ -75,8 +75,8 @@ GLOBAL OPTIONS:
acl := ACL{ acl := ACL{
Comment: c.String("comment"), Comment: c.String("comment"),
HostPattern: c.String("pattern"), HostPattern: c.String("pattern"),
UserGroups: []UserGroup{}, UserGroups: []*UserGroup{},
HostGroups: []HostGroup{}, HostGroups: []*HostGroup{},
Weight: c.Uint("weight"), Weight: c.Uint("weight"),
Action: c.String("action"), Action: c.String("action"),
} }
@ -89,14 +89,14 @@ GLOBAL OPTIONS:
if err != nil { if err != nil {
return fmt.Errorf("unknown user group %q: %v", name, err) return fmt.Errorf("unknown user group %q: %v", name, err)
} }
acl.UserGroups = append(acl.UserGroups, *userGroup) acl.UserGroups = append(acl.UserGroups, userGroup)
} }
for _, name := range c.StringSlice("hostgroup") { for _, name := range c.StringSlice("hostgroup") {
hostGroup, err := FindHostGroupByIdOrName(db, name) hostGroup, err := FindHostGroupByIdOrName(db, name)
if err != nil { if err != nil {
return fmt.Errorf("unknown host group %q: %v", name, err) return fmt.Errorf("unknown host group %q: %v", name, err)
} }
acl.HostGroups = append(acl.HostGroups, *hostGroup) acl.HostGroups = append(acl.HostGroups, hostGroup)
} }
if len(acl.UserGroups) == 0 { if len(acl.UserGroups) == 0 {
@ -330,12 +330,12 @@ GLOBAL OPTIONS:
ArgsUsage: "<user>[:<password>]@<host>[:<port>]", ArgsUsage: "<user>[:<password>]@<host>[:<port>]",
Description: "$> host create bart@foo.org\n $> host create bob:marley@example.com:2222", Description: "$> host create bart@foo.org\n $> host create bob:marley@example.com:2222",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{Name: "name", Usage: "Assigns a name to the host"}, cli.StringFlag{Name: "name, n", Usage: "Assigns a name to the host"},
cli.StringFlag{Name: "password", Usage: "If present, sshportal will use password-based authentication"}, cli.StringFlag{Name: "password, p", Usage: "If present, sshportal will use password-based authentication"},
cli.StringFlag{Name: "fingerprint", Usage: "SSH host key fingerprint"}, cli.StringFlag{Name: "fingerprint, f", Usage: "SSH host key fingerprint"},
cli.StringFlag{Name: "comment"}, cli.StringFlag{Name: "comment, c"},
cli.StringFlag{Name: "key", Usage: "ID or name of the key to use for authentication"}, cli.StringFlag{Name: "key, k", Usage: "ID or name of the key to use for authentication"},
cli.StringFlag{Name: "group", Usage: "Name or ID of the host group", Value: "default"}, cli.StringSliceFlag{Name: "group, g", Usage: "Names or IDs of host groups (default: \"default\")"},
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if c.NArg() != 1 { if c.NArg() != 1 {
@ -373,11 +373,15 @@ GLOBAL OPTIONS:
} }
// host group // host group
hostGroup, err := FindHostGroupByIdOrName(db, c.String("group")) inputGroups := c.StringSlice("group")
if len(inputGroups) == 0 {
inputGroups = []string{"default"}
}
hostGroups, err := FindHostGroupsByIdOrName(db, inputGroups)
if err != nil { if err != nil {
return err return err
} }
host.Groups = []HostGroup{*hostGroup} host.Groups = hostGroups
if err := db.Create(&host).Error; err != nil { if err := db.Create(&host).Error; err != nil {
return err return err
@ -407,7 +411,7 @@ GLOBAL OPTIONS:
Name: "ls", Name: "ls",
Usage: "Lists hosts", Usage: "Lists hosts",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
var hosts []Host var hosts []*Host
if err := db.Preload("Groups").Find(&hosts).Error; err != nil { if err := db.Preload("Groups").Find(&hosts).Error; err != nil {
return err return err
} }
@ -515,7 +519,7 @@ GLOBAL OPTIONS:
Name: "ls", Name: "ls",
Usage: "Lists host groups", Usage: "Lists host groups",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
var hostGroups []HostGroup var hostGroups []*HostGroup
if err := db.Preload("ACLs").Preload("Hosts").Find(&hostGroups).Error; err != nil { if err := db.Preload("ACLs").Preload("Hosts").Find(&hostGroups).Error; err != nil {
return err return err
} }
@ -727,7 +731,7 @@ GLOBAL OPTIONS:
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{Name: "name", Usage: "Assigns a name to the user"}, cli.StringFlag{Name: "name", Usage: "Assigns a name to the user"},
cli.StringFlag{Name: "comment"}, cli.StringFlag{Name: "comment"},
cli.StringFlag{Name: "group", Usage: "Name or ID of the user group", Value: "default"}, cli.StringSliceFlag{Name: "group, g", Usage: "Names or IDs of user groups (default: \"default\")"},
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if c.NArg() != 1 { if c.NArg() != 1 {
@ -750,11 +754,15 @@ GLOBAL OPTIONS:
} }
// user group // user group
userGroup, err := FindUserGroupByIdOrName(db, c.String("group")) inputGroups := c.StringSlice("group")
if len(inputGroups) == 0 {
inputGroups = []string{"default"}
}
userGroups, err := FindUserGroupsByIdOrName(db, inputGroups)
if err != nil { if err != nil {
return err return err
} }
user.Groups = []UserGroup{*userGroup} user.Groups = userGroups
// save the user in database // save the user in database
if err := db.Create(&user).Error; err != nil { if err := db.Create(&user).Error; err != nil {
@ -840,7 +848,7 @@ GLOBAL OPTIONS:
// add myself to the new group // add myself to the new group
myself := s.Context().Value(userContextKey).(User) myself := s.Context().Value(userContextKey).(User)
// FIXME: use foreign key with ID to avoid updating the user with the context // FIXME: use foreign key with ID to avoid updating the user with the context
userGroup.Users = []User{myself} userGroup.Users = []*User{&myself}
if err := db.Create(&userGroup).Error; err != nil { if err := db.Create(&userGroup).Error; err != nil {
return err return err
@ -870,7 +878,7 @@ GLOBAL OPTIONS:
Name: "ls", Name: "ls",
Usage: "Lists user groups", Usage: "Lists user groups",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
var userGroups []UserGroup var userGroups []*UserGroup
if err := db.Preload("ACLs").Preload("Users").Find(&userGroups).Error; err != nil { if err := db.Preload("ACLs").Preload("Users").Find(&userGroups).Error; err != nil {
return err return err
} }