github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/lorry/engines/redis/user.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package redis
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"fmt"
    26  	"strings"
    27  
    28  	"golang.org/x/exp/slices"
    29  
    30  	"github.com/1aal/kubeblocks/pkg/lorry/engines/models"
    31  )
    32  
    33  const (
    34  	listUserTpl   = "ACL USERS"
    35  	descUserTpl   = "ACL GETUSER %s"
    36  	createUserTpl = "ACL SETUSER %s >%s"
    37  	dropUserTpl   = "ACL DELUSER %s"
    38  	grantTpl      = "ACL SETUSER %s %s"
    39  	revokeTpl     = "ACL SETUSER %s %s"
    40  )
    41  
    42  var (
    43  	redisPreDefinedUsers = []string{
    44  		"default",
    45  		"kbadmin",
    46  		"kbdataprotection",
    47  		"kbmonitoring",
    48  		"kbprobe",
    49  		"kbreplicator",
    50  	}
    51  )
    52  
    53  func (mgr *Manager) ListUsers(ctx context.Context) ([]models.UserInfo, error) {
    54  	data, err := mgr.Query(ctx, listUserTpl)
    55  	if err != nil {
    56  		mgr.Logger.Error(err, "error executing %s")
    57  		return nil, err
    58  	}
    59  
    60  	results := make([]string, 0)
    61  	err = json.Unmarshal(data, &results)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	users := make([]models.UserInfo, 0)
    66  	for _, userInfo := range results {
    67  		userName := strings.TrimSpace(userInfo)
    68  		if slices.Contains(redisPreDefinedUsers, userName) {
    69  			continue
    70  		}
    71  		user := models.UserInfo{UserName: userName}
    72  		users = append(users, user)
    73  	}
    74  	return users, nil
    75  }
    76  
    77  func (mgr *Manager) ListSystemAccounts(ctx context.Context) ([]models.UserInfo, error) {
    78  	data, err := mgr.Query(ctx, listUserTpl)
    79  	if err != nil {
    80  		mgr.Logger.Error(err, "error executing %s")
    81  		return nil, err
    82  	}
    83  
    84  	results := make([]string, 0)
    85  	err = json.Unmarshal(data, &results)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	users := make([]models.UserInfo, 0)
    90  	for _, userInfo := range results {
    91  		userName := strings.TrimSpace(userInfo)
    92  		if !slices.Contains(redisPreDefinedUsers, userName) {
    93  			continue
    94  		}
    95  		user := models.UserInfo{UserName: userName}
    96  		users = append(users, user)
    97  	}
    98  	return users, nil
    99  }
   100  
   101  func (mgr *Manager) DescribeUser(ctx context.Context, userName string) (*models.UserInfo, error) {
   102  	sql := fmt.Sprintf(descUserTpl, userName)
   103  
   104  	data, err := mgr.Query(ctx, sql)
   105  	if err != nil {
   106  		mgr.Logger.Error(err, "execute sql failed", "sql", sql)
   107  		return nil, err
   108  	}
   109  
   110  	// parse it to a map or an []interface
   111  	// try map first
   112  	var profile map[string]string
   113  	profile, err = parseCommandAndKeyFromMap(data)
   114  	if err != nil {
   115  		// try list
   116  		profile, err = parseCommandAndKeyFromList(data)
   117  		if err != nil {
   118  			return nil, err
   119  		}
   120  	}
   121  
   122  	user := &models.UserInfo{
   123  		UserName: userName,
   124  		RoleName: (string)(priv2Role(profile["commands"] + " " + profile["keys"])),
   125  	}
   126  	return user, nil
   127  }
   128  
   129  func (mgr *Manager) CreateUser(ctx context.Context, userName, password string) error {
   130  	sql := fmt.Sprintf(createUserTpl, userName, password)
   131  
   132  	_, err := mgr.Exec(ctx, sql)
   133  	if err != nil {
   134  		mgr.Logger.Error(err, "execute sql failed", "sql", sql)
   135  		return err
   136  	}
   137  
   138  	return nil
   139  }
   140  
   141  func (mgr *Manager) DeleteUser(ctx context.Context, userName string) error {
   142  	sql := fmt.Sprintf(dropUserTpl, userName)
   143  
   144  	_, err := mgr.Exec(ctx, sql)
   145  	if err != nil {
   146  		mgr.Logger.Error(err, "execute sql failed", "sql", sql)
   147  		return err
   148  	}
   149  
   150  	return nil
   151  }
   152  
   153  func (mgr *Manager) GrantUserRole(ctx context.Context, userName, roleName string) error {
   154  	var sql string
   155  	command := role2Priv("+", roleName)
   156  	sql = fmt.Sprintf(grantTpl, userName, command)
   157  	_, err := mgr.Exec(ctx, sql)
   158  	if err != nil {
   159  		mgr.Logger.Error(err, "execute sql failed", "sql", sql)
   160  		return err
   161  	}
   162  
   163  	return nil
   164  }
   165  
   166  func (mgr *Manager) RevokeUserRole(ctx context.Context, userName, roleName string) error {
   167  	var sql string
   168  	command := role2Priv("-", roleName)
   169  	sql = fmt.Sprintf(revokeTpl, userName, command)
   170  	_, err := mgr.Exec(ctx, sql)
   171  	if err != nil {
   172  		mgr.Logger.Error(err, "execute sql failed", "sql", sql)
   173  		return err
   174  	}
   175  
   176  	return nil
   177  }
   178  
   179  func role2Priv(prefix, roleName string) string {
   180  	var command string
   181  
   182  	roleType := models.String2RoleType(roleName)
   183  	switch roleType {
   184  	case models.SuperUserRole:
   185  		command = fmt.Sprintf("%s@all allkeys", prefix)
   186  	case models.ReadWriteRole:
   187  		command = fmt.Sprintf("-@all %s@write %s@read allkeys", prefix, prefix)
   188  	case models.ReadOnlyRole:
   189  		command = fmt.Sprintf("-@all %s@read allkeys", prefix)
   190  	}
   191  	return command
   192  }
   193  
   194  func priv2Role(commands string) models.RoleType {
   195  	if commands == "-@all" {
   196  		return models.NoPrivileges
   197  	}
   198  	switch commands {
   199  	case "-@all +@read ~*":
   200  		return models.ReadOnlyRole
   201  	case "-@all +@write +@read ~*":
   202  		return models.ReadWriteRole
   203  	case "+@all ~*":
   204  		return models.SuperUserRole
   205  	default:
   206  		return models.CustomizedRole
   207  	}
   208  }
   209  
   210  func parseCommandAndKeyFromMap(data interface{}) (map[string]string, error) {
   211  	var (
   212  		redisUserPrivContxt = []string{"commands", "keys", "channels", "selectors"}
   213  	)
   214  
   215  	profile := make(map[string]string, 0)
   216  	results := make(map[string]interface{}, 0)
   217  
   218  	err := json.Unmarshal(data.([]byte), &results)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  	for k, v := range results {
   223  		// each key is string, and each v is string or list of string
   224  		if !slices.Contains(redisUserPrivContxt, k) {
   225  			continue
   226  		}
   227  
   228  		switch v := v.(type) {
   229  		case string:
   230  			profile[k] = v
   231  		case []interface{}:
   232  			selectors := make([]string, 0)
   233  			for _, sel := range v {
   234  				selectors = append(selectors, sel.(string))
   235  			}
   236  			profile[k] = strings.Join(selectors, " ")
   237  		default:
   238  			return nil, fmt.Errorf("unknown data type: %v", v)
   239  		}
   240  	}
   241  	return profile, nil
   242  }
   243  
   244  func parseCommandAndKeyFromList(data interface{}) (map[string]string, error) {
   245  	var (
   246  		redisUserPrivContxt  = []string{"commands", "keys", "channels", "selectors"}
   247  		redisUserInfoContext = []string{"flags", "passwords"}
   248  	)
   249  
   250  	profile := make(map[string]string, 0)
   251  	results := make([]interface{}, 0)
   252  
   253  	err := json.Unmarshal(data.([]byte), &results)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	// parse line by line
   258  	var context string
   259  	for i := 0; i < len(results); i++ {
   260  		result := results[i]
   261  		switch result := result.(type) {
   262  		case string:
   263  			strVal := strings.TrimSpace(result)
   264  			if len(strVal) == 0 {
   265  				continue
   266  			}
   267  			if slices.Contains(redisUserInfoContext, strVal) {
   268  				i++
   269  				continue
   270  			}
   271  			if slices.Contains(redisUserPrivContxt, strVal) {
   272  				context = strVal
   273  			} else {
   274  				profile[context] = strVal
   275  			}
   276  		case []interface{}:
   277  			selectors := make([]string, 0)
   278  			for _, sel := range result {
   279  				selectors = append(selectors, sel.(string))
   280  			}
   281  			profile[context] = strings.Join(selectors, " ")
   282  		}
   283  	}
   284  	return profile, nil
   285  }