common/credCache_linux.go (131 lines of code) (raw):
// Copyright © 2017 Microsoft <wastore@microsoft.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package common
import (
"errors"
"fmt"
"github.com/wastore/keyctl" // forked form "github.com/jsipprell/keyctl", todo: make a release to ensure stability
"runtime"
"sync"
"syscall"
)
// CredCache manages credential caches.
// Use keyring in Linux OS. Session keyring is chosen,
// the session hooks key should be created since user first login (i.e. by pam).
// So the session is inherited by processes created from login session.
// When user logout, the session keyring is recycled.
type CredCache struct {
keyName string // the Name of key would be cached in keyring, composed with current UID, in case user su
lock sync.Mutex
key *keyctl.Key
isPermSet bool // state used to ensure key has been set permission correctly
}
// NewCredCache creates a cred cache.
func NewCredCache(options CredCacheOptions) *CredCache {
c := &CredCache{
keyName: options.KeyName,
}
runtime.SetFinalizer(c, func(CredCache *CredCache) {
if !CredCache.isPermSet && CredCache.key != nil {
// Indicates Permission is by default ProcessAll, which is not safe and try to recycle the key.
// Note: there is no method to grant permission during adding key,
// this mechanism is added to ensure key exists only if its permission is set properly.
unlinkErr := CredCache.key.Unlink()
if unlinkErr != nil {
panic(errors.New("failed to set permission, and cannot unlink key, please logout current login session for safety consideration"))
}
}
})
return c
}
// HasCachedToken returns if there is cached token for current executing user.
func (c *CredCache) HasCachedToken() (bool, error) {
c.lock.Lock()
has, err := c.hasCachedTokenInternal()
c.lock.Unlock()
return has, err
}
// RemoveCachedToken deletes the cached token.
func (c *CredCache) RemoveCachedToken() error {
c.lock.Lock()
err := c.removeCachedTokenInternal()
c.lock.Unlock()
return err
}
// SaveToken saves an oauth token.
func (c *CredCache) SaveToken(token OAuthTokenInfo) error {
c.lock.Lock()
err := c.saveTokenInternal(token)
c.lock.Unlock()
return err
}
// LoadToken gets the cached oauth token.
func (c *CredCache) LoadToken() (*OAuthTokenInfo, error) {
c.lock.Lock()
token, err := c.loadTokenInternal()
c.lock.Unlock()
return token, err
}
///////////////////////////////////////////////////////////////////////////////////////////////
// This internal method pattern is applied to avoid defer locks.
// The reason is:
// We use locks to protect shared state from being accessed from multiple threads/goroutines at the same time.
// If a bug is in this method that causes a panic,
// then the defer will unlock another thread/goroutine allowing it to access the shared state.
// BUT, if a panic happened, the shared state is hard to be decide whether in a good or corrupted state.
// So currently let the other threads/goroutines hang forever instead of letting them access the potentially corrupted shared state.
// Once having bad state, more bad state gets injected into app and figuring out how it happened and how to recover from it is near impossible.
// On the other hand, hanging threads is MUCH easier to detect and devs can fix the bug in code to make sure that the panic doesn't happen in the first place.
///////////////////////////////////////////////////////////////////////////////////////////////
// hasCachedTokenInternal returns if there is cached token in session key ring for current login session.
func (c *CredCache) hasCachedTokenInternal() (bool, error) {
keyring, err := keyctl.SessionKeyring()
if err != nil {
return false, err
}
_, err = keyring.Search(c.keyName)
// TODO: better logging what's cause for token caching failure
// e.g. Error message: "required key not available"
// the source library could be updated to use keyctl_search
if err != nil {
return false, err
} else {
return true, nil
}
}
// removeCachedTokenInternal deletes the cached token in session key ring.
func (c *CredCache) removeCachedTokenInternal() error {
keyring, err := keyctl.SessionKeyring()
if err != nil {
return fmt.Errorf("failed to get keyring during removing cached token, %v", err)
}
key, err := keyring.Search(c.keyName)
if err != nil {
if err == syscall.ENOKEY {
return fmt.Errorf("no cached token found for current user")
}
return fmt.Errorf("no cached token found for current user, %v", err)
}
err = key.Unlink()
if err != nil {
return fmt.Errorf("failed to remove cached token, %v", err)
}
c.isPermSet = false
c.key = nil
return nil
}
// saveTokenInternal saves an oauth token in session key ring.
func (c *CredCache) saveTokenInternal(token OAuthTokenInfo) error {
c.isPermSet = false
c.key = nil
b, err := token.toJSON()
if err != nil {
return fmt.Errorf("failed to marshal during saving token, %v", err)
}
keyring, err := keyctl.SessionKeyring()
if err != nil {
return fmt.Errorf("failed to get keyring during saving token, %v", err)
}
k, err := keyring.Add(c.keyName, b)
if err != nil {
return fmt.Errorf("failed to save key, %v", err)
}
c.key = k
// Set permissions to only current user.
err = keyctl.SetPerm(k, keyctl.PermUserAll)
if err != nil {
// which indicates Permission is by default ProcessAll
unlinkErr := k.Unlink()
if unlinkErr != nil {
panic(errors.New("failed to set permission, and cannot unlink key, please logout current login session for safety consideration"))
}
return fmt.Errorf("failed to set permission for cached token, %v", err)
}
c.isPermSet = true
return nil
}
// loadTokenInternal gets an oauth token from session key ring.
func (c *CredCache) loadTokenInternal() (*OAuthTokenInfo, error) {
keyring, err := keyctl.SessionKeyring()
if err != nil {
return nil, fmt.Errorf("failed to get keyring during loading token, %v", err)
}
key, err := keyring.Search(c.keyName)
if err != nil {
return nil, fmt.Errorf("failed to find cached token during loading token, %v", err)
}
data, err := key.Get()
if err != nil {
return nil, fmt.Errorf("failed to load token, %v", err)
}
token, err := jsonToTokenInfo(data)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal token during loading key, %v", err)
}
return token, nil
}