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 }