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  }