diff --git a/.circleci/config.yml b/.circleci/config.yml index 1d581d7c..bdb825b9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -116,8 +116,10 @@ jobs: environment: PGVERSION: "<< parameters.pgversion >>" DIST: "<< parameters.dist >>" + COMPOSE_FILE: docker-compose.yml:test/docker-compose.yml command: | - COMPOSE_FILE=docker-compose.yml:test/docker-compose.yml docker compose up --exit-code-from=test + docker compose pull + docker compose up --exit-code-from=test pkg: parameters: diff --git a/.gitignore b/.gitignore index c96270ae..2d7b3e04 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ docker-compose.override.yml # test/conftest.py creates .env files .env +test/samba.keytab diff --git a/docker-compose.yml b/docker-compose.yml index 9c30aef5..47c7b790 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,15 +6,16 @@ services: environment: REALM: bridoulou.fr ADMIN_PASS: 1Ntegral - DNS_BACKEND: "NONE" volumes: + - ./test/fixtures/samba/kerberos.sh:/docker-entrypoint-init.d/00-kerberos.sh + - ./test:/test # Use to export keytab in kerberos.sh - ./test/fixtures/samba/nominal.sh:/docker-entrypoint-init.d/95-nominal.sh - ./test/fixtures/samba/extra.sh:/docker-entrypoint-init.d/96-extra.sh hostname: samba1 domainname: ldap2pg.docker labels: com.dnsdock.alias: samba1.ldap2pg.docker - command: [-d=1] + command: [-d=3] postgres: image: postgres:${PGVERSION-16}-alpine diff --git a/go.mod b/go.mod index d30f714d..5a12b1f9 100644 --- a/go.mod +++ b/go.mod @@ -27,18 +27,27 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gosimple/unidecode v1.0.1 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.22.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index abf0fa7a..03dfab63 100644 --- a/go.sum +++ b/go.sum @@ -20,7 +20,9 @@ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsM github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es= github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= diff --git a/internal/ldap/client.go b/internal/ldap/client.go index 291fe906..9b0be4d2 100644 --- a/internal/ldap/client.go +++ b/internal/ldap/client.go @@ -6,12 +6,14 @@ import ( "log/slog" "net" "net/url" + "os" "strings" "time" "github.com/avast/retry-go/v4" "github.com/dalibo/ldap2pg/internal/perf" ldap3 "github.com/go-ldap/ldap/v3" + "github.com/go-ldap/ldap/v3/gssapi" ) type Client struct { @@ -90,6 +92,38 @@ func Connect() (client Client, err error) { } slog.Debug("LDAP SASL/DIGEST-MD5 bind.", "authcid", client.SaslAuthCID, "host", parsedURI.Host) err = client.Conn.MD5Bind(parsedURI.Host, client.SaslAuthCID, password) + case "GSSAPI": + // Get the principal + client.SaslAuthCID = k.String("SASL_AUTHCID") + ccache, ok := os.LookupEnv("KRB5CCNAME") + if ok { + ccache = strings.TrimPrefix(ccache, "FILE:") + } else { + uid := os.Getuid() + ccache = fmt.Sprintf("/tmp/krb5cc_%d", uid) + } + krb5confPath, ok := os.LookupEnv("KRB5_CONFIG") + if !ok { + krb5confPath = "/etc/krb5.conf" + } + slog.Debug("Initial SSPI client.", "ccache", ccache, "krb5conf", krb5confPath) + sspiClient, err := gssapi.NewClientFromCCache(ccache, krb5confPath) + if err != nil { + return client, err + } + defer sspiClient.Close() + // Build service Principal from URI. + var parsedURI *url.URL + parsedURI, err = url.Parse(client.URI) + if err != nil { + return client, err + } + spn := "ldap/" + strings.Split(parsedURI.Host, ":")[0] + slog.Debug("LDAP SASL/GSSAPI bind.", "principal", client.SaslAuthCID, "spn", spn) + err = client.Conn.GSSAPIBind(sspiClient, spn, client.SaslAuthCID) + if err != nil { + return client, err + } default: err = fmt.Errorf("unhandled SASL_MECH") } diff --git a/ldaprc b/ldaprc index 56fbbc2b..6f0227f7 100644 --- a/ldaprc +++ b/ldaprc @@ -4,3 +4,7 @@ TLS_REQCERT allow NETWORK_TIMEOUT 5 TIMEOUT 5 REFERRALS off +SASL_AUTHCID Administrator +# Disable canonicalization which trigger Kerberos SPN ldap/172.X.Y.Z instead of ldap/samba1.ldap2pg.docker. +# With canonicalisation, GSSAPI fails with "Server not found in Kerberos database". +SASL_NOCANON on diff --git a/test/docker-compose.yml b/test/docker-compose.yml index 569a9490..e15d96bd 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -1,6 +1,17 @@ version: '3' services: + samba1: + labels: + # Disable dnsdock on test. + com.dnsdock.alias: samba1.test.ldap2pg.docker + networks: + default: + aliases: + # Fake dnsdock for CI. + # This value is used in test/krb5.conf for KDC. + - samba1.ldap2pg.docker + test: image: dalibo/buildpack-python:${DIST-rockylinux8} volumes: diff --git a/test/entrypoint.sh b/test/entrypoint.sh index 4fb00c38..9c83d643 100755 --- a/test/entrypoint.sh +++ b/test/entrypoint.sh @@ -47,6 +47,9 @@ psql -tc "SELECT version();" # ldap-utils on CentOS does not read properly current ldaprc. Linking it in ~ # workaround this. ln -fsv "${PWD}/ldaprc" ~/ldaprc -retry ldapsearch -x -v -w "${LDAPPASSWORD}" -z none +retry ldapsearch -x -v -w "${LDAPPASSWORD}" -z none >/dev/null +export KRB5_CONFIG="${PWD}/test/krb5.conf" +kinit -V -k -t "${PWD}/test/samba.keytab" Administrator +LDAPURI="${LDAPURI/ldaps:/ldap:}" ldapsearch -v -Y GSSAPI -U Administrator >/dev/null "$python" -m pytest test/ "$@" diff --git a/test/fixtures/samba/kerberos.sh b/test/fixtures/samba/kerberos.sh new file mode 100644 index 00000000..8252ee7c --- /dev/null +++ b/test/fixtures/samba/kerberos.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -x +samba-tool spn add ldap/samba1.ldap2pg.docker SAMBA1$ +samba-tool spn add ldap/localhost SAMBA1$ +samba-tool spn add ldap/localhost.localdomain SAMBA1$ + +# Get Gateway (field 3) from default route (destination is 0.0.0.0). +gateway_hex="$(grep -E '^\w+\s+00000000' /proc/net/route | cut -f 3)" +gateway_bytes=( # IP is little endian. + $((16#${gateway_hex:6:2})) + $((16#${gateway_hex:4:2})) + $((16#${gateway_hex:2:2})) + $((16#${gateway_hex:0:2})) +) +printf -v gateway "%d.%d.%d.%d" "${gateway_bytes[@]}" + +samba-tool spn add "ldap/$gateway" SAMBA1$ +samba-tool spn list SAMBA1$ +samba-tool domain exportkeytab /test/samba.keytab --principal=Administrator +chown -v "$(stat -c %u:%g "${BASH_SOURCE[0]}")" /test/samba.keytab diff --git a/test/krb5.conf b/test/krb5.conf new file mode 100644 index 00000000..f6d7fe9c --- /dev/null +++ b/test/krb5.conf @@ -0,0 +1,18 @@ +[libdefaults] + ticket_lifetime = 24h + renew_lifetime = 7d + forwardable = true + rdns = false + default_realm = BRIDOULOU.FR + default_keytab_name = FILE:test/samba.keytab + +[realms] + BRIDOULOU.FR = { + # Requires dnsdock on network alias as in test/docker-compose.yml + kdc = samba1.ldap2pg.docker + admin_server = samba1.ldap2pg.docker + } + +[domain_realm] +.ldap2pg.docker = BRIDOULOU.FR +ldap2pg.docker = BRIDOULOU.FR diff --git a/test/test_config.py b/test/test_config.py index fe031507..eddfd7a7 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -32,16 +32,15 @@ def test_stdin(ldap2pg, capsys): assert 'stdinuser' in err -@pytest.mark.xfail(reason="Samba does not support SASL DIGEST-MD5.") +@pytest.mark.xfail( + 'CI' not in os.environ, + reason="Set CI=true to run GSSAPI test." +) def test_sasl(ldap2pg, capsys): env = dict( os.environ, - # py-ldap2pg reads non-standard var USER. - LDAPUSER='testsasl', - # ldap2pg requires explicit SASL_MECH, and standard SASL_AUTHID. - LDAPSASL_MECH='DIGEST-MD5', - LDAPSASL_AUTHCID='testsasl', - LDAPPASSWORD='voyage', + LDAPSASL_MECH='GSSAPI', + LDAPSASL_AUTHCID='Administrator', ) ldap2pg(config='ldap2pg.yml', verbose=True, _env=env)