Files
SugarDB/internal/modules/acl/user.go
2024-06-02 04:01:45 +08:00

328 lines
10 KiB
Go

// Copyright 2024 Kelvin Clement Mwinuka
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package acl
import (
"slices"
"strings"
)
const (
PasswordPlainText = "plaintext"
PasswordSHA256 = "SHA256"
)
type Password struct {
PasswordType string `json:"PasswordType" yaml:"PasswordType"` // plaintext, SHA256
PasswordValue string `json:"PasswordValue" yaml:"PasswordValue"`
}
type User struct {
Username string `json:"Username" yaml:"Username"`
Enabled bool `json:"Enabled" yaml:"Enabled"`
NoPassword bool `json:"NoPassword" yaml:"NoPassword"`
NoKeys bool `json:"NoKeys" yaml:"NoKeys"`
Passwords []Password `json:"Passwords" yaml:"Passwords"`
IncludedCategories []string `json:"IncludedCategories" yaml:"IncludedCategories"`
ExcludedCategories []string `json:"ExcludedCategories" yaml:"ExcludedCategories"`
IncludedCommands []string `json:"IncludedCommands" yaml:"IncludedCommands"`
ExcludedCommands []string `json:"ExcludedCommands" yaml:"ExcludedCommands"`
IncludedReadKeys []string `json:"IncludedReadKeys" yaml:"IncludedReadKeys"`
IncludedWriteKeys []string `json:"IncludedWriteKeys" yaml:"IncludedWriteKeys"`
IncludedPubSubChannels []string `json:"IncludedPubSubChannels" yaml:"IncludedPubSubChannels"`
ExcludedPubSubChannels []string `json:"ExcludedPubSubChannels" yaml:"ExcludedPubSubChannels"`
}
func (user *User) Normalise() {
user.IncludedCategories = RemoveDuplicateEntries(user.IncludedCategories, "allCategories")
if len(user.IncludedCategories) == 0 {
user.IncludedCategories = []string{"*"}
}
user.ExcludedCategories = RemoveDuplicateEntries(user.ExcludedCategories, "allCategories")
if slices.Contains(user.ExcludedCategories, "*") {
user.IncludedCategories = []string{}
}
user.IncludedCommands = RemoveDuplicateEntries(user.IncludedCommands, "allCommands")
if len(user.IncludedCommands) == 0 {
user.IncludedCommands = []string{"*"}
}
user.ExcludedCommands = RemoveDuplicateEntries(user.ExcludedCommands, "allCommands")
if slices.Contains(user.ExcludedCommands, "*") {
user.IncludedCommands = []string{}
}
user.IncludedReadKeys = RemoveDuplicateEntries(user.IncludedReadKeys, "allKeys")
if len(user.IncludedReadKeys) == 0 && !user.NoKeys {
user.IncludedReadKeys = []string{"*"}
}
user.IncludedWriteKeys = RemoveDuplicateEntries(user.IncludedWriteKeys, "allKeys")
if len(user.IncludedWriteKeys) == 0 && !user.NoKeys {
user.IncludedWriteKeys = []string{"*"}
}
user.IncludedPubSubChannels = RemoveDuplicateEntries(user.IncludedPubSubChannels, "allChannels")
if len(user.IncludedPubSubChannels) == 0 {
user.IncludedPubSubChannels = []string{"*"}
}
user.ExcludedPubSubChannels = RemoveDuplicateEntries(user.ExcludedPubSubChannels, "allChannels")
if slices.Contains(user.ExcludedPubSubChannels, "*") {
user.IncludedPubSubChannels = []string{}
}
// Sort passwords
slices.SortStableFunc(user.Passwords, func(a, b Password) int {
types := map[string]int{
PasswordPlainText: 0,
PasswordSHA256: 1,
}
return types[a.PasswordType] - types[b.PasswordType]
})
}
func RemoveDuplicateEntries(entries []string, allAlias string) (res []string) {
entriesMap := make(map[string]int)
for _, entry := range entries {
if entry == allAlias {
entriesMap["*"] += 1
continue
}
entriesMap[entry] += 1
}
for key, _ := range entriesMap {
if key == "*" && len(entriesMap) == 1 {
res = []string{"*"}
return
}
if key != "*" {
res = append(res, key)
}
}
return
}
func (user *User) UpdateUser(cmd []string) error {
for _, str := range cmd {
// Parse enabled
if strings.EqualFold(str, "on") {
user.Enabled = true
}
if strings.EqualFold(str, "off") {
user.Enabled = false
}
// Parse passwords
if str[0] == '>' || str[0] == '#' {
user.Passwords = append(user.Passwords, Password{
PasswordType: GetPasswordType(str),
PasswordValue: str[1:],
})
user.NoPassword = false
continue
}
if str[0] == '<' {
user.Passwords = slices.DeleteFunc(user.Passwords, func(password Password) bool {
return strings.EqualFold(password.PasswordType, PasswordPlainText) && password.PasswordValue == str[1:]
})
continue
}
if str[0] == '!' {
user.Passwords = slices.DeleteFunc(user.Passwords, func(password Password) bool {
return strings.EqualFold(password.PasswordType, PasswordSHA256) && password.PasswordValue == str[1:]
})
continue
}
// Parse categories
if strings.EqualFold(str, "nocommands") {
user.ExcludedCategories = []string{"*"}
user.ExcludedCommands = []string{"*"}
continue
}
if strings.EqualFold(str, "allCategories") {
user.IncludedCategories = []string{"*"}
continue
}
if len(str) > 3 && str[1] == '@' {
if str[0] == '+' {
user.IncludedCategories = append(user.IncludedCategories, str[2:])
continue
}
if str[0] == '-' {
user.ExcludedCategories = append(user.ExcludedCategories, str[2:])
continue
}
}
// Parse keys
if strings.EqualFold(str, "allKeys") {
user.IncludedReadKeys = []string{"*"}
user.IncludedWriteKeys = []string{"*"}
user.NoKeys = false
continue
}
if (len(str) > 1 && str[0] == '~') || len(str) > 4 && strings.EqualFold(str[0:4], "%RW~") {
startIndex := strings.Index(str, "~") + 1
user.IncludedReadKeys = append(user.IncludedReadKeys, str[startIndex:])
user.IncludedWriteKeys = append(user.IncludedWriteKeys, str[startIndex:])
user.NoKeys = false
continue
}
if len(str) > 3 && strings.EqualFold(str[0:3], "%R~") {
user.IncludedReadKeys = append(user.IncludedReadKeys, str[3:])
user.NoKeys = false
continue
}
if len(str) > 3 && strings.EqualFold(str[0:3], "%W~") {
user.IncludedWriteKeys = append(user.IncludedWriteKeys, str[3:])
user.NoKeys = false
continue
}
// Parse channels
if strings.EqualFold(str, "allChannels") {
user.IncludedPubSubChannels = []string{"*"}
continue
}
if len(str) > 2 && str[1] == '&' {
if str[0] == '+' {
user.IncludedPubSubChannels = append(user.IncludedPubSubChannels, str[2:])
continue
}
if str[0] == '-' {
user.ExcludedPubSubChannels = append(user.ExcludedPubSubChannels, str[2:])
continue
}
}
// Parse commands
if strings.EqualFold(str, "allCommands") {
user.IncludedCommands = []string{"*"}
user.ExcludedCommands = []string{}
continue
}
if len(str) > 2 && !slices.Contains([]uint8{'&', '@'}, str[1]) {
if str[0] == '+' {
user.IncludedCommands = append(user.IncludedCommands, str[1:])
continue
}
if str[0] == '-' {
user.ExcludedCommands = append(user.ExcludedCommands, str[1:])
continue
}
}
}
// If nopass is provided, delete all passwords
for _, str := range cmd {
if strings.EqualFold(str, "nopass") {
user.Passwords = []Password{}
user.NoPassword = true
}
}
for _, str := range cmd {
// If resetpass is provided, delete all passwords and set NoPassword to false
if strings.EqualFold(str, "resetpass") {
user.Passwords = []Password{}
user.NoPassword = false
}
// If nocommands is provided, disable all commands for this user
if strings.EqualFold(str, "nocommands") {
user.IncludedCommands = []string{}
user.ExcludedCommands = []string{"*"}
user.IncludedCategories = []string{}
user.ExcludedCategories = []string{"*"}
}
// If resetkeys or nokeys is provided, reset all keys that the user can access.
if slices.Contains([]string{"resetkeys", "nokeys"}, str) {
user.IncludedReadKeys = []string{}
user.IncludedWriteKeys = []string{}
user.NoKeys = true
}
// If resetchannels is provided, remove all the pub/sub channels that the user can access
if strings.EqualFold(str, "resetchannels") {
user.IncludedPubSubChannels = []string{}
user.ExcludedPubSubChannels = []string{"*"}
}
}
return nil
}
func (user *User) Merge(new *User) {
user.Enabled = new.Enabled
user.NoKeys = new.NoKeys
user.NoPassword = new.NoPassword
user.IncludedCategories = append(user.IncludedCategories, new.IncludedCategories...)
user.ExcludedCategories = append(user.ExcludedCategories, new.ExcludedCategories...)
user.IncludedCommands = append(user.IncludedCommands, new.IncludedCommands...)
user.ExcludedCommands = append(user.ExcludedCommands, new.ExcludedCommands...)
user.IncludedReadKeys = append(user.IncludedReadKeys, new.IncludedReadKeys...)
user.IncludedWriteKeys = append(user.IncludedWriteKeys, new.IncludedWriteKeys...)
user.IncludedPubSubChannels = append(user.IncludedPubSubChannels, new.IncludedPubSubChannels...)
user.ExcludedPubSubChannels = append(user.ExcludedPubSubChannels, new.ExcludedPubSubChannels...)
// Add passwords.
for _, password := range new.Passwords {
if !slices.ContainsFunc(user.Passwords, func(p Password) bool {
return p.PasswordType == password.PasswordType && p.PasswordValue == password.PasswordValue
}) {
user.Passwords = append(user.Passwords, new.Passwords...)
}
}
user.Normalise()
}
func (user *User) Replace(new *User) {
user.Enabled = new.Enabled
user.NoKeys = new.NoKeys
user.NoPassword = new.NoPassword
user.Passwords = new.Passwords
user.IncludedCategories = new.IncludedCategories
user.ExcludedCategories = new.ExcludedCategories
user.IncludedCommands = new.IncludedCommands
user.ExcludedCommands = new.ExcludedCommands
user.IncludedReadKeys = new.IncludedReadKeys
user.IncludedWriteKeys = new.IncludedWriteKeys
user.IncludedPubSubChannels = new.IncludedPubSubChannels
user.ExcludedPubSubChannels = new.ExcludedPubSubChannels
}
func CreateUser(username string) *User {
return &User{
Username: username,
Enabled: true,
NoPassword: false,
Passwords: []Password{},
IncludedCategories: []string{},
ExcludedCategories: []string{},
IncludedCommands: []string{},
ExcludedCommands: []string{},
IncludedReadKeys: []string{},
IncludedWriteKeys: []string{},
IncludedPubSubChannels: []string{},
ExcludedPubSubChannels: []string{},
}
}
func GetPasswordType(password string) string {
if password[0] == '#' {
return PasswordSHA256
}
return PasswordPlainText
}