10000 smb: refresh kerberos credentials when ccache changes. · rclone/rclone@ceed2c3 · GitHub
[go: up one dir, main page]

Skip to content

Commit ceed2c3

Browse files
committed
smb: refresh kerberos credentials when ccache changes.
Signed-off-by: sudipto baral <sudiptobaral.me@gmail.com>
1 parent 35c542d commit ceed2c3

File tree

2 files changed

+101
-10
lines changed

2 files changed

+101
-10
lines changed

backend/smb/kerberos.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,31 @@ import (
77
"path/filepath"
88
"strings"
99
"sync"
10+
"time"
1011

1112
"github.com/jcmturner/gokrb5/v8/client"
1213
"github.com/jcmturner/gokrb5/v8/config"
1314
"github.com/jcmturner/gokrb5/v8/credentials"
1415
)
1516

1617
var (
18+
// kerberosClient caches Kerberos clients keyed by resolved ccache path.
19+
// Clients are reused unless the associated ccache file changes.
1720
kerberosClient sync.Map // map[string]*client.Client
18-
kerberosErr sync.Map // map[string]error
21+
22+
// kerberosErr caches errors encountered when loading Kerberos clients.
23+
// Prevents repeated attempts for paths that previously failed.
24+
kerberosErr sync.Map // map[string]error
25+
26+
// kerberosCredModTime tracks the last known modification time of ccache files.
27+
// Used to detect changes and trigger credential refresh.
28+
kerberosCredModTime sync.Map // map[string]time.Time
29+
)
30+
31+
var (
32+
loadCCacheFunc = credentials.LoadCCache
33+
newClientFromCCache = client.NewFromCCache
34+
loadKrbConfig = loadKerberosConfig
1935
)
2036

2137
func resolveCcachePath(ccachePath string) (string, error) {
@@ -65,30 +81,45 @@ func createKerberosClient(ccachePath string) (*client.Client, error) {
6581
return nil, err
6682
}
6783

68-
// check if we already have a client or an error for this ccache path
69-
if errVal, ok := kerberosErr.Load(ccachePath); ok {
70-
return nil, errVal.(error)
84+
// Check if the ccache file is modified since last check
85+
stat, statErr := os.Stat(ccachePath)
86+
if statErr != nil {
87+
kerberosErr.Store(ccachePath, statErr)
88+
return nil, statErr
7189
}
72-
if clientVal, ok := kerberosClient.Load(ccachePath); ok {
73-
return clientVal.(*client.Client), nil
90+
mtime := stat.ModTime()
91+
92+
if oldCredModTimeVal, ok := kerberosCredModTime.Load(ccachePath); ok {
93+
if oldMtime, ok := oldCredModTimeVal.(time.Time); ok && oldMtime.Equal(mtime) {
94+
// ccache hasn't changed — return cached client or error
95+
if errVal, ok := kerberosErr.Load(ccachePath); ok {
96+
return nil, errVal.(error)
97+
}
98+
if clientVal, ok := kerberosClient.Load(ccachePath); ok {
99+
return clientVal.(*client.Client), nil
100+
}
101+
}
74102
}
75103

76-
// create a new client if not found in the map
77-
cfg, err := loadKerberosConfig()
104+
// ccache changed or no valid cached client — reload credentials
105+
cfg, err := loadKrbConfig()
78106
if err != nil {
79107
kerberosErr.Store(ccachePath, err)
80108
return nil, err
81109
}
82-
ccache, err := credentials.LoadCCache(ccachePath)
110+
ccache, err := loadCCacheFunc(ccachePath)
83111
if err != nil {
84112
kerberosErr.Store(ccachePath, err)
85113
return nil, err
86114
}
87-
cl, err := client.NewFromCCache(ccache, cfg)
115+
cl, err := newClientFromCCache(ccache, cfg)
88116
if err != nil {
89117
kerberosErr.Store(ccachePath, err)
90118
return nil, err
91119
}
120+
92121
kerberosClient.Store(ccachePath, cl)
122+
kerberosErr.Delete(ccachePath)
123+
kerberosCredModTime.Store(ccachePath, mtime)
93124
return cl, nil
94125
}

backend/smb/kerberos_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import (
44
"os"
55
"path/filepath"
66
"testing"
7+
"time"
78

9+
"github.com/jcmturner/gokrb5/v8/client"
10+
"github.com/jcmturner/gokrb5/v8/config"
11+
"github.com/jcmturner/gokrb5/v8/credentials"
812
"github.com/stretchr/testify/assert"
913
)
1014

@@ -77,3 +81,59 @@ func TestResolveCcachePath(t *testing.T) {
7781
})
7882
}
7983
}
84+
85+
func TestCreateKerberosClient_ReloadOnCcacheChange(t *testing.T) {
86+
87+
// Create temporary fake ccache file
88+
tmpFile, err := os.CreateTemp("", "krb5cc_test")
89+
assert.NoError(t, err)
90+
defer os.Remove(tmpFile.Name())
91+
92+
fakeCcacheContent := []byte("CCACHE_VERSION 4\n")
93+
_, err = tmpFile.Write(fakeCcacheContent)
94+
assert.NoError(t, err)
95+
assert.NoError(t, tmpFile.Close())
96+
97+
// Patch functions
98+
origLoadCCache := loadCCacheFunc
99+
origNewFromCCache := newClientFromCCache
100+
origLoadKerberosConfig := loadKrbConfig
101+
102+
defer func() {
103+
loadCCacheFunc = origLoadCCache
104+
newClientFromCCache = origNewFromCCache
105+
loadKrbConfig = origLoadKerberosConfig
106+
}()
107+
108+
loadCallCount := 0
109+
loadCCacheFunc = func(path string) (*credentials.CCache, error) {
110+
loadCallCount++
111+
return &credentials.CCache{}, nil
112+
}
113+
newClientFromCCache = func(cc *credentials.CCache, cfg *config.Config, _ ...func(*client.Settings)) (*client.Client, error) {
114+
return &client.Client{}, nil
115+
}
116+
loadKrbConfig = func() (*config.Config, error) {
117+
return &config.Config{}, nil
118+
}
119+
120+
// First call — should trigger load
121+
_, err = createKerberosClient(tmpFile.Name())
122+
assert.NoError(t, err)
123+
assert.Equal(t, 1, loadCallCount, "expected 1 load call")
124+
125+
// Second call — should reuse cached client
126+
_, err = createKerberosClient(tmpFile.Name())
127+
assert.NoError(t, err)
128+
assert.Equal(t, 1, loadCallCount, "expected reuse on unchanged ccache")
129+
130+
// Simulate file update
131+
time.Sleep(1 * time.Second) // ensure mtime actually changes
132+
err = os.WriteFile(tmpFile.Name(), []byte("CCACHE_VERSION 4\n#updated"), 0600)
133+
assert.NoError(t, err)
134+
135+
// Third call — should detect change and reload
136+
_, err = createKerberosClient(tmpFile.Name())
137+
assert.NoError(t, err)
138+
assert.Equal(t, 2, loadCallCount, "expected reload on changed ccache")
139+
}

0 commit comments

Comments
 (0)
0