From 2964b1a16929122bdb7ccb4b96e44f6c561073c1 Mon Sep 17 00:00:00 2001 From: Sudipto Baral Date: Thu, 10 Jul 2025 05:17:42 -0400 Subject: [PATCH] smb: add --smb-kerberos-ccache option to set kerberos ccache per smb backend --- backend/smb/connpool.go | 2 +- backend/smb/kerberos.go | 92 +++++++++++-------- backend/smb/kerberos_test.go | 79 ++++++++++++++++ backend/smb/smb.go | 15 +++ backend/smb/smb_test.go | 21 +++++ fstest/test_all/config.yaml | 7 ++ fstest/testserver/init.d/PORTS.md | 2 + .../testserver/init.d/TestSMBKerberosCcache | 85 +++++++++++++++++ 8 files changed, 264 insertions(+), 39 deletions(-) create mode 100644 backend/smb/kerberos_test.go create mode 100755 fstest/testserver/init.d/TestSMBKerberosCcache diff --git a/backend/smb/connpool.go b/backend/smb/connpool.go index 1120533a8..9067b1f05 100644 --- a/backend/smb/connpool.go +++ b/backend/smb/connpool.go @@ -38,7 +38,7 @@ func (f *Fs) dial(ctx context.Context, network, addr string) (*conn, error) { d := &smb2.Dialer{} if f.opt.UseKerberos { - cl, err := getKerberosClient() + cl, err := createKerberosClient(f.opt.KerberosCCache) if err != nil { return nil, err } diff --git a/backend/smb/kerberos.go b/backend/smb/kerberos.go index a00608de7..58414cda1 100644 --- a/backend/smb/kerberos.go +++ b/backend/smb/kerberos.go @@ -14,65 +14,81 @@ import ( ) var ( - kerberosClient *client.Client - kerberosErr error - kerberosOnce sync.Once + kerberosClient sync.Map // map[string]*client.Client + kerberosErr sync.Map // map[string]error ) -// getKerberosClient returns a Kerberos client that can be used to authenticate. -func getKerberosClient() (*client.Client, error) { - if kerberosClient == nil || kerberosErr == nil { - kerberosOnce.Do(func() { - kerberosClient, kerberosErr = createKerberosClient() - }) +func resolveCcachePath(ccachePath string) (string, error) { + if ccachePath == "" { + ccachePath = os.Getenv("KRB5CCNAME") } - return kerberosClient, kerberosErr -} - -// createKerberosClient creates a new Kerberos client. -func createKerberosClient() (*client.Client, error) { - cfgPath := os.Getenv("KRB5_CONFIG") - if cfgPath == "" { - cfgPath = "/etc/krb5.conf" - } - - cfg, err := config.Load(cfgPath) - if err != nil { - return nil, err - } - - // Determine the ccache location from the environment, falling back to the - // default location. - ccachePath := os.Getenv("KRB5CCNAME") switch { case strings.Contains(ccachePath, ":"): parts := strings.SplitN(ccachePath, ":", 2) - switch parts[0] { + prefix, path := parts[0], parts[1] + switch prefix { case "FILE": - ccachePath = parts[1] + return path, nil case "DIR": - primary, err := os.ReadFile(filepath.Join(parts[1], "primary")) + primary, err := os.ReadFile(filepath.Join(path, "primary")) if err != nil { - return nil, err + return "", err } - ccachePath = filepath.Join(parts[1], strings.TrimSpace(string(primary))) + return filepath.Join(path, strings.TrimSpace(string(primary))), nil default: - return nil, fmt.Errorf("unsupported KRB5CCNAME: %s", ccachePath) + return "", fmt.Errorf("unsupported KRB5CCNAME: %s", ccachePath) } case ccachePath == "": u, err := user.Current() if err != nil { - return nil, err + return "", err } - - ccachePath = "/tmp/krb5cc_" + u.Uid + return "/tmp/krb5cc_" + u.Uid, nil + default: + return ccachePath, nil } +} - ccache, err := credentials.LoadCCache(ccachePath) +func loadKerberosConfig() (*config.Config, error) { + cfgPath := os.Getenv("KRB5_CONFIG") + if cfgPath == "" { + cfgPath = "/etc/krb5.conf" + } + return config.Load(cfgPath) +} + +// createKerberosClient creates a new Kerberos client. +func createKerberosClient(ccachePath string) (*client.Client, error) { + ccachePath, err := resolveCcachePath(ccachePath) if err != nil { return nil, err } - return client.NewFromCCache(ccache, cfg) + // check if we already have a client or an error for this ccache path + if errVal, ok := kerberosErr.Load(ccachePath); ok { + return nil, errVal.(error) + } + if clientVal, ok := kerberosClient.Load(ccachePath); ok { + return clientVal.(*client.Client), nil + } + + // create a new client if not found in the map + cfg, err := loadKerberosConfig() + if err != nil { + kerberosErr.Store(ccachePath, err) + return nil, err + } + ccache, err := credentials.LoadCCache(ccachePath) + if err != nil { + kerberosErr.Store(ccachePath, err) + return nil, err + } + cl, err := client.NewFromCCache(ccache, cfg) + if err != nil { + kerberosErr.Store(ccachePath, err) + return nil, err + } + kerberosClient.Store(ccachePath, cl) + return cl, nil } diff --git a/backend/smb/kerberos_test.go b/backend/smb/kerberos_test.go new file mode 100644 index 000000000..4bfe6d240 --- /dev/null +++ b/backend/smb/kerberos_test.go @@ -0,0 +1,79 @@ +package smb + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolveCcachePath(t *testing.T) { + tmpDir := t.TempDir() + + // Setup: files for FILE and DIR modes + fileCcache := filepath.Join(tmpDir, "file_ccache") + err := os.WriteFile(fileCcache, []byte{}, 0600) + assert.NoError(t, err) + + dirCcache := filepath.Join(tmpDir, "dir_ccache") + err = os.Mkdir(dirCcache, 0755) + assert.NoError(t, err) + err = os.WriteFile(filepath.Join(dirCcache, "primary"), []byte("ticket"), 0600) + assert.NoError(t, err) + dirCcacheTicket := filepath.Join(dirCcache, "ticket") + err = os.WriteFile(dirCcacheTicket, []byte{}, 0600) + assert.NoError(t, err) + + tests := []struct { + name string + ccachePath string + envKRB5CCNAME string + expected string + expectError bool + }{ + { + name: "FILE: prefix from env", + ccachePath: "", + envKRB5CCNAME: "FILE:" + fileCcache, + expected: fileCcache, + }, + { + name: "DIR: prefix from env", + ccachePath: "", + envKRB5CCNAME: "DIR:" + dirCcache, + expected: dirCcacheTicket, + }, + { + name: "Unsupported prefix", + ccachePath: "", + envKRB5CCNAME: "MEMORY:/bad/path", + expectError: true, + }, + { + name: "Direct file path (no prefix)", + ccachePath: "/tmp/myccache", + expected: "/tmp/myccache", + }, + { + name: "Default to /tmp/krb5cc_", + ccachePath: "", + envKRB5CCNAME: "", + expected: "/tmp/krb5cc_", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("KRB5CCNAME", tt.envKRB5CCNAME) + result, err := resolveCcachePath(tt.ccachePath) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Contains(t, result, tt.expected) + } + }) + } +} diff --git a/backend/smb/smb.go b/backend/smb/smb.go index c6911fb76..7e53a39db 100644 --- a/backend/smb/smb.go +++ b/backend/smb/smb.go @@ -107,6 +107,20 @@ Set to 0 to keep connections indefinitely. Help: "Whether the server is configured to be case-insensitive.\n\nAlways true on Windows shares.", Default: true, Advanced: true, + }, { + Name: "kerberos_ccache", + Help: `Path to the Kerberos credential cache (krb5cc). + +Overrides the default KRB5CCNAME environment variable and allows this +instance of the SMB backend to use a different Kerberos cache file. +This is useful when mounting multiple SMB with different credentials +or running in multi-user environments. + +Supported formats: + - FILE:/path/to/ccache – Use the specified file. + - DIR:/path/to/ccachedir – Use the primary file inside the specified directory. + - /path/to/ccache – Interpreted as a file path.`, + Advanced: true, }, { Name: config.ConfigEncoding, Help: config.ConfigEncodingHelp, @@ -137,6 +151,7 @@ type Options struct { Domain string `config:"domain"` SPN string `config:"spn"` UseKerberos bool `config:"use_kerberos"` + KerberosCCache string `config:"kerberos_ccache"` HideSpecial bool `config:"hide_special_share"` CaseInsensitive bool `config:"case_insensitive"` IdleTimeout fs.Duration `config:"idle_timeout"` diff --git a/backend/smb/smb_test.go b/backend/smb/smb_test.go index 168966adb..1f77dd404 100644 --- a/backend/smb/smb_test.go +++ b/backend/smb/smb_test.go @@ -30,3 +30,24 @@ func TestIntegration2(t *testing.T) { NilObject: (*smb.Object)(nil), }) } + +func TestIntegration3(t *testing.T) { + if *fstest.RemoteName != "" { + t.Skip("skipping as -remote is set") + } + + krb5Dir := t.TempDir() + t.Setenv("KRB5_CONFIG", filepath.Join(krb5Dir, "krb5.conf")) + ccache := filepath.Join(krb5Dir, "ccache") + t.Setenv("RCLONE_TEST_CUSTOM_CCACHE_LOCATION", ccache) + + name := "TestSMBKerberosCcache" + + fstests.Run(t, &fstests.Opt{ + RemoteName: name + ":rclone", + NilObject: (*smb.Object)(nil), + ExtraConfig: []fstests.ExtraConfigItem{ + {Name: name, Key: "kerberos_ccache", Value: ccache}, + }, + }) +} diff --git a/fstest/test_all/config.yaml b/fstest/test_all/config.yaml index 500d2aacf..566cbe513 100644 --- a/fstest/test_all/config.yaml +++ b/fstest/test_all/config.yaml @@ -602,6 +602,13 @@ backends: - KRB5CCNAME=/tmp/rclone_krb5/ccache ignoretests: - cmd/gitannex + - backend: "smb" + remote: "TestSMBKerberosCcache:rclone" + fastlist: false + env: + - KRB5_CONFIG=/tmp/rclone_krb5_ccache/krb5.conf + ignoretests: + - cmd/gitannex - backend: "storj" remote: "TestStorj:" fastlist: true diff --git a/fstest/testserver/init.d/PORTS.md b/fstest/testserver/init.d/PORTS.md index c4962361f..d86f3767c 100644 --- a/fstest/testserver/init.d/PORTS.md +++ b/fstest/testserver/init.d/PORTS.md @@ -30,6 +30,8 @@ They should be bound to localhost so they are not accessible externally. | 28634 | TestSMBKerberos | | 28635 | TestS3Exaba | | 28636 | TestS3Exaba | +| 28637 | TestSMBKerberosCcache | +| 28638 | TestSMBKerberosCcache | | 38081 | TestWebdavOwncloud | ## Non localhost tests diff --git a/fstest/testserver/init.d/TestSMBKerberosCcache b/fstest/testserver/init.d/TestSMBKerberosCcache new file mode 100755 index 000000000..1238299c3 --- /dev/null +++ b/fstest/testserver/init.d/TestSMBKerberosCcache @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +set -e + +# Set default location for Kerberos config and ccache. Can be overridden by the caller +# using environment variables RCLONE_TEST_CUSTOM_CCACHE_LOCATION and KRB5_CONFIG. +export TEMP_DIR=/tmp/rclone_krb5_ccache +mkdir -p "${TEMP_DIR}" +export KRB5_CONFIG=${KRB5_CONFIG:-${TEMP_DIR}/krb5.conf} +export RCLONE_TEST_CUSTOM_CCACHE_LOCATION=${RCLONE_TEST_CUSTOM_CCACHE_LOCATION:-${TEMP_DIR}/ccache} + +IMAGE=rclone/test-smb-kerberos-ccache +NAME=smb-kerberos-ccache +USER=rclone +DOMAIN=RCLONE +REALM=RCLONE.LOCAL +SMB_PORT=28637 +KRB5_PORT=28638 + +. $(dirname "$0")/docker.bash + +start() { + docker build -t ${IMAGE} --load - <> /etc/samba/smb.conf +[global] +server signing = auto +[public] +path = /share +browseable = yes +read only = yes +guest ok = yes +[rclone] +path = /rclone +browseable = yes +read only = no +guest ok = no +valid users = rclone +EOS +CMD ["samba", "-i"] +EOF + + docker run --rm -d --name ${NAME} \ + -p 127.0.0.1:${SMB_PORT}:445 \ + -p 127.0.0.1:${SMB_PORT}:445/udp \ + -p 127.0.0.1:${KRB5_PORT}:88 \ + ${IMAGE} + + cat > "${KRB5_CONFIG}" <