// 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 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 "+@". // 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 "-@". // // "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 "+". // 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 "-". // // "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~". // 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~". // 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~". // // "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 "+&". // 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 "-&". // // 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 }