website/pkg/models/admin_scopes.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
}