mirror of
https://github.com/rclone/rclone.git
synced 2025-08-17 00:51:34 +02:00
smb: add --smb-kerberos-ccache option to set kerberos ccache per smb backend
This commit is contained in:
@ -38,7 +38,7 @@ func (f *Fs) dial(ctx context.Context, network, addr string) (*conn, error) {
|
|||||||
|
|
||||||
d := &smb2.Dialer{}
|
d := &smb2.Dialer{}
|
||||||
if f.opt.UseKerberos {
|
if f.opt.UseKerberos {
|
||||||
cl, err := getKerberosClient()
|
cl, err := createKerberosClient(f.opt.KerberosCCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -14,65 +14,81 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
kerberosClient *client.Client
|
kerberosClient sync.Map // map[string]*client.Client
|
||||||
kerberosErr error
|
kerberosErr sync.Map // map[string]error
|
||||||
kerberosOnce sync.Once
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// getKerberosClient returns a Kerberos client that can be used to authenticate.
|
func resolveCcachePath(ccachePath string) (string, error) {
|
||||||
func getKerberosClient() (*client.Client, error) {
|
if ccachePath == "" {
|
||||||
if kerberosClient == nil || kerberosErr == nil {
|
ccachePath = os.Getenv("KRB5CCNAME")
|
||||||
kerberosOnce.Do(func() {
|
|
||||||
kerberosClient, kerberosErr = createKerberosClient()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
switch {
|
||||||
case strings.Contains(ccachePath, ":"):
|
case strings.Contains(ccachePath, ":"):
|
||||||
parts := strings.SplitN(ccachePath, ":", 2)
|
parts := strings.SplitN(ccachePath, ":", 2)
|
||||||
switch parts[0] {
|
prefix, path := parts[0], parts[1]
|
||||||
|
switch prefix {
|
||||||
case "FILE":
|
case "FILE":
|
||||||
ccachePath = parts[1]
|
return path, nil
|
||||||
case "DIR":
|
case "DIR":
|
||||||
primary, err := os.ReadFile(filepath.Join(parts[1], "primary"))
|
primary, err := os.ReadFile(filepath.Join(path, "primary"))
|
||||||
if err != nil {
|
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:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported KRB5CCNAME: %s", ccachePath)
|
return "", fmt.Errorf("unsupported KRB5CCNAME: %s", ccachePath)
|
||||||
}
|
}
|
||||||
case ccachePath == "":
|
case ccachePath == "":
|
||||||
u, err := user.Current()
|
u, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
return "/tmp/krb5cc_" + u.Uid, nil
|
||||||
ccachePath = "/tmp/krb5cc_" + u.Uid
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
||||||
}
|
}
|
||||||
|
79
backend/smb/kerberos_test.go
Normal file
79
backend/smb/kerberos_test.go
Normal file
@ -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_<uid>",
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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.",
|
Help: "Whether the server is configured to be case-insensitive.\n\nAlways true on Windows shares.",
|
||||||
Default: true,
|
Default: true,
|
||||||
Advanced: 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,
|
Name: config.ConfigEncoding,
|
||||||
Help: config.ConfigEncodingHelp,
|
Help: config.ConfigEncodingHelp,
|
||||||
@ -137,6 +151,7 @@ type Options struct {
|
|||||||
Domain string `config:"domain"`
|
Domain string `config:"domain"`
|
||||||
SPN string `config:"spn"`
|
SPN string `config:"spn"`
|
||||||
UseKerberos bool `config:"use_kerberos"`
|
UseKerberos bool `config:"use_kerberos"`
|
||||||
|
KerberosCCache string `config:"kerberos_ccache"`
|
||||||
HideSpecial bool `config:"hide_special_share"`
|
HideSpecial bool `config:"hide_special_share"`
|
||||||
CaseInsensitive bool `config:"case_insensitive"`
|
CaseInsensitive bool `config:"case_insensitive"`
|
||||||
IdleTimeout fs.Duration `config:"idle_timeout"`
|
IdleTimeout fs.Duration `config:"idle_timeout"`
|
||||||
|
@ -30,3 +30,24 @@ func TestIntegration2(t *testing.T) {
|
|||||||
NilObject: (*smb.Object)(nil),
|
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},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -602,6 +602,13 @@ backends:
|
|||||||
- KRB5CCNAME=/tmp/rclone_krb5/ccache
|
- KRB5CCNAME=/tmp/rclone_krb5/ccache
|
||||||
ignoretests:
|
ignoretests:
|
||||||
- cmd/gitannex
|
- cmd/gitannex
|
||||||
|
- backend: "smb"
|
||||||
|
remote: "TestSMBKerberosCcache:rclone"
|
||||||
|
fastlist: false
|
||||||
|
env:
|
||||||
|
- KRB5_CONFIG=/tmp/rclone_krb5_ccache/krb5.conf
|
||||||
|
ignoretests:
|
||||||
|
- cmd/gitannex
|
||||||
- backend: "storj"
|
- backend: "storj"
|
||||||
remote: "TestStorj:"
|
remote: "TestStorj:"
|
||||||
fastlist: true
|
fastlist: true
|
||||||
|
@ -30,6 +30,8 @@ They should be bound to localhost so they are not accessible externally.
|
|||||||
| 28634 | TestSMBKerberos |
|
| 28634 | TestSMBKerberos |
|
||||||
| 28635 | TestS3Exaba |
|
| 28635 | TestS3Exaba |
|
||||||
| 28636 | TestS3Exaba |
|
| 28636 | TestS3Exaba |
|
||||||
|
| 28637 | TestSMBKerberosCcache |
|
||||||
|
| 28638 | TestSMBKerberosCcache |
|
||||||
| 38081 | TestWebdavOwncloud |
|
| 38081 | TestWebdavOwncloud |
|
||||||
|
|
||||||
## Non localhost tests
|
## Non localhost tests
|
||||||
|
85
fstest/testserver/init.d/TestSMBKerberosCcache
Executable file
85
fstest/testserver/init.d/TestSMBKerberosCcache
Executable file
@ -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 - <<EOF
|
||||||
|
FROM alpine:3.21
|
||||||
|
RUN apk add --no-cache samba-dc
|
||||||
|
RUN rm -rf /etc/samba/smb.conf /var/lib/samba \
|
||||||
|
&& mkdir -p /var/lib/samba/private \
|
||||||
|
&& samba-tool domain provision \
|
||||||
|
--use-rfc2307 \
|
||||||
|
--option acl_xattr:security_acl_name=user.NTACL \
|
||||||
|
--realm=$REALM \
|
||||||
|
--domain=$DOMAIN \
|
||||||
|
--server-role=dc \
|
||||||
|
--dns-backend=SAMBA_INTERNAL \
|
||||||
|
--host-name=localhost \
|
||||||
|
&& samba-tool user add --random-password $USER \
|
||||||
|
&& samba-tool user setexpiry $USER --noexpiry \
|
||||||
|
&& mkdir -m 777 /share /rclone \
|
||||||
|
&& cat <<EOS >> /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}" <<EOF
|
||||||
|
[libdefaults]
|
||||||
|
default_realm = ${REALM}
|
||||||
|
[realms]
|
||||||
|
${REALM} = {
|
||||||
|
kdc = localhost
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
docker cp "${KRB5_CONFIG}" ${NAME}:/etc/krb5.conf
|
||||||
|
docker exec ${NAME} samba-tool user get-kerberos-ticket rclone --output-krb5-ccache=/tmp/ccache
|
||||||
|
docker cp ${NAME}:/tmp/ccache "${RCLONE_TEST_CUSTOM_CCACHE_LOCATION}"
|
||||||
|
sed -i -e "s/localhost/localhost:${KRB5_PORT}/" "${KRB5_CONFIG}"
|
||||||
|
|
||||||
|
echo type=smb
|
||||||
|
echo host=localhost
|
||||||
|
echo port=$SMB_PORT
|
||||||
|
echo use_kerberos=true
|
||||||
|
echo kerberos_ccache=${RCLONE_TEST_CUSTOM_CCACHE_LOCATION}
|
||||||
|
echo _connect=127.0.0.1:${SMB_PORT}
|
||||||
|
}
|
||||||
|
|
||||||
|
. $(dirname "$0")/run.bash
|
Reference in New Issue
Block a user