mirror of
https://github.com/EchoVault/SugarDB.git
synced 2025-09-27 04:16:06 +08:00
388 lines
13 KiB
Go
388 lines
13 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 sugardb
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"github.com/echovault/sugardb/internal"
|
|
"github.com/tidwall/resp"
|
|
"strings"
|
|
)
|
|
|
|
// ACLLoadOptions modifies the behaviour of the ACLLoad function.
|
|
// If Merge is true, the ACL configuration from the file will be merged with the in-memory ACL configuration.
|
|
// If Replace is set to true, the ACL configuration from the file will replace the in-memory ACL configuration.
|
|
// If both flags are set to true, Merge will be prioritised.
|
|
type ACLLoadOptions struct {
|
|
Merge bool
|
|
Replace bool
|
|
}
|
|
|
|
// User is the user object passed to the ACLSetUser function to update an existing user or create a new user.
|
|
//
|
|
// Username - string - the user's username.
|
|
//
|
|
// Enabled - bool - whether the user should be enabled (i.e connections can authenticate with this user).
|
|
//
|
|
// NoPassword - bool - if true, this user can be authenticated against without a password.
|
|
//
|
|
// NoKeys - bool - if true, this user will not be allowed to access any keys.
|
|
//
|
|
// NoCommands - bool - if true, this user will not be allowed to execute any commands.
|
|
//
|
|
// ResetPass - bool - if true, all the user's configured passwords are removed and NoPassword is set to false.
|
|
//
|
|
// ResetKeys - bool - if true, the user's NoKeys flag is set to true and all their currently accessible keys are cleared.
|
|
//
|
|
// ResetChannels - bool - if true, the user will be allowed to access all PubSub channels.
|
|
//
|
|
// AddPlainPasswords - []string - the list of plaintext passwords to add to the user's passwords.
|
|
//
|
|
// RemovePlainPasswords - []string - the list of plaintext passwords to remove from the user's passwords.
|
|
//
|
|
// AddHashPasswords - []string - the list of SHA256 password hashes to add to the user's passwords.
|
|
//
|
|
// RemoveHashPasswords - []string - the list of SHA256 password hashes to add to the user's passwords.
|
|
//
|
|
// IncludeCategories - []string - the list of ACL command categories to allow this user to access, default is all.
|
|
//
|
|
// ExcludeCategories - []string - the list of ACL command categories to bar the user from accessing. The default is none.
|
|
//
|
|
// IncludeCommands - []string - the list of commands to allow the user to execute. The default is none. If you want to
|
|
// specify a subcommand, use the format "command|subcommand".
|
|
//
|
|
// ExcludeCommands - []string - the list of commands to bar the user from executing.
|
|
// The default is none. If you want to specify a subcommand, use the format "command|subcommand".
|
|
//
|
|
// IncludeReadWriteKeys - []string - the list of keys the user is allowed read and write access to. The default is all.
|
|
// This field accepts glob pattern strings.
|
|
//
|
|
// IncludeReadKeys - []string - the list of keys the user is allowed read access to. The default is all.
|
|
// This field accepts glob pattern strings.
|
|
//
|
|
// IncludeWriteKeys - []string - the list of keys the user is allowed write access to. The default is all.
|
|
// This field accepts glob pattern strings.
|
|
//
|
|
// IncludeChannels - []string - the list of PubSub channels the user is allowed to access ("Subscribe" and "Publish").
|
|
// This field accepts glob pattern strings.
|
|
//
|
|
// ExcludeChannels - []string - the list of PubSub channels the user cannot access ("Subscribe" and "Publish").
|
|
// This field accepts glob pattern strings.
|
|
type User struct {
|
|
Username string
|
|
Enabled bool
|
|
NoPassword bool
|
|
NoKeys bool
|
|
NoCommands bool
|
|
ResetPass bool
|
|
ResetKeys bool
|
|
ResetChannels bool
|
|
|
|
AddPlainPasswords []string
|
|
RemovePlainPasswords []string
|
|
AddHashPasswords []string
|
|
RemoveHashPasswords []string
|
|
|
|
IncludeCategories []string
|
|
ExcludeCategories []string
|
|
|
|
IncludeCommands []string
|
|
ExcludeCommands []string
|
|
|
|
IncludeReadWriteKeys []string
|
|
IncludeReadKeys []string
|
|
IncludeWriteKeys []string
|
|
|
|
IncludeChannels []string
|
|
ExcludeChannels []string
|
|
}
|
|
|
|
// ACLCat returns either the list of all categories or the list of commands within a specified category.
|
|
//
|
|
// Parameters:
|
|
//
|
|
// `category` - ...string - an optional string specifying the category. If more than one category is passed,
|
|
// only the first one will be used.
|
|
//
|
|
// Returns: string slice of categories loaded in SugarDB if category is not specified. Otherwise, returns string
|
|
// slice of commands within the specified category.
|
|
//
|
|
// Errors:
|
|
//
|
|
// "category <category> not found" - when the provided category is not found in the loaded commands.
|
|
func (server *SugarDB) ACLCat(category ...string) ([]string, error) {
|
|
cmd := []string{"ACL", "CAT"}
|
|
if len(category) > 0 {
|
|
cmd = append(cmd, category[0])
|
|
}
|
|
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return internal.ParseStringArrayResponse(b)
|
|
}
|
|
|
|
// ACLUsers returns a string slice containing the usernames of all the loaded users in the ACL module.
|
|
func (server *SugarDB) ACLUsers() ([]string, error) {
|
|
b, err := server.handleCommand(server.context, internal.EncodeCommand([]string{"ACL", "USERS"}), nil, false, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return internal.ParseStringArrayResponse(b)
|
|
}
|
|
|
|
// ACLSetUser modifies or creates a new user. If the user with the specified username exists, the ACL user will be modified.
|
|
// Otherwise, a new User is created.
|
|
//
|
|
// Parameters:
|
|
//
|
|
// `user` - User - The user object to add/update.
|
|
//
|
|
// Returns: true if the user is successfully created/updated.
|
|
func (server *SugarDB) ACLSetUser(user User) (bool, error) {
|
|
cmd := []string{"ACL", "SETUSER", user.Username}
|
|
|
|
if user.Enabled {
|
|
cmd = append(cmd, "on")
|
|
} else {
|
|
cmd = append(cmd, "off")
|
|
}
|
|
|
|
if user.NoPassword {
|
|
cmd = append(cmd, "nopass")
|
|
}
|
|
|
|
if user.NoKeys {
|
|
cmd = append(cmd, "nokeys")
|
|
}
|
|
|
|
if user.NoCommands {
|
|
cmd = append(cmd, "nocommands")
|
|
}
|
|
|
|
if user.ResetPass {
|
|
cmd = append(cmd, "resetpass")
|
|
}
|
|
|
|
if user.ResetKeys {
|
|
cmd = append(cmd, "resetkeys")
|
|
}
|
|
|
|
if user.ResetChannels {
|
|
cmd = append(cmd, "resetchannels")
|
|
}
|
|
|
|
for _, password := range user.AddPlainPasswords {
|
|
cmd = append(cmd, fmt.Sprintf(">%s", password))
|
|
}
|
|
|
|
for _, password := range user.RemovePlainPasswords {
|
|
cmd = append(cmd, fmt.Sprintf("<%s", password))
|
|
}
|
|
|
|
for _, password := range user.AddHashPasswords {
|
|
cmd = append(cmd, fmt.Sprintf("#%s", password))
|
|
}
|
|
|
|
for _, password := range user.RemoveHashPasswords {
|
|
cmd = append(cmd, fmt.Sprintf("!%s", password))
|
|
}
|
|
|
|
for _, category := range user.IncludeCategories {
|
|
cmd = append(cmd, fmt.Sprintf("+@%s", category))
|
|
}
|
|
|
|
for _, category := range user.ExcludeCategories {
|
|
cmd = append(cmd, fmt.Sprintf("-@%s", category))
|
|
}
|
|
|
|
for _, command := range user.IncludeCommands {
|
|
cmd = append(cmd, fmt.Sprintf("+%s", command))
|
|
}
|
|
|
|
for _, command := range user.ExcludeCommands {
|
|
cmd = append(cmd, fmt.Sprintf("-%s", command))
|
|
}
|
|
|
|
for _, key := range user.IncludeReadWriteKeys {
|
|
cmd = append(cmd, fmt.Sprintf("%s~%s", "%RW", key))
|
|
}
|
|
|
|
for _, key := range user.IncludeReadKeys {
|
|
cmd = append(cmd, fmt.Sprintf("%s~%s", "%R", key))
|
|
}
|
|
|
|
for _, key := range user.IncludeWriteKeys {
|
|
cmd = append(cmd, fmt.Sprintf("%s~%s", "%W", key))
|
|
}
|
|
|
|
for _, channel := range user.IncludeChannels {
|
|
cmd = append(cmd, fmt.Sprintf("+&%s", channel))
|
|
}
|
|
|
|
for _, channel := range user.ExcludeChannels {
|
|
cmd = append(cmd, fmt.Sprintf("-&%s", channel))
|
|
}
|
|
|
|
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
s, err := internal.ParseStringResponse(b)
|
|
return strings.EqualFold(s, "ok"), err
|
|
}
|
|
|
|
// ACLGetUser gets the ACL configuration of the name with the given username.
|
|
//
|
|
// Parameters:
|
|
//
|
|
// `username` - string - the username whose ACL rules you'd like to retrieve.
|
|
//
|
|
// Returns: A map[string][]string map where each key is the rule category and each value is a string slice of relevant values.
|
|
// The map returned has the following structure:
|
|
//
|
|
// "username" - string slice containing the user's username.
|
|
//
|
|
// "flags" - string slices containing the following values: "on" if the user is enabled, otherwise "off",
|
|
// "nokeys" if the user is not allowed to access any keys (and NoKeys is true),
|
|
// "nopass" if the user has no passwords (and NoPass is true).
|
|
//
|
|
// "categories" - string slice af ACL command categories associated with the user.
|
|
// If the user is allowed to access all categories, it will contain "+@*".
|
|
// For each category the user is allowed to access, the slice will contain "+@<category>".
|
|
// If the user is not allowed to access any categories, it will contain "-@*".
|
|
// For each category the user is not allowed to access, the slice will contain "-@<category>".
|
|
//
|
|
// "commands" - string slice af commands associated with the user.
|
|
// If the user is allowed to execute all commands, it will contain "+all".
|
|
// For each command the user is allowed to execute, the slice will contain "+<command>".
|
|
// If the user is not allowed to execute any commands, it will contain "-all".
|
|
// For each command the user is not allowed to execute, the slice will contain "-<category>".
|
|
//
|
|
// "keys" - string slice af keys associated with the user.
|
|
// If the user is allowed read/write access all keys, the slice will contain "%RW~*".
|
|
// For each key glob pattern the user has read/write access to, the slice will contain "%RW~<pattern>".
|
|
// If the user is allowed read access to all keys, the slice will contain "%R~*".
|
|
// For each key glob pattern the user has read access to, the slice will contain "%R~<pattern>".
|
|
// If the user is allowed write access to all keys, the slice will contain "%W~*".
|
|
// For each key glob pattern the user has write access to, the slice will contain "%W~<pattern>".
|
|
//
|
|
// "channels" - string slice af pubsub channels associated with the user.
|
|
// If the user is allowed to access all channels, the slice will contain "+&*".
|
|
// For each channel the user is allowed to access, the slice will contain "+&<channel>".
|
|
// If the user is not allowed to access any channels, the slice will contain "-&*".
|
|
// For each channel the user is not allowed to access, the slice will contain "-&<channel>".
|
|
//
|
|
// Errors:
|
|
//
|
|
// "user not found" - if the user requested does not exist in the ACL rules.
|
|
func (server *SugarDB) ACLGetUser(username string) (map[string][]string, error) {
|
|
b, err := server.handleCommand(server.context, internal.EncodeCommand([]string{"ACL", "GETUSER", username}), nil, false, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := resp.NewReader(bytes.NewReader(b))
|
|
v, _, err := r.ReadValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
arr := v.Array()
|
|
|
|
result := make(map[string][]string)
|
|
|
|
for i := 0; i < len(arr); i += 2 {
|
|
key := arr[i].String()
|
|
value := arr[i+1].Array()
|
|
|
|
result[key] = make([]string, len(value))
|
|
|
|
for j := 0; j < len(value); j++ {
|
|
result[key][j] = value[j].String()
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ACLDelUser deletes all the users with the specified usernames.
|
|
//
|
|
// Parameters:
|
|
//
|
|
// `usernames` - ...string - A string of usernames to delete from the ACL module.
|
|
//
|
|
// Returns: true if the deletion is successful.
|
|
func (server *SugarDB) ACLDelUser(usernames ...string) (bool, error) {
|
|
cmd := append([]string{"ACL", "DELUSER"}, usernames...)
|
|
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
s, err := internal.ParseStringResponse(b)
|
|
return strings.EqualFold(s, "ok"), err
|
|
}
|
|
|
|
// ACLList lists all the currently loaded ACL users and their rules.
|
|
func (server *SugarDB) ACLList() ([]string, error) {
|
|
b, err := server.handleCommand(server.context, internal.EncodeCommand([]string{"ACL", "LIST"}), nil, false, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return internal.ParseStringArrayResponse(b)
|
|
}
|
|
|
|
// ACLLoad loads the ACL configuration from the configured ACL file. The load function can either merge the loaded
|
|
// config with the in-memory config, or replace the in-memory config with the loaded config entirely.
|
|
//
|
|
// Parameters:
|
|
//
|
|
// `options` - ACLLoadOptions - modifies the load behaviour.
|
|
//
|
|
// Returns: true if the load is successful.
|
|
func (server *SugarDB) ACLLoad(options ACLLoadOptions) (bool, error) {
|
|
cmd := []string{"ACL", "LOAD"}
|
|
switch {
|
|
case options.Merge:
|
|
cmd = append(cmd, "MERGE")
|
|
case options.Replace:
|
|
cmd = append(cmd, "REPLACE")
|
|
default:
|
|
cmd = append(cmd, "REPLACE")
|
|
}
|
|
|
|
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
s, err := internal.ParseStringResponse(b)
|
|
return strings.EqualFold(s, "ok"), err
|
|
}
|
|
|
|
// ACLSave saves the current ACL configuration to the configured ACL file.
|
|
//
|
|
// Returns: true if the save is successful.
|
|
func (server *SugarDB) ACLSave() (bool, error) {
|
|
b, err := server.handleCommand(server.context, internal.EncodeCommand([]string{"ACL", "SAVE"}), nil, false, true)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
s, err := internal.ParseStringResponse(b)
|
|
return strings.EqualFold(s, "ok"), err
|
|
}
|