mirror of
https://github.com/moul/sshportal.git
synced 2024-11-15 12:46:17 +08:00
356 lines
9.6 KiB
Go
356 lines
9.6 KiB
Go
//go:generate stringer -type=SessionStatus
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/asaskevich/govalidator"
|
|
"github.com/gliderlabs/ssh"
|
|
"github.com/jinzhu/gorm"
|
|
)
|
|
|
|
type Config struct {
|
|
SSHKeys []*SSHKey `json:"keys"`
|
|
Hosts []*Host `json:"hosts"`
|
|
UserKeys []*UserKey `json:"user_keys"`
|
|
Users []*User `json:"users"`
|
|
UserGroups []*UserGroup `json:"user_groups"`
|
|
HostGroups []*HostGroup `json:"host_groups"`
|
|
ACLs []*ACL `json:"acls"`
|
|
Settings []*Setting `json:"settings"`
|
|
Events []*Event `json:"events"`
|
|
Sessions []*Session `json:"sessions"`
|
|
// FIXME: add latest migration
|
|
Date time.Time `json:"date"`
|
|
}
|
|
|
|
type Setting struct {
|
|
gorm.Model
|
|
Name string `valid:"required"`
|
|
Value string `valid:"required"`
|
|
}
|
|
|
|
// SSHKey defines a ssh client key (used by sshportal to connect to remote hosts)
|
|
type SSHKey struct {
|
|
// FIXME: use uuid for ID
|
|
gorm.Model
|
|
Name string `valid:"required,length(1|32),unix_user"`
|
|
Type string `valid:"required"`
|
|
Length uint `valid:"required"`
|
|
Fingerprint string `valid:"optional"`
|
|
PrivKey string `sql:"size:10000" valid:"required"`
|
|
PubKey string `sql:"size:10000" valid:"optional"`
|
|
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
|
|
Comment string `valid:"optional"`
|
|
}
|
|
|
|
type Host struct {
|
|
// FIXME: use uuid for ID
|
|
gorm.Model
|
|
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
|
|
Addr string `valid:"required"`
|
|
User string `valid:"optional"`
|
|
Password string `valid:"optional"`
|
|
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
|
|
SSHKeyID uint `gorm:"index"`
|
|
HostKey []byte `sql:"size:10000" valid:"optional"`
|
|
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
|
Comment string `valid:"optional"`
|
|
}
|
|
|
|
// UserKey defines a user public key used by sshportal to identify the user
|
|
type UserKey struct {
|
|
gorm.Model
|
|
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
|
|
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
|
|
UserID uint ``
|
|
User *User `gorm:"ForeignKey:UserID"`
|
|
Comment string `valid:"optional"`
|
|
}
|
|
|
|
type UserRole struct {
|
|
gorm.Model
|
|
Name string `valid:"required,length(1|32),unix_user"`
|
|
Users []*User `gorm:"many2many:user_user_roles"`
|
|
}
|
|
|
|
type User struct {
|
|
// FIXME: use uuid for ID
|
|
gorm.Model
|
|
Roles []*UserRole `gorm:"many2many:user_user_roles"`
|
|
Email string `valid:"required,email"`
|
|
Name string `valid:"required,length(1|32),unix_user"`
|
|
Keys []*UserKey `gorm:"ForeignKey:UserID"`
|
|
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
|
|
Comment string `valid:"optional"`
|
|
InviteToken string `valid:"optional,length(10|60)"`
|
|
}
|
|
|
|
type UserGroup struct {
|
|
gorm.Model
|
|
Name string `valid:"required,length(1|32),unix_user"`
|
|
Users []*User `gorm:"many2many:user_user_groups;"`
|
|
ACLs []*ACL `gorm:"many2many:user_group_acls;"`
|
|
Comment string `valid:"optional"`
|
|
}
|
|
|
|
type HostGroup struct {
|
|
gorm.Model
|
|
Name string `valid:"required,length(1|32),unix_user"`
|
|
Hosts []*Host `gorm:"many2many:host_host_groups;"`
|
|
ACLs []*ACL `gorm:"many2many:host_group_acls;"`
|
|
Comment string `valid:"optional"`
|
|
}
|
|
|
|
type ACL struct {
|
|
gorm.Model
|
|
HostGroups []*HostGroup `gorm:"many2many:host_group_acls;"`
|
|
UserGroups []*UserGroup `gorm:"many2many:user_group_acls;"`
|
|
HostPattern string `valid:"optional"`
|
|
Action string `valid:"required"`
|
|
Weight uint ``
|
|
Comment string `valid:"optional"`
|
|
}
|
|
|
|
type Session struct {
|
|
gorm.Model
|
|
StoppedAt *time.Time `sql:"index" valid:"optional"`
|
|
Status string `valid:"required"`
|
|
User *User `gorm:"ForeignKey:UserID"`
|
|
Host *Host `gorm:"ForeignKey:HostID"`
|
|
UserID uint `valid:"optional"`
|
|
HostID uint `valid:"optional"`
|
|
ErrMsg string `valid:"optional"`
|
|
Comment string `valid:"optional"`
|
|
}
|
|
|
|
type Event struct {
|
|
gorm.Model
|
|
Author *User `gorm:"ForeignKey:AuthorID"`
|
|
AuthorID uint `valid:"optional"`
|
|
Domain string `valid:"required"`
|
|
Action string `valid:"required"`
|
|
Entity string `valid:"optional"`
|
|
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)" json:"-"`
|
|
ArgsMap map[string]interface{} `gorm:"-" json:"Args"`
|
|
}
|
|
|
|
type SessionStatus string
|
|
|
|
const (
|
|
SessionStatusUnknown SessionStatus = "unknown"
|
|
SessionStatusActive = "active"
|
|
SessionStatusClosed = "closed"
|
|
)
|
|
|
|
type ACLAction string
|
|
|
|
const (
|
|
ACLActionUnknown ACLAction = "unknown"
|
|
ACLActionAllow = "allow"
|
|
ACLActionDeny = "deny"
|
|
)
|
|
|
|
func init() {
|
|
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
|
|
|
|
govalidator.CustomTypeTagMap.Set("unix_user", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
|
name, ok := i.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return unixUserRegexp.MatchString(name)
|
|
}))
|
|
}
|
|
|
|
func RemoteHostFromSession(s ssh.Session, db *gorm.DB) (*Host, error) {
|
|
var host Host
|
|
db.Preload("SSHKey").Where("name = ?", s.User()).Find(&host)
|
|
if host.Name == "" {
|
|
// FIXME: add available hosts
|
|
return nil, fmt.Errorf("No such target: %q", s.User())
|
|
}
|
|
return &host, nil
|
|
}
|
|
|
|
func (host *Host) URL() string {
|
|
return fmt.Sprintf("%s@%s", host.User, host.Addr)
|
|
}
|
|
|
|
func NewHostFromURL(rawurl string) (*Host, error) {
|
|
if !strings.Contains(rawurl, "://") {
|
|
rawurl = "ssh://" + rawurl
|
|
}
|
|
u, err := url.Parse(rawurl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
host := Host{Addr: u.Host}
|
|
if !strings.Contains(host.Addr, ":") {
|
|
host.Addr += ":22" // add port if not present
|
|
}
|
|
host.User = "root" // default username
|
|
if u.User != nil {
|
|
password, _ := u.User.Password()
|
|
host.Password = password
|
|
host.User = u.User.Username()
|
|
}
|
|
return &host, nil
|
|
}
|
|
|
|
func (host *Host) Hostname() string {
|
|
return strings.Split(host.Addr, ":")[0]
|
|
}
|
|
|
|
// Host helpers
|
|
|
|
func HostsPreload(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("Groups").Preload("SSHKey")
|
|
}
|
|
func HostsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
|
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
|
}
|
|
|
|
// SSHKey helpers
|
|
|
|
func SSHKeysPreload(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("Hosts")
|
|
}
|
|
func SSHKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
|
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
|
}
|
|
|
|
// HostGroup helpers
|
|
|
|
func HostGroupsPreload(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("ACLs").Preload("Hosts")
|
|
}
|
|
func HostGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
|
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
|
}
|
|
|
|
// UserGroup helpers
|
|
|
|
func UserGroupsPreload(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("ACLs").Preload("Users")
|
|
}
|
|
func UserGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
|
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
|
}
|
|
|
|
// User helpers
|
|
|
|
func UsersPreload(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("Groups").Preload("Keys").Preload("Roles")
|
|
}
|
|
func UsersByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
|
return db.Where("id IN (?)", identifiers).Or("email IN (?)", identifiers).Or("name IN (?)", identifiers)
|
|
}
|
|
func UserHasRole(user User, name string) bool {
|
|
for _, role := range user.Roles {
|
|
if role.Name == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
func UserCheckRoles(user User, names []string) error {
|
|
ok := false
|
|
for _, name := range names {
|
|
if UserHasRole(user, name) {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
if ok {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("you don't have permission to access this feature (requires any of these roles: '%s')", strings.Join(names, "', '"))
|
|
}
|
|
|
|
// ACL helpers
|
|
|
|
func ACLsPreload(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("UserGroups").Preload("HostGroups")
|
|
}
|
|
func ACLsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
|
return db.Where("id IN (?)", identifiers)
|
|
}
|
|
|
|
// UserKey helpers
|
|
|
|
func UserKeysPreload(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("User")
|
|
}
|
|
func UserKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
|
return db.Where("id IN (?)", identifiers)
|
|
}
|
|
|
|
// UserRole helpers
|
|
|
|
func UserRolesPreload(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("Users")
|
|
}
|
|
func UserRolesByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
|
return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
|
|
}
|
|
|
|
// Session helpers
|
|
|
|
func SessionsPreload(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("User").Preload("Host")
|
|
}
|
|
func SessionsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
|
return db.Where("id IN (?)", identifiers)
|
|
}
|
|
|
|
// Events helpers
|
|
|
|
func EventsPreload(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("Author")
|
|
}
|
|
func EventsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
|
|
return db.Where("id IN (?)", identifiers)
|
|
}
|
|
|
|
func NewEvent(domain, action string) *Event {
|
|
return &Event{
|
|
Domain: domain,
|
|
Action: action,
|
|
ArgsMap: map[string]interface{}{},
|
|
}
|
|
}
|
|
|
|
func (e *Event) String() string {
|
|
return fmt.Sprintf("%s %s %s %s", e.Domain, e.Action, e.Entity, string(e.Args))
|
|
}
|
|
|
|
func (e *Event) Log(db *gorm.DB) {
|
|
if len(e.ArgsMap) > 0 {
|
|
var err error
|
|
if e.Args, err = json.Marshal(e.ArgsMap); err != nil {
|
|
log.Printf("error: %v", err)
|
|
}
|
|
}
|
|
log.Printf("info: %s", e)
|
|
if err := db.Create(e).Error; err != nil {
|
|
log.Printf("warning: %v", err)
|
|
}
|
|
}
|
|
|
|
func (e *Event) SetAuthor(user *User) *Event {
|
|
e.Author = user
|
|
e.AuthorID = user.ID
|
|
return e
|
|
}
|
|
|
|
func (e *Event) SetArg(name string, value interface{}) *Event {
|
|
e.ArgsMap[name] = value
|
|
return e
|
|
}
|