From 9cd9152a91821840e447dd91679423b9b1b13feb Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 23 Nov 2017 16:22:23 +0100 Subject: [PATCH] Switch from IsAdmin boolean to Roles --- Makefile | 2 +- db.go | 28 +++++++++++++++-- dbinit.go | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- main.go | 4 +-- shell.go | 51 +++++++++++++++--------------- 5 files changed, 145 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index abf1a37..8db5e48 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ _docker_install: .PHONY: dev dev: -go get github.com/githubnemo/CompileDaemon - CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --demo --debug --port=$(PORT)" . + CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --demo --debug --bind-address=:$(PORT)" . .PHONY: test test: diff --git a/db.go b/db.go index 2130888..3654db2 100644 --- a/db.go +++ b/db.go @@ -65,10 +65,16 @@ type UserKey struct { 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 - IsAdmin bool + 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"` @@ -188,10 +194,18 @@ func UserGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB { // User helpers func UsersPreload(db *gorm.DB) *gorm.DB { - return db.Preload("Groups").Preload("Keys") + 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) + 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 } // ACL helpers @@ -209,3 +223,11 @@ func UserKeysPreload(db *gorm.DB) *gorm.DB { 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) +} diff --git a/dbinit.go b/dbinit.go index b5607f0..75b4d1a 100644 --- a/dbinit.go +++ b/dbinit.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log" "os" @@ -201,6 +202,88 @@ func dbInit(db *gorm.DB) error { Rollback: func(tx *gorm.DB) error { return db.Model(&HostGroup{}).RemoveIndex("uix_hostgroups_name").Error }, + }, { + ID: "15", + Migrate: func(tx *gorm.DB) error { + type UserRole struct { + gorm.Model + Name string `valid:"required,length(1|32),unix_user"` + Users []*User `gorm:"many2many:user_user_roles"` + } + return tx.AutoMigrate(&UserRole{}).Error + }, + Rollback: func(tx *gorm.DB) error { + return tx.DropTable("user_roles").Error + }, + }, { + ID: "16", + Migrate: func(tx *gorm.DB) error { + type User struct { + gorm.Model + IsAdmin bool + 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)"` + } + return tx.AutoMigrate(&User{}).Error + }, + Rollback: func(tx *gorm.DB) error { + return fmt.Errorf("not implemented") + }, + }, { + ID: "17", + Migrate: func(tx *gorm.DB) error { + return tx.Create(&UserRole{Name: "admin"}).Error + }, + Rollback: func(tx *gorm.DB) error { + return tx.Where("name = ?", "admin").Delete(&UserRole{}).Error + }, + }, { + ID: "18", + Migrate: func(tx *gorm.DB) error { + var adminRole UserRole + if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil { + return err + } + + var users []User + if err := db.Preload("Roles").Where("is_admin = ?", true).Find(&users).Error; err != nil { + return err + } + + for _, user := range users { + user.Roles = append(user.Roles, &adminRole) + if err := tx.Save(&user).Error; err != nil { + return err + } + } + return nil + }, + Rollback: func(tx *gorm.DB) error { + return fmt.Errorf("not implemented") + }, + }, { + ID: "19", + Migrate: func(tx *gorm.DB) error { + type User struct { + 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)"` + } + return tx.AutoMigrate(&User{}).Error + }, + Rollback: func(tx *gorm.DB) error { + return fmt.Errorf("not implemented") + }, }, }) if err := m.Migrate(); err != nil { @@ -284,15 +367,21 @@ func dbInit(db *gorm.DB) error { if os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") != "" { inviteToken = os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") } + var adminRole UserRole + if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil { + return err + } user := User{ Name: "Administrator", Email: "admin@sshportal", Comment: "created by sshportal", - IsAdmin: true, + Roles: []*UserRole{&adminRole}, InviteToken: inviteToken, Groups: []*UserGroup{&defaultUserGroup}, } - db.Create(&user) + if err := db.Create(&user).Error; err != nil { + return err + } log.Printf("Admin user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken) } diff --git a/main.go b/main.go index 9df20bb..b73a32c 100644 --- a/main.go +++ b/main.go @@ -120,7 +120,7 @@ func server(c *cli.Context) error { switch username := s.User(); { case username == c.String("config-user"): - if !currentUser.IsAdmin { + if !UserHasRole(currentUser, "admin") { fmt.Fprintf(s, "You are not an administrator, permission denied.\n") return } @@ -181,7 +181,7 @@ func server(c *cli.Context) error { // lookup user by key db.Where("key = ?", key.Marshal()).First(&userKey) if userKey.UserID > 0 { - db.Where("id = ?", userKey.UserID).First(&user) + db.Preload("Roles").Where("id = ?", userKey.UserID).First(&user) if strings.HasPrefix(username, "invite:") { ctx.SetValue(errorContextKey, fmt.Errorf("invites are only supported for ney SSH keys; your ssh key is already associated with the user %q.", user.Email)) } diff --git a/shell.go b/shell.go index 851bbe3..fa1ccb6 100644 --- a/shell.go +++ b/shell.go @@ -897,11 +897,11 @@ GLOBAL OPTIONS: Usage: "Lists users", Action: func(c *cli.Context) error { var users []User - if err := db.Preload("Groups").Preload("Keys").Find(&users).Error; err != nil { + if err := db.Preload("Groups").Preload("Roles").Preload("Keys").Find(&users).Error; err != nil { return err } table := tablewriter.NewWriter(s) - table.SetHeader([]string{"ID", "Name", "Email", "Admin", "Keys", "Groups", "Comment"}) + table.SetHeader([]string{"ID", "Name", "Email", "Roles", "Keys", "Groups", "Comment"}) table.SetBorder(false) table.SetCaption(true, fmt.Sprintf("Total: %d users.", len(users))) for _, user := range users { @@ -909,15 +909,15 @@ GLOBAL OPTIONS: for _, userGroup := range user.Groups { groupNames = append(groupNames, userGroup.Name) } - isAdmin := "" - if user.IsAdmin { - isAdmin = "yes" + roleNames := []string{} + for _, role := range user.Roles { + roleNames = append(roleNames, role.Name) } table.Append([]string{ fmt.Sprintf("%d", user.ID), user.Name, user.Email, - isAdmin, + strings.Join(roleNames, ", "), fmt.Sprintf("%d", len(user.Keys)), strings.Join(groupNames, ", "), user.Comment, @@ -946,10 +946,10 @@ GLOBAL OPTIONS: Flags: []cli.Flag{ cli.StringFlag{Name: "name, n", Usage: "Renames the user"}, cli.StringFlag{Name: "email, e", Usage: "Updates the email"}, - cli.BoolFlag{Name: "set-admin", Usage: "Sets admin flag"}, - cli.BoolFlag{Name: "unset-admin", Usage: "Unsets admin flag"}, - cli.StringSliceFlag{Name: "assign-group, g", Usage: "Assign the user to a new `USERGROUPS`"}, - cli.StringSliceFlag{Name: "unassign-group", Usage: "Unassign the user from a `USERGROUPS`"}, + 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 { @@ -983,27 +983,13 @@ GLOBAL OPTIONS: } } - // special fields - if c.Bool("set-admin") { - if err := model.Updates(User{IsAdmin: true}).Error; err != nil { - tx.Rollback() - return err - } - } - if c.Bool("unset-admin") { - if err := model.Updates(map[string]interface{}{"is_admin": false}).Error; err != nil { - tx.Rollback() - return err - } - } - // associations var appendGroups []UserGroup - var deleteGroups []UserGroup if err := UserGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil { tx.Rollback() return err } + var deleteGroups []UserGroup if err := UserGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil { tx.Rollback() return err @@ -1012,6 +998,21 @@ GLOBAL OPTIONS: tx.Rollback() return err } + + var appendRoles []UserRole + if err := UserRolesByIdentifiers(db, c.StringSlice("assign-role")).Find(&appendRoles).Error; err != nil { + tx.Rollback() + return err + } + var deleteRoles []UserRole + if err := UserRolesByIdentifiers(db, c.StringSlice("unassign-role")).Find(&deleteRoles).Error; err != nil { + tx.Rollback() + return err + } + if err := model.Association("Roles").Append(&appendRoles).Delete(deleteRoles).Error; err != nil { + tx.Rollback() + return err + } } return tx.Commit().Error