291 lines
7.3 KiB
Go
291 lines
7.3 KiB
Go
|
package models
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"regexp"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||
|
"gorm.io/gorm"
|
||
|
)
|
||
|
|
||
|
// AdminGroup table gives a name to a set of admin permission scopes.
|
||
|
type AdminGroup struct {
|
||
|
ID uint64 `gorm:"primaryKey"`
|
||
|
Name string `gorm:"index"`
|
||
|
CreatedAt time.Time
|
||
|
UpdatedAt time.Time
|
||
|
Scopes []AdminScope `gorm:"foreignKey:GroupID"`
|
||
|
Users []*User `gorm:"many2many:admin_group_users;"`
|
||
|
}
|
||
|
|
||
|
// AdminScope table maps admin group IDs to named scopes (which may contain wildcards).
|
||
|
type AdminScope struct {
|
||
|
ID uint64 `gorm:"primaryKey"`
|
||
|
GroupID uint64 `gorm:"index"`
|
||
|
Scope string
|
||
|
}
|
||
|
|
||
|
// Preload related tables for the AdminGroup.
|
||
|
func (g *AdminGroup) Preload() *gorm.DB {
|
||
|
return DB.Preload("Scopes").Preload("Users")
|
||
|
}
|
||
|
|
||
|
// HasAdminScope returns whether the user is an admin and has the requested scope.
|
||
|
func (u *User) HasAdminScope(scope string) bool {
|
||
|
if !u.IsAdmin {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
for _, group := range u.AdminGroups {
|
||
|
for _, compare := range group.Scopes {
|
||
|
// Scope has a wildcard?
|
||
|
if strings.ContainsRune(compare.Scope, '*') {
|
||
|
re := regexp.MustCompile(
|
||
|
fmt.Sprintf(`^%s$`, strings.ReplaceAll(compare.Scope, "*", ".+?")),
|
||
|
)
|
||
|
if res := re.FindStringSubmatch(scope); len(res) > 0 {
|
||
|
log.Debug("Regexp scope '%s' matched requested '%s'", compare.Scope, scope)
|
||
|
return true
|
||
|
}
|
||
|
} else if compare.Scope == scope {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// ListAdminGroups returns the list of admin group (names) the user belongs to.
|
||
|
func (u *User) ListAdminGroups() []string {
|
||
|
var names = []string{}
|
||
|
for _, group := range u.AdminGroups {
|
||
|
names = append(names, group.Name)
|
||
|
}
|
||
|
sort.Strings(names)
|
||
|
return names
|
||
|
}
|
||
|
|
||
|
// ListAdminScopes returns the list of admin group (names) the user belongs to.
|
||
|
func (u *User) ListAdminScopes() []string {
|
||
|
var (
|
||
|
names = []string{}
|
||
|
distinct = map[string]interface{}{}
|
||
|
)
|
||
|
for _, group := range u.AdminGroups {
|
||
|
for _, scope := range group.Scopes {
|
||
|
if _, ok := distinct[scope.Scope]; ok {
|
||
|
continue
|
||
|
}
|
||
|
distinct[scope.Scope] = nil
|
||
|
names = append(names, scope.Scope)
|
||
|
}
|
||
|
}
|
||
|
sort.Strings(names)
|
||
|
return names
|
||
|
}
|
||
|
|
||
|
// CreateAdminGroup inserts a new admin group.
|
||
|
func CreateAdminGroup(name string, scopes []string) (*AdminGroup, error) {
|
||
|
// Verify the name is unique.
|
||
|
if _, err := FindAdminGroup(name); err == nil {
|
||
|
return nil, fmt.Errorf("that group name already exists: %s", err)
|
||
|
}
|
||
|
|
||
|
g := &AdminGroup{
|
||
|
Name: name,
|
||
|
Scopes: []AdminScope{},
|
||
|
}
|
||
|
for _, scope := range scopes {
|
||
|
scope = strings.TrimSpace(scope)
|
||
|
if scope != "" {
|
||
|
g.Scopes = append(g.Scopes, AdminScope{
|
||
|
Scope: scope,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
result := DB.Create(g)
|
||
|
return g, result.Error
|
||
|
}
|
||
|
|
||
|
// FindAdminGroup by name.
|
||
|
func FindAdminGroup(name string) (*AdminGroup, error) {
|
||
|
if name == "" {
|
||
|
return nil, errors.New("username is required")
|
||
|
}
|
||
|
|
||
|
g := &AdminGroup{}
|
||
|
result := g.Preload().Where("name = ?", name).Limit(1).First(g)
|
||
|
return g, result.Error
|
||
|
}
|
||
|
|
||
|
// ListAdminGroups returns all admin groups.
|
||
|
func ListAdminGroups() ([]*AdminGroup, error) {
|
||
|
var groups = []*AdminGroup{}
|
||
|
|
||
|
result := (&AdminGroup{}).Preload().Order("name asc").Find(&groups)
|
||
|
return groups, result.Error
|
||
|
}
|
||
|
|
||
|
// ListAdminUsers returns all admin user accounts.
|
||
|
func ListAdminUsers() ([]*User, error) {
|
||
|
var users = []*User{}
|
||
|
|
||
|
result := (&User{}).Preload().Where("is_admin is true").Order("id asc").Find(&users)
|
||
|
return users, result.Error
|
||
|
}
|
||
|
|
||
|
// ScopesString returns the scopes as a newline separated string.
|
||
|
func (g *AdminGroup) ScopesString() string {
|
||
|
var scopes = []string{}
|
||
|
for _, scope := range g.Scopes {
|
||
|
scopes = append(scopes, scope.Scope)
|
||
|
}
|
||
|
return strings.Join(scopes, "\n")
|
||
|
}
|
||
|
|
||
|
// ReplaceScopes sets new scopes for the admin group.
|
||
|
func (g *AdminGroup) ReplaceScopes(scopes []string) error {
|
||
|
// Delete the original scopes.
|
||
|
var replace = []AdminScope{}
|
||
|
for _, scope := range scopes {
|
||
|
scope = strings.TrimSpace(scope)
|
||
|
if scope != "" {
|
||
|
replace = append(replace, AdminScope{
|
||
|
Scope: scope,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
err := DB.Model(g).Association("Scopes").Replace(replace)
|
||
|
|
||
|
// Cleanup orphaned scopes.
|
||
|
if result := DB.Where("group_id IS NULL").Delete(&AdminScope{}); result.Error != nil {
|
||
|
log.Error("AdminGroup.ReplaceScopes: cleanup orphaned scopes: %s", result.Error)
|
||
|
}
|
||
|
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// HasAdmin returns whether the given username is a member of the group.
|
||
|
func (g *AdminGroup) HasAdmin(username string) bool {
|
||
|
for _, user := range g.Users {
|
||
|
if user.Username == username {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// ReplaceUsers easily adds or removes admin users from a group.
|
||
|
//
|
||
|
// Post the full list of usernames who should be in the group, and it will add or
|
||
|
// remove users as needed and return which users were changed.
|
||
|
func (g *AdminGroup) ReplaceUsers(usernames []string) (added, removed []string, err error) {
|
||
|
added = []string{}
|
||
|
removed = []string{}
|
||
|
|
||
|
// Map who is currently in the group.
|
||
|
var currentUsernames = map[string]interface{}{}
|
||
|
var allUsernames = []string{}
|
||
|
for _, user := range g.Users {
|
||
|
currentUsernames[user.Username] = nil
|
||
|
allUsernames = append(allUsernames, user.Username)
|
||
|
}
|
||
|
|
||
|
// Map the incoming username list.
|
||
|
var incomingUsernames = map[string]interface{}{}
|
||
|
|
||
|
// Who needs added?
|
||
|
for _, username := range usernames {
|
||
|
incomingUsernames[username] = nil
|
||
|
allUsernames = append(allUsernames, username)
|
||
|
if _, ok := currentUsernames[username]; !ok {
|
||
|
added = append(added, username)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Who needs removed?
|
||
|
for username := range currentUsernames {
|
||
|
if _, ok := incomingUsernames[username]; !ok {
|
||
|
removed = append(removed, username)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
log.Info("AdminGroup(%s).ReplaceUsers: complete list %s (adding: %s) (removing: %s)",
|
||
|
g.Name,
|
||
|
usernames,
|
||
|
added,
|
||
|
removed,
|
||
|
)
|
||
|
|
||
|
// Select all affected users from the DB.
|
||
|
usermap, err := MapUsersByUsername(allUsernames)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Do the needful.
|
||
|
for _, username := range added {
|
||
|
log.Info("AdminGroup(%s).ReplaceUsers: ADD user %s", g.Name, username)
|
||
|
if err := g.AddUser(usermap[username]); err != nil {
|
||
|
return added, removed, fmt.Errorf("adding user %s: %s", username, err)
|
||
|
}
|
||
|
}
|
||
|
for _, username := range removed {
|
||
|
log.Info("AdminGroup(%s).ReplaceUsers: REMOVE user %s", g.Name, username)
|
||
|
if err := g.RemoveUser(usermap[username]); err != nil {
|
||
|
return added, removed, fmt.Errorf("removing user %s: %s", username, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// AddUser puts the given user into the admin group.
|
||
|
func (g *AdminGroup) AddUser(user *User) error {
|
||
|
if user.AdminGroups == nil {
|
||
|
user.AdminGroups = []*AdminGroup{}
|
||
|
}
|
||
|
|
||
|
// Already exists?
|
||
|
for _, group := range user.AdminGroups {
|
||
|
if group.ID == g.ID {
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Insert them.
|
||
|
user.AdminGroups = append(user.AdminGroups, g)
|
||
|
return user.Save()
|
||
|
}
|
||
|
|
||
|
// RemoveUser removes a user from the admin group.
|
||
|
func (g *AdminGroup) RemoveUser(user *User) error {
|
||
|
if user.AdminGroups == nil {
|
||
|
return errors.New("user had no admin groups")
|
||
|
}
|
||
|
|
||
|
return DB.Model(user).Association("AdminGroups").Delete(g)
|
||
|
}
|
||
|
|
||
|
// Delete the AdminGroup.
|
||
|
func (g *AdminGroup) Delete() error {
|
||
|
// Remove scopes.
|
||
|
if result := DB.Where("group_id = ?", g.ID).Delete(&AdminScope{}); result.Error != nil {
|
||
|
return fmt.Errorf("can't remove scopes for group ID %d: %s", g.ID, result.Error)
|
||
|
}
|
||
|
|
||
|
// Remove users.
|
||
|
if _, _, err := g.ReplaceUsers([]string{}); err != nil {
|
||
|
return fmt.Errorf("can't remove users for group ID %d: %s", g.ID, err)
|
||
|
}
|
||
|
|
||
|
return DB.Delete(g).Error
|
||
|
}
|