github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/auth/authorize/cached_provider.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 authorize 21 22 import ( 23 "encoding/json" 24 "log" 25 "os" 26 "path/filepath" 27 28 "github.com/99designs/keyring" 29 "github.com/pkg/errors" 30 31 "github.com/1aal/kubeblocks/pkg/cli/cmd/auth/authorize/authenticator" 32 "github.com/1aal/kubeblocks/pkg/cli/util" 33 ) 34 35 const ( 36 authDir = "auth" 37 userInfoFile = "user_info.json" 38 tokenFile = "token.json" 39 40 keyringKey = "token" 41 keyringService = "kubeblocks" 42 keyringLabel = "KUBEBLOCKS CLI" 43 44 fileMode = 0o600 45 ) 46 47 type KeyringCached struct { 48 key string 49 valid bool 50 keyring keyring.Keyring 51 } 52 53 func (k *KeyringCached) isValid() bool { 54 return k.valid 55 } 56 57 func (k *KeyringCached) get() ([]byte, error) { 58 item, err := k.keyring.Get(k.key) 59 if err != nil { 60 return nil, err 61 } 62 return item.Data, nil 63 } 64 65 func (k *KeyringCached) set(data []byte) error { 66 return k.keyring.Set(keyring.Item{ 67 Key: k.key, 68 Data: data, 69 Label: keyringLabel, 70 }) 71 } 72 73 func (k *KeyringCached) remove() error { 74 return k.keyring.Remove(k.key) 75 } 76 77 type FileCached struct { 78 tokenFilename string 79 userInfoFilename string 80 } 81 82 type KeyringCachedTokenProvider struct { 83 keyringCached KeyringProvider 84 fileCached FileCached 85 } 86 87 func NewKeyringCachedTokenProvider(keyringCached *KeyringProvider) *KeyringCachedTokenProvider { 88 fileCached := FileCached{ 89 tokenFilename: tokenFile, 90 userInfoFilename: userInfoFile, 91 } 92 93 if keyringCached == nil { 94 defaultKeyring, isValid := getDefaultKeyring() 95 return &KeyringCachedTokenProvider{ 96 keyringCached: &KeyringCached{ 97 key: keyringKey, 98 valid: isValid, 99 keyring: defaultKeyring, 100 }, 101 fileCached: fileCached, 102 } 103 } 104 105 return &KeyringCachedTokenProvider{ 106 keyringCached: *keyringCached, 107 fileCached: fileCached, 108 } 109 } 110 111 func getDefaultKeyring() (keyring.Keyring, bool) { 112 k, err := keyring.Open(keyring.Config{ 113 AllowedBackends: []keyring.BackendType{ 114 keyring.SecretServiceBackend, 115 keyring.KWalletBackend, 116 keyring.KeychainBackend, 117 keyring.WinCredBackend, 118 }, 119 ServiceName: keyringService, 120 KeychainTrustApplication: true, 121 KeychainSynchronizable: true, 122 }) 123 124 if err != nil { 125 return nil, false 126 } 127 return k, true 128 } 129 130 func (k *KeyringCachedTokenProvider) GetTokens() (*authenticator.TokenResponse, error) { 131 if !k.keyringCached.isValid() { 132 token, tokenErr := k.fileCached.readToken() 133 if os.IsNotExist(tokenErr) { 134 return nil, nil 135 } 136 return token, tokenErr 137 } 138 139 data, err := k.keyringCached.get() 140 if err != nil { 141 if err == keyring.ErrKeyNotFound { 142 return nil, nil 143 } 144 return nil, errors.Wrap(err, "error getting token information from keyring") 145 } 146 147 var tokenResponse authenticator.TokenResponse 148 err = json.Unmarshal(data, &tokenResponse) 149 if err != nil { 150 return nil, errors.Wrap(err, "could not unmarshal token data from keyring") 151 } 152 153 return &tokenResponse, nil 154 } 155 156 func (k *KeyringCachedTokenProvider) cacheTokens(tokenResponse *authenticator.TokenResponse) error { 157 data, err := json.Marshal(tokenResponse) 158 if err != nil { 159 return errors.Wrap(err, "could not marshal token data for keyring") 160 } 161 162 if !k.keyringCached.isValid() { 163 return k.fileCached.writeToken(data) 164 } 165 166 return k.keyringCached.set(data) 167 } 168 169 func (k *KeyringCachedTokenProvider) deleteTokens() error { 170 if !k.keyringCached.isValid() { 171 return k.fileCached.deleteToken() 172 } 173 174 return k.keyringCached.remove() 175 } 176 177 func (k *KeyringCachedTokenProvider) cacheUserInfo(userInfo *authenticator.UserInfoResponse) error { 178 saveDir, err := k.fileCached.getConfigDir() 179 if err != nil { 180 return err 181 } 182 183 if err := os.MkdirAll(saveDir, os.ModePerm); err != nil { 184 return errors.Wrap(err, "failed to create config directory") 185 } 186 savePath := filepath.Join(saveDir, k.fileCached.userInfoFilename) 187 188 newData, err := json.Marshal(userInfo) 189 if err != nil { 190 return errors.Wrap(err, "failed to marshal user info") 191 } 192 193 if err := os.WriteFile(savePath, newData, fileMode); err != nil { 194 return errors.Wrap(err, "failed to write user info file") 195 } 196 return nil 197 } 198 199 func (k *KeyringCachedTokenProvider) getUserInfo() (*authenticator.UserInfoResponse, error) { 200 saveDir, err := k.fileCached.getConfigDir() 201 if err != nil { 202 return nil, err 203 } 204 savePath := filepath.Join(saveDir, k.fileCached.userInfoFilename) 205 data, err := os.ReadFile(savePath) 206 if err != nil { 207 return nil, errors.Wrap(err, "failed to read user info file") 208 } 209 210 var userInfo authenticator.UserInfoResponse 211 if err := json.Unmarshal(data, &userInfo); err != nil { 212 return nil, errors.Wrap(err, "failed to unmarshal user info") 213 } 214 return &userInfo, nil 215 } 216 217 func (f *FileCached) getConfigDir() (string, error) { 218 cliHomeDir, err := util.GetCliHomeDir() 219 if err != nil { 220 return "", err 221 } 222 return filepath.Join(cliHomeDir, authDir), nil 223 } 224 225 func (f *FileCached) getTokenPath() (string, error) { 226 dir, err := f.getConfigDir() 227 if err != nil { 228 return "", err 229 } 230 231 return filepath.Join(dir, f.tokenFilename), nil 232 } 233 234 func (f *FileCached) writeToken(data []byte) error { 235 tokenPath, err := f.getTokenPath() 236 if err != nil { 237 return err 238 } 239 240 configDir := filepath.Dir(tokenPath) 241 242 _, err = os.Stat(configDir) 243 if os.IsNotExist(err) { 244 err = os.MkdirAll(configDir, os.ModePerm) 245 if err != nil { 246 return errors.New("error creating config directory") 247 } 248 } else if err != nil { 249 return err 250 } 251 252 err = os.WriteFile(tokenPath, data, fileMode) 253 if err != nil { 254 return errors.Wrap(err, "error writing token") 255 } 256 257 return nil 258 } 259 260 func (f *FileCached) readToken() (*authenticator.TokenResponse, error) { 261 var data []byte 262 tokenPath, err := f.getTokenPath() 263 if err != nil { 264 return nil, err 265 } 266 267 stat, err := os.Stat(tokenPath) 268 if err != nil { 269 if !os.IsNotExist(err) { 270 log.Fatal(err) 271 } 272 return nil, err 273 } else { 274 if stat.Mode()&^fileMode != 0 { 275 err = os.Chmod(tokenPath, fileMode) 276 if err != nil { 277 log.Printf("Unable to change %v file mode to 0%o: %v", tokenPath, fileMode, err) 278 } 279 } 280 data, err = os.ReadFile(tokenPath) 281 if err != nil { 282 log.Fatal(err) 283 } 284 } 285 286 var tokenResponse *authenticator.TokenResponse 287 err = json.Unmarshal(data, tokenResponse) 288 if err != nil { 289 return nil, err 290 } 291 292 return tokenResponse, nil 293 } 294 295 func (f *FileCached) deleteToken() error { 296 tokenPath, err := f.getTokenPath() 297 if err != nil { 298 return err 299 } 300 301 err = os.Remove(tokenPath) 302 if err != nil { 303 if !os.IsNotExist(err) { 304 return errors.Wrap(err, "error removing access token file") 305 } 306 } 307 308 configFile, err := f.getConfigDir() 309 if err != nil { 310 return err 311 } 312 313 err = os.Remove(configFile) 314 if err != nil { 315 if !os.IsNotExist(err) { 316 return errors.Wrap(err, "error removing default config file") 317 } 318 } 319 return nil 320 }