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 }