mirror of
https://github.com/rclone/rclone.git
synced 2025-07-18 21:14:41 +02:00
This change enhances the SMB backend in Rclone to automatically refresh Kerberos credentials when the associated ccache file is updated. Previously, credentials were only loaded once per path and cached indefinitely, which caused issues when service tickets expired or the cache was renewed on the server.
139 lines
3.6 KiB
Go
139 lines
3.6 KiB
Go
package smb
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/jcmturner/gokrb5/v8/client"
|
|
"github.com/jcmturner/gokrb5/v8/config"
|
|
"github.com/jcmturner/gokrb5/v8/credentials"
|
|
)
|
|
|
|
// KerberosFactory encapsulates dependencies and caches for Kerberos clients.
|
|
type KerberosFactory struct {
|
|
// clientCache caches Kerberos clients keyed by resolved ccache path.
|
|
// Clients are reused unless the associated ccache file changes.
|
|
clientCache sync.Map // map[string]*client.Client
|
|
|
|
// errCache caches errors encountered when loading Kerberos clients.
|
|
// Prevents repeated attempts for paths that previously failed.
|
|
errCache sync.Map // map[string]error
|
|
|
|
// modTimeCache tracks the last known modification time of ccache files.
|
|
// Used to detect changes and trigger credential refresh.
|
|
modTimeCache sync.Map // map[string]time.Time
|
|
|
|
loadCCache func(string) (*credentials.CCache, error)
|
|
newClient func(*credentials.CCache, *config.Config, ...func(*client.Settings)) (*client.Client, error)
|
|
loadConfig func() (*config.Config, error)
|
|
}
|
|
|
|
// NewKerberosFactory creates a new instance of KerberosFactory with default dependencies.
|
|
func NewKerberosFactory() *KerberosFactory {
|
|
return &KerberosFactory{
|
|
loadCCache: credentials.LoadCCache,
|
|
newClient: client.NewFromCCache,
|
|
loadConfig: defaultLoadKerberosConfig,
|
|
}
|
|
}
|
|
|
|
// GetClient returns a cached Kerberos client or creates a new one if needed.
|
|
func (kf *KerberosFactory) GetClient(ccachePath string) (*client.Client, error) {
|
|
resolvedPath, err := resolveCcachePath(ccachePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stat, err := os.Stat(resolvedPath)
|
|
if err != nil {
|
|
kf.errCache.Store(resolvedPath, err)
|
|
return nil, err
|
|
}
|
|
mtime := stat.ModTime()
|
|
|
|
if oldMod, ok := kf.modTimeCache.Load(resolvedPath); ok {
|
|
if oldTime, ok := oldMod.(time.Time); ok && oldTime.Equal(mtime) {
|
|
if errVal, ok := kf.errCache.Load(resolvedPath); ok {
|
|
return nil, errVal.(error)
|
|
}
|
|
if clientVal, ok := kf.clientCache.Load(resolvedPath); ok {
|
|
return clientVal.(*client.Client), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load Kerberos config
|
|
cfg, err := kf.loadConfig()
|
|
if err != nil {
|
|
kf.errCache.Store(resolvedPath, err)
|
|
return nil, err
|
|
}
|
|
|
|
// Load ccache
|
|
ccache, err := kf.loadCCache(resolvedPath)
|
|
if err != nil {
|
|
kf.errCache.Store(resolvedPath, err)
|
|
return nil, err
|
|
}
|
|
|
|
// Create new client
|
|
cl, err := kf.newClient(ccache, cfg)
|
|
if err != nil {
|
|
kf.errCache.Store(resolvedPath, err)
|
|
return nil, err
|
|
}
|
|
|
|
// Cache and return
|
|
kf.clientCache.Store(resolvedPath, cl)
|
|
kf.errCache.Delete(resolvedPath)
|
|
kf.modTimeCache.Store(resolvedPath, mtime)
|
|
return cl, nil
|
|
}
|
|
|
|
// resolveCcachePath resolves the KRB5 ccache path.
|
|
func resolveCcachePath(ccachePath string) (string, error) {
|
|
if ccachePath == "" {
|
|
ccachePath = os.Getenv("KRB5CCNAME")
|
|
}
|
|
|
|
switch {
|
|
case strings.Contains(ccachePath, ":"):
|
|
parts := strings.SplitN(ccachePath, ":", 2)
|
|
prefix, path := parts[0], parts[1]
|
|
switch prefix {
|
|
case "FILE":
|
|
return path, nil
|
|
case "DIR":
|
|
primary, err := os.ReadFile(filepath.Join(path, "primary"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(path, strings.TrimSpace(string(primary))), nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported KRB5CCNAME: %s", ccachePath)
|
|
}
|
|
case ccachePath == "":
|
|
u, err := user.Current()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return "/tmp/krb5cc_" + u.Uid, nil
|
|
default:
|
|
return ccachePath, nil
|
|
}
|
|
}
|
|
|
|
// defaultLoadKerberosConfig loads Kerberos config from default or env path.
|
|
func defaultLoadKerberosConfig() (*config.Config, error) {
|
|
cfgPath := os.Getenv("KRB5_CONFIG")
|
|
if cfgPath == "" {
|
|
cfgPath = "/etc/krb5.conf"
|
|
}
|
|
return config.Load(cfgPath)
|
|
}
|