From 630793682b4fb3ba91dfdd227bd442341a222b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Thu, 16 May 2024 15:32:08 +0200 Subject: [PATCH] Implement GSSAPI auth --- .circleci/config.yml | 4 +++- go.mod | 9 +++++++++ go.sum | 2 ++ internal/ldap/client.go | 34 ++++++++++++++++++++++++++++++++++ test/docker-compose.yml | 11 +++++++++++ test/entrypoint.sh | 5 ++++- test/krb5.conf | 1 + test/test_config.py | 13 ++++++------- 8 files changed, 70 insertions(+), 9 deletions(-) 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/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/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/krb5.conf b/test/krb5.conf index 5559c481..f6d7fe9c 100644 --- a/test/krb5.conf +++ b/test/krb5.conf @@ -8,6 +8,7 @@ [realms] BRIDOULOU.FR = { + # Requires dnsdock on network alias as in test/docker-compose.yml kdc = samba1.ldap2pg.docker admin_server = samba1.ldap2pg.docker } 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)