diff --git a/connector-ldap/README.md b/connector-ldap/README.md new file mode 100644 index 00000000..93973de4 --- /dev/null +++ b/connector-ldap/README.md @@ -0,0 +1,22 @@ +# LDAP connector +> LDAP connector is a plug-in designed to support login via LDAP or Active Directory, including LDAPS and StartTLS. + +## How to use + +### Build +```bash +./answer build --with github.com/apache/answer-plugins/connector-ldap +``` + +### Configuration +- `Name` - Display name for the connector shown on the login page +- `Server` - LDAP server URL, e.g. `ldaps://ldap.example.com:636` or `ldap://ldap.example.com:389` +- `Base DN` - Base DN to search for users, e.g. `dc=example,dc=com` +- `Bind DN` - DN of the service account used to bind and search the directory +- `Bind Password` - Password for the bind DN +- `User Attribute` - LDAP attribute holding the username, e.g. `uid` for OpenLDAP or `sAMAccountName` for Active Directory +- `TLS CA Certificate Path` - Optional path to a custom CA certificate, used to verify the LDAP server's certificate (e.g. for a private/internal CA) + +If the server URL starts with `ldaps://`, the connection is established over TLS directly. Otherwise, a plain connection is opened and upgraded via StartTLS. + +Users must have a `mail` attribute set in LDAP, since it is required to create/match the Answer account on login. diff --git a/connector-ldap/go.mod b/connector-ldap/go.mod new file mode 100644 index 00000000..a80cc115 --- /dev/null +++ b/connector-ldap/go.mod @@ -0,0 +1,62 @@ +module github.com/apache/answer-plugins/connector-ldap + +go 1.22.0 + +toolchain go1.24.2 + +require ( + github.com/apache/answer v1.4.2-RC1.0.20250107023923-061894735091 + github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954 + github.com/gin-gonic/gin v1.10.0 + github.com/go-ldap/ldap/v3 v3.4.10 + github.com/jimlambrt/gldap v0.1.14 + github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f +) + +require ( + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bytedance/sonic v1.12.2 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cenkalti/backoff v2.2.1+incompatible // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/google/wire v0.5.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f // indirect + github.com/stretchr/testify v1.9.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.10.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/connector-ldap/go.sum b/connector-ldap/go.sum new file mode 100644 index 00000000..635e0565 --- /dev/null +++ b/connector-ldap/go.sum @@ -0,0 +1,241 @@ +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= +github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/apache/answer v1.4.2-RC1.0.20250107023923-061894735091 h1:TmUPU0tX3VzbUO7rCBW3hJDUGO/WOI343zcbymIWQSM= +github.com/apache/answer v1.4.2-RC1.0.20250107023923-061894735091/go.mod h1:ehAJmrP4X9kBJKlYzTtRrwRbKeRqjnHxyYlhf11yzfw= +github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954 h1:jVqxzyeHvYxAT30vGfHXtZLNoi9qAWnvTtuMSj6pFys= +github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954/go.mod h1:wQEKNXVa/BKKq5yro9qo5bFiO3/SW1noORabxEndk3o= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= +github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= +github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +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/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jimlambrt/gldap v0.1.14 h1:InG9kldhIu6OoQK0hvfkW1Lqpc5eLJhxiiDTNmRnrDM= +github.com/jimlambrt/gldap v0.1.14/go.mod h1:yobW9JIAmqe23dVNOaMWewPaff6jGaHgYjspPIIgYmg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f h1:xia6AXJor4UV4T6htmHlfN7CGXZ04vlWwybVtFKJ/mA= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= +golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/connector-ldap/i18n/de_DE.yaml b/connector-ldap/i18n/de_DE.yaml new file mode 100644 index 00000000..e50e7db9 --- /dev/null +++ b/connector-ldap/i18n/de_DE.yaml @@ -0,0 +1,31 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +plugin: + ldap_connector: + backend: + login: + title: + other: Anmeldung + subtitle: + other: Mit LDAP-Konto anmelden + username: + other: Benutzername + password: + other: Passwort + submit: + other: Anmelden diff --git a/connector-ldap/i18n/en_US.yaml b/connector-ldap/i18n/en_US.yaml new file mode 100644 index 00000000..ad71ff86 --- /dev/null +++ b/connector-ldap/i18n/en_US.yaml @@ -0,0 +1,79 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +plugin: + ldap_connector: + backend: + name: + other: LDAP + info: + name: + other: LDAP Connector + description: + other: Connect to LDAP for third-party login + config: + name: + title: + other: LDAP + description: + other: LDAP connector name + server: + title: + other: LDAP Server + description: + other: e.g. ldaps://ldap.example.com:636 + base_dn: + title: + other: Base DN + description: + other: e.g. dc=example,dc=com + bind_dn: + title: + other: Bind DN + description: + other: DN of LDAP bind user + bind_password: + title: + other: Bind Password + description: + other: Password for bind DN + user_attr: + title: + other: User Attribute + description: + other: LDAP attribute for username (e.g., uid or sAMAccountName) + external_id_attr: + title: + other: External ID Attribute + description: + other: Stable LDAP attribute used to identify the user across logins, e.g. entryUUID (OpenLDAP) or objectGUID (Active Directory). Do not use a mutable attribute like uid. + tls_ca_cert_path: + title: + other: TLS CA Certificate Path + description: + other: Path to custom CA certificate file (optional) + login: + title: + other: Login + subtitle: + other: Sign in with your LDAP account + username: + other: Username + password: + other: Password + submit: + other: Login diff --git a/connector-ldap/i18n/translation.go b/connector-ldap/i18n/translation.go new file mode 100644 index 00000000..923c020c --- /dev/null +++ b/connector-ldap/i18n/translation.go @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package i18n + +const ( + ConnectorName = "plugin.ldap_connector.backend.name" + InfoName = "plugin.ldap_connector.backend.info.name" + InfoDescription = "plugin.ldap_connector.backend.info.description" + + ConfigNameTitle = "plugin.ldap_connector.backend.config.name.title" + ConfigNameDescription = "plugin.ldap_connector.backend.config.name.description" + ConfigServerTitle = "plugin.ldap_connector.backend.config.server.title" + ConfigServerDescription = "plugin.ldap_connector.backend.config.server.description" + ConfigBaseDNTitle = "plugin.ldap_connector.backend.config.base_dn.title" + ConfigBaseDNDescription = "plugin.ldap_connector.backend.config.base_dn.description" + ConfigBindDNTitle = "plugin.ldap_connector.backend.config.bind_dn.title" + ConfigBindDNDescription = "plugin.ldap_connector.backend.config.bind_dn.description" + ConfigBindPasswordTitle = "plugin.ldap_connector.backend.config.bind_password.title" + ConfigBindPasswordDescription = "plugin.ldap_connector.backend.config.bind_password.description" + ConfigUserAttrTitle = "plugin.ldap_connector.backend.config.user_attr.title" + ConfigUserAttrDescription = "plugin.ldap_connector.backend.config.user_attr.description" + ConfigExternalIDAttrTitle = "plugin.ldap_connector.backend.config.external_id_attr.title" + ConfigExternalIDAttrDescription = "plugin.ldap_connector.backend.config.external_id_attr.description" + ConfigTLSCACertPathTitle = "plugin.ldap_connector.backend.config.tls_ca_cert_path.title" + ConfigTLSCACertPathDescription = "plugin.ldap_connector.backend.config.tls_ca_cert_path.description" + + LoginTitle = "plugin.ldap_connector.backend.login.title" + LoginSubtitle = "plugin.ldap_connector.backend.login.subtitle" + LoginUsername = "plugin.ldap_connector.backend.login.username" + LoginPassword = "plugin.ldap_connector.backend.login.password" + LoginSubmit = "plugin.ldap_connector.backend.login.submit" +) diff --git a/connector-ldap/info.yaml b/connector-ldap/info.yaml new file mode 100644 index 00000000..62121270 --- /dev/null +++ b/connector-ldap/info.yaml @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +slug_name: ldap_connector +type: connector +version: 0.1.0 +author: DanielAuerX +link: https://github.com/apache/answer-plugins/tree/main/connector-ldap diff --git a/connector-ldap/ldap.go b/connector-ldap/ldap.go new file mode 100644 index 00000000..2cd321a9 --- /dev/null +++ b/connector-ldap/ldap.go @@ -0,0 +1,412 @@ +package ldap + +import ( + "crypto/tls" + "crypto/x509" + "embed" + "encoding/binary" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "strings" + + "github.com/apache/answer-plugins/connector-ldap/i18n" + "github.com/segmentfault/pacman/log" + + "github.com/apache/answer-plugins/util" + "github.com/apache/answer/plugin" + "github.com/go-ldap/ldap/v3" +) + +//go:embed info.yaml +var Info embed.FS + +//go:embed login.html +var loginHTML embed.FS + +const ( + LdapAttributeDn = "dn" + LdapAttributeUid = "uid" + LdapAttributeCn = "cn" + LdapAttributeMail = "mail" + LdapAttributeDisplayName = "displayName" + LdapAttributeSamAccountName = "sAMAccountName" + LdapAttributeObjectGUID = "objectGUID" + + DefaultExternalIDAttr = "entryUUID" +) + +type Connector struct { + Config *ConnectorConfig +} + +type ConnectorConfig struct { + Name string `json:"name"` + Server string `json:"server"` + BaseDN string `json:"base_dn"` + BindDN string `json:"bind_dn"` + BindPassword string `json:"bind_password"` + UserAttr string `json:"user_attr"` + ExternalIDAttr string `json:"external_id_attr"` + TLSCACertPath string `json:"tls_ca_cert_path"` +} + +var _ plugin.Connector = &Connector{} +var _ plugin.ConnectorStateRequired = &Connector{} + +var loginHTMLContent string + +func init() { + plugin.Register(&Connector{ + Config: &ConnectorConfig{}, + }) + + htmlContent, err := loginHTML.ReadFile("login.html") + if err != nil { + log.Errorf("failed to read embedded html file: %v", err) + } + loginHTMLContent = string(htmlContent) + if "" == loginHTMLContent { + log.Error("html file is empty") + } +} + +func (g *Connector) Info() plugin.Info { + info := &util.Info{} + info.GetInfo(Info) + + return plugin.Info{ + Name: plugin.MakeTranslator(i18n.InfoName), + SlugName: info.SlugName, + Description: plugin.MakeTranslator(i18n.InfoDescription), + Author: info.Author, + Version: info.Version, + Link: info.Link, + } +} + +func (g *Connector) ConnectorName() plugin.Translator { + if g.Config.Name != "" { + return plugin.MakeTranslator(g.Config.Name) + } + return plugin.MakeTranslator(i18n.ConnectorName) +} + +func (g *Connector) ConnectorSlugName() string { + return "ldap" + +} + +func (g *Connector) ConnectorLogoSVG() string { + return "" +} + +func (g *Connector) ConnectorRequireState() bool { + return true +} + +func (g *Connector) ConnectorSender(ctx *plugin.GinContext, receiverURL string) string { + + state := ctx.Request.URL.Query().Get("state") + if state != "" { + receiverURL = receiverURL + "?state=" + url.QueryEscape(state) + } + + htmlContent := strings.Replace(loginHTMLContent, "RECEIVER_URL_PLACEHOLDER", receiverURL, -1) + htmlContent = strings.Replace(htmlContent, "LOGIN_TITLE_PLACEHOLDER", plugin.Translate(ctx, i18n.LoginTitle), -1) + htmlContent = strings.Replace(htmlContent, "LOGIN_SUBTITLE_PLACEHOLDER", plugin.Translate(ctx, i18n.LoginSubtitle), -1) + htmlContent = strings.Replace(htmlContent, "LOGIN_USERNAME_PLACEHOLDER", plugin.Translate(ctx, i18n.LoginUsername), -1) + htmlContent = strings.Replace(htmlContent, "LOGIN_PASSWORD_PLACEHOLDER", plugin.Translate(ctx, i18n.LoginPassword), -1) + htmlContent = strings.Replace(htmlContent, "LOGIN_SUBMIT_PLACEHOLDER", plugin.Translate(ctx, i18n.LoginSubmit), -1) + ctx.Writer.WriteHeader(200) + ctx.Writer.Header().Set("Content-Type", "text/html") + err := writeHtmlContent(ctx, htmlContent) + if err != nil { + log.Errorf("failed to write HTML response: %v", err) + } + + return "" +} + +func writeHtmlContent(ctx *plugin.GinContext, htmlContent string) error { + ctx.Writer.WriteHeader(200) + ctx.Writer.Header().Set("Content-Type", "text/html") + _, err := ctx.Writer.Write([]byte(htmlContent)) + return err +} + +func (g *Connector) ConfigFields() []plugin.ConfigField { + return []plugin.ConfigField{ + createTextInput("name", i18n.ConfigNameTitle, i18n.ConfigNameDescription, g.Config.Name, true, false), + createTextInput("server", i18n.ConfigServerTitle, i18n.ConfigServerDescription, g.Config.Server, true, false), + createTextInput("base_dn", i18n.ConfigBaseDNTitle, i18n.ConfigBaseDNDescription, g.Config.BaseDN, true, false), + createTextInput("bind_dn", i18n.ConfigBindDNTitle, i18n.ConfigBindDNDescription, g.Config.BindDN, true, false), + createTextInput("bind_password", i18n.ConfigBindPasswordTitle, i18n.ConfigBindPasswordDescription, g.Config.BindPassword, true, true), + createTextInput("user_attr", i18n.ConfigUserAttrTitle, i18n.ConfigUserAttrDescription, g.Config.UserAttr, true, false), + createTextInput("external_id_attr", i18n.ConfigExternalIDAttrTitle, i18n.ConfigExternalIDAttrDescription, externalIDAttrOrDefault(g.Config.ExternalIDAttr), true, false), + createTextInput("tls_ca_cert_path", i18n.ConfigTLSCACertPathTitle, i18n.ConfigTLSCACertPathDescription, g.Config.TLSCACertPath, false, false), + } +} + +func (g *Connector) ConfigReceiver(config []byte) error { + c := &ConnectorConfig{} + if err := json.Unmarshal(config, c); err != nil { + return fmt.Errorf("invalid config json: %w", err) + } + g.Config = c + return nil +} + +func (c *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string) (userInfo plugin.ExternalLoginUserInfo, err error) { + + if err := checkSameOrigin(ctx.Request, receiverURL); err != nil { + return userInfo, fmt.Errorf("csrf check failed: %w", err) + } + + username, password, err := extractCredentials(ctx.Request) + if err != nil { + return userInfo, err + } + + l, err := connectLDAP(c.Config.Server, c.Config.TLSCACertPath) + if err != nil { + return userInfo, fmt.Errorf("failed to connect to LDAP server: %w", err) + } + defer l.Close() + + if err := bindServiceAccount(l, c.Config.BindDN, c.Config.BindPassword); err != nil { + return userInfo, fmt.Errorf("service account bind failed: %w", err) + } + + externalIDAttr := externalIDAttrOrDefault(c.Config.ExternalIDAttr) + + entry, err := searchUser(l, c.Config.BaseDN, c.Config.UserAttr, externalIDAttr, username) + if err != nil { + return userInfo, err + } + + err = l.Bind(entry.DN, password) + if err != nil { + return userInfo, fmt.Errorf("invalid username or password") + } + + userInfo, err = extractUserInfo(entry, externalIDAttr) + if err != nil { + return userInfo, err + } + + return userInfo, nil +} + +var connectLDAP = dialWithTLS + +func bindServiceAccount(l ldap.Client, bindDN, bindPassword string) error { + return l.Bind(bindDN, bindPassword) +} + +func searchUser(l ldap.Client, baseDN, userAttr, externalIDAttr, username string) (*ldap.Entry, error) { + attributes := []string{LdapAttributeDn, LdapAttributeUid, LdapAttributeCn, LdapAttributeMail, LdapAttributeDisplayName, LdapAttributeSamAccountName, externalIDAttr} + searchRequest := ldap.NewSearchRequest( + baseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, + fmt.Sprintf("(%s=%s)", userAttr, ldap.EscapeFilter(username)), + attributes, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil || len(sr.Entries) == 0 { + return nil, fmt.Errorf("user not found: %w", err) + } + + return sr.Entries[0], nil +} + +func checkSameOrigin(request *http.Request, receiverURL string) error { + expected, err := url.Parse(receiverURL) + if err != nil { + return fmt.Errorf("invalid receiver URL: %w", err) + } + + if origin := request.Header.Get("Origin"); origin != "" { + originURL, err := url.Parse(origin) + if err != nil || originURL.Scheme != expected.Scheme || originURL.Host != expected.Host { + return fmt.Errorf("request origin %q does not match site origin", origin) + } + return nil + } + + if referer := request.Header.Get("Referer"); referer != "" { + refererURL, err := url.Parse(referer) + if err != nil || refererURL.Scheme != expected.Scheme || refererURL.Host != expected.Host { + return fmt.Errorf("request referer %q does not match site origin", referer) + } + return nil + } + + return fmt.Errorf("missing Origin and Referer headers") +} + +func extractCredentials(request *http.Request) (username string, password string, err error) { + err = request.ParseForm() + if err != nil { + log.Errorf("failed to parse form: %v", err) + return "", "", err + } + + username = request.FormValue("username") + password = request.FormValue("password") + + if username == "" || password == "" { + log.Errorf("missing username and/or password") + err = fmt.Errorf("missing username or password") + } + return +} + +func extractUserInfo(entry *ldap.Entry, externalIDAttr string) (plugin.ExternalLoginUserInfo, error) { + + displayName := entry.GetAttributeValue(LdapAttributeDisplayName) + + if displayName == "" { + displayName = entry.GetAttributeValue(LdapAttributeCn) + } + + username := entry.GetAttributeValue(LdapAttributeUid) + if username == "" { + username = entry.GetAttributeValue(LdapAttributeSamAccountName) + } + + externalID, err := extractExternalID(entry, externalIDAttr) + if err != nil { + return plugin.ExternalLoginUserInfo{}, err + } + + email := entry.GetAttributeValue(LdapAttributeMail) + if email == "" { + return plugin.ExternalLoginUserInfo{}, fmt.Errorf("email is required") + } + + return plugin.ExternalLoginUserInfo{ + ExternalID: externalID, + DisplayName: displayName, + Username: username, + Email: email, + }, nil +} + +func externalIDAttrOrDefault(externalIDAttr string) string { + if externalIDAttr == "" { + return DefaultExternalIDAttr + } + return externalIDAttr +} + +func extractExternalID(entry *ldap.Entry, externalIDAttr string) (string, error) { + if externalIDAttr == LdapAttributeObjectGUID { + raw := entry.GetRawAttributeValue(externalIDAttr) + if len(raw) != 16 { + return "", fmt.Errorf("missing or invalid %s attribute", externalIDAttr) + } + return formatObjectGUID(raw), nil + } + + externalID := entry.GetAttributeValue(externalIDAttr) + if externalID == "" { + return "", fmt.Errorf("missing %s attribute", externalIDAttr) + } + return externalID, nil +} + +func formatObjectGUID(guid []byte) string { + return fmt.Sprintf("%08x-%04x-%04x-%x-%x", + binary.LittleEndian.Uint32(guid[0:4]), + binary.LittleEndian.Uint16(guid[4:6]), + binary.LittleEndian.Uint16(guid[6:8]), + guid[8:10], + guid[10:16], + ) +} + +func createTextInput(name, title, desc, value string, require bool, password bool) plugin.ConfigField { + uiOptions := plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + } + if password { + uiOptions = plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypePassword, + } + } + return plugin.ConfigField{ + Name: name, + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(title), + Description: plugin.MakeTranslator(desc), + Required: require, + UIOptions: uiOptions, + Value: value, + } +} + +func createBoolInput(name, title, desc string, value bool, require bool) plugin.ConfigField { + return plugin.ConfigField{ + + Name: name, + Type: plugin.ConfigTypeCheckbox, + Title: plugin.MakeTranslator(title), + Description: plugin.MakeTranslator(desc), + Required: require, + UIOptions: plugin.ConfigFieldUIOptions{}, + Value: value, + } + +} + +func dialWithTLS(server string, certPath string) (ldap.Client, error) { + + serverURL, err := url.Parse(server) + if err != nil { + return nil, fmt.Errorf("invalid LDAP server URL: %w", err) + } + + tlsConfig := &tls.Config{ + InsecureSkipVerify: false, + ServerName: serverURL.Hostname(), + } + + if certPath != "" { + certPool := x509.NewCertPool() + certData, err := os.ReadFile(certPath) + if err != nil { + log.Errorf("failed to read cert file: %v", err) + return nil, fmt.Errorf("failed to read LDAP cert: %w", err) + } + + if !certPool.AppendCertsFromPEM(certData) { + log.Errorf("failed to append cert from %s", certPath) + return nil, fmt.Errorf("failed to append cert") + } + + tlsConfig.RootCAs = certPool + } + + if strings.HasPrefix(server, "ldaps://") { + return ldap.DialURL(server, ldap.DialWithTLSConfig(tlsConfig)) + } + + conn, err := ldap.DialURL(server) + if err != nil { + log.Errorf("initial plain connection failed: %v", err) + return nil, err + } + + if err := conn.StartTLS(tlsConfig); err != nil { + log.Errorf("startTLS failed: %v", err) + conn.Close() + return nil, err + } + + return conn, nil +} diff --git a/connector-ldap/ldap_integration_test.go b/connector-ldap/ldap_integration_test.go new file mode 100644 index 00000000..67ba06ea --- /dev/null +++ b/connector-ldap/ldap_integration_test.go @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package ldap + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/jimlambrt/gldap" + "github.com/jimlambrt/gldap/testdirectory" +) + +func newTestDirectoryEntry() *gldap.Entry { + dn := "uid=" + testUsername + "," + testBaseDN + return gldap.NewEntry(dn, map[string][]string{ + LdapAttributeUid: {testUsername}, + LdapAttributeCn: {testUsername}, + LdapAttributeMail: {testEmail}, + DefaultExternalIDAttr: {testExternalID}, + "password": {testPassword}, + }) +} + +func caCertFile(t *testing.T, pemCert string) string { + path := filepath.Join(t.TempDir(), "ca.crt") + if err := os.WriteFile(path, []byte(pemCert), 0o600); err != nil { + t.Fatal(err) + } + return path +} + +func TestIntegration_StartTLS(t *testing.T) { + td := testdirectory.Start(t, testdirectory.WithNoTLS(t)) + td.SetUsers(newTestDirectoryEntry()) + + c := &Connector{Config: &ConnectorConfig{ + Server: fmt.Sprintf("ldap://%s:%d", td.Host(), td.Port()), + BaseDN: testBaseDN, + BindDN: "uid=" + testUsername + "," + testBaseDN, + BindPassword: testPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + TLSCACertPath: caCertFile(t, td.Cert()), + }} + + userInfo, err := c.ConnectorReceiver(loginRequest(testUsername, testPassword), testReceiverURL) + if err != nil { + t.Fatal(err) + } + if userInfo.ExternalID != testExternalID { + t.Fatalf("expected external ID %q, got %q", testExternalID, userInfo.ExternalID) + } +} + +func TestIntegration_LDAPS(t *testing.T) { + td := testdirectory.Start(t) + td.SetUsers(newTestDirectoryEntry()) + + c := &Connector{Config: &ConnectorConfig{ + Server: fmt.Sprintf("ldaps://%s:%d", td.Host(), td.Port()), + BaseDN: testBaseDN, + BindDN: "uid=" + testUsername + "," + testBaseDN, + BindPassword: testPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + TLSCACertPath: caCertFile(t, td.Cert()), + }} + + userInfo, err := c.ConnectorReceiver(loginRequest(testUsername, testPassword), testReceiverURL) + if err != nil { + t.Fatal(err) + } + if userInfo.ExternalID != testExternalID { + t.Fatalf("expected external ID %q, got %q", testExternalID, userInfo.ExternalID) + } +} + +func TestIntegration_PrivateCA_RejectedWithoutIt(t *testing.T) { + td := testdirectory.Start(t) + td.SetUsers(newTestDirectoryEntry()) + + c := &Connector{Config: &ConnectorConfig{ + Server: fmt.Sprintf("ldaps://%s:%d", td.Host(), td.Port()), + BaseDN: testBaseDN, + BindDN: "uid=" + testUsername + "," + testBaseDN, + BindPassword: testPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + }} + + _, err := c.ConnectorReceiver(loginRequest(testUsername, testPassword), testReceiverURL) + if err == nil { + t.Fatal("expected the connection to fail without the private CA configured") + } +} diff --git a/connector-ldap/ldap_test.go b/connector-ldap/ldap_test.go new file mode 100644 index 00000000..378eb7a0 --- /dev/null +++ b/connector-ldap/ldap_test.go @@ -0,0 +1,364 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package ldap + +import ( + "context" + "crypto/tls" + "fmt" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/gin-gonic/gin" + goldap "github.com/go-ldap/ldap/v3" +) + +type mockLDAPClient struct { + bindFunc func(username, password string) error + searchFunc func(req *goldap.SearchRequest) (*goldap.SearchResult, error) +} + +var _ goldap.Client = &mockLDAPClient{} + +const ( + testUsername = "mickeyMouse" + testPassword = "iLoveMinnieMouse" + testEmail = "mickey@test.com" + testExternalID = "01234567-89ab-cdef-0123-456789abcdef" + testServer = "ldap://localhost:389" + testBaseDN = "dc=example,dc=com" + testBindDN = "cn=admin,dc=example,dc=com" + testBindPassword = "admin-password" + testReceiverURL = "http://localhost:8080/answer/api/v1/connector/redirect/ldap" + testOrigin = "http://localhost:8080" +) + +func (m *mockLDAPClient) Start() {} +func (m *mockLDAPClient) StartTLS(*tls.Config) error { return nil } +func (m *mockLDAPClient) Close() error { return nil } +func (m *mockLDAPClient) GetLastError() error { return nil } +func (m *mockLDAPClient) IsClosing() bool { return false } +func (m *mockLDAPClient) SetTimeout(time.Duration) {} +func (m *mockLDAPClient) TLSConnectionState() (tls.ConnectionState, bool) { + return tls.ConnectionState{}, false +} + +func (m *mockLDAPClient) Bind(username, password string) error { + return m.bindFunc(username, password) +} +func (m *mockLDAPClient) UnauthenticatedBind(username string) error { return nil } +func (m *mockLDAPClient) SimpleBind(*goldap.SimpleBindRequest) (*goldap.SimpleBindResult, error) { + return nil, nil +} +func (m *mockLDAPClient) ExternalBind() error { return nil } +func (m *mockLDAPClient) NTLMUnauthenticatedBind(domain, username string) error { return nil } +func (m *mockLDAPClient) Unbind() error { return nil } + +func (m *mockLDAPClient) Add(*goldap.AddRequest) error { return nil } +func (m *mockLDAPClient) Del(*goldap.DelRequest) error { return nil } +func (m *mockLDAPClient) Modify(*goldap.ModifyRequest) error { return nil } +func (m *mockLDAPClient) ModifyDN(*goldap.ModifyDNRequest) error { + return nil +} +func (m *mockLDAPClient) ModifyWithResult(*goldap.ModifyRequest) (*goldap.ModifyResult, error) { + return nil, nil +} +func (m *mockLDAPClient) Extended(*goldap.ExtendedRequest) (*goldap.ExtendedResponse, error) { + return nil, nil +} + +func (m *mockLDAPClient) Compare(dn, attribute, value string) (bool, error) { + return false, nil +} +func (m *mockLDAPClient) PasswordModify(*goldap.PasswordModifyRequest) (*goldap.PasswordModifyResult, error) { + return nil, nil +} + +func (m *mockLDAPClient) Search(req *goldap.SearchRequest) (*goldap.SearchResult, error) { + return m.searchFunc(req) +} +func (m *mockLDAPClient) SearchAsync(ctx context.Context, req *goldap.SearchRequest, bufferSize int) goldap.Response { + return nil +} +func (m *mockLDAPClient) SearchWithPaging(*goldap.SearchRequest, uint32) (*goldap.SearchResult, error) { + return nil, nil +} +func (m *mockLDAPClient) DirSync(req *goldap.SearchRequest, flags, maxAttrCount int64, cookie []byte) (*goldap.SearchResult, error) { + return nil, nil +} +func (m *mockLDAPClient) DirSyncAsync(ctx context.Context, req *goldap.SearchRequest, bufferSize int, flags, maxAttrCount int64, cookie []byte) goldap.Response { + return nil +} +func (m *mockLDAPClient) Syncrepl(ctx context.Context, req *goldap.SearchRequest, bufferSize int, mode goldap.ControlSyncRequestMode, cookie []byte, reloadHint bool) goldap.Response { + return nil +} + +func withMockLDAP(t *testing.T, mock *mockLDAPClient) { + original := connectLDAP + connectLDAP = func(server, certPath string) (goldap.Client, error) { + return mock, nil + } + t.Cleanup(func() { connectLDAP = original }) +} + +func testEntry(username, email, externalIDAttr, externalIDValue string) *goldap.Entry { + return &goldap.Entry{ + DN: "uid=" + username + "," + testBaseDN, + Attributes: []*goldap.EntryAttribute{ + {Name: LdapAttributeUid, Values: []string{username}}, + {Name: LdapAttributeCn, Values: []string{username}}, + {Name: LdapAttributeMail, Values: []string{email}}, + {Name: externalIDAttr, Values: []string{externalIDValue}}, + }, + } +} + +func loginRequest(username, password string) *gin.Context { + form := url.Values{"username": {username}, "password": {password}} + req := httptest.NewRequest("POST", "/answer/api/v1/connector/redirect/ldap", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Origin", testOrigin) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + ctx.Request = req + return ctx +} + +func TestConnector_SenderIncludesStateDespiteGinQueryCache(t *testing.T) { + c := &Connector{Config: &ConnectorConfig{}} + + req := httptest.NewRequest("GET", "/answer/api/v1/connector/login/ldap", nil) + rec := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(rec) + ctx.Request = req + + ctx.Query("state") + ctx.Request.URL.RawQuery = "state=injected-after-cache" + + c.ConnectorSender(ctx, "https://example.com/answer/api/v1/connector/redirect/ldap") + + if !strings.Contains(rec.Body.String(), "state=injected-after-cache") { + t.Fatalf("expected rendered form action to include the state injected after gin's query cache was primed, got: %s", rec.Body.String()) + } +} + +func TestConnector_SuccessfulLogin(t *testing.T) { + entry := testEntry(testUsername, testEmail, DefaultExternalIDAttr, testExternalID) + withMockLDAP(t, &mockLDAPClient{ + bindFunc: func(username, password string) error { + if username == testBindDN { + return nil + } + if username == entry.DN && password == testPassword { + return nil + } + return fmt.Errorf("invalid credentials") + }, + searchFunc: func(req *goldap.SearchRequest) (*goldap.SearchResult, error) { + return &goldap.SearchResult{Entries: []*goldap.Entry{entry}}, nil + }, + }) + + c := &Connector{Config: &ConnectorConfig{ + Server: testServer, + BaseDN: testBaseDN, + BindDN: testBindDN, + BindPassword: testBindPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + }} + + userInfo, err := c.ConnectorReceiver(loginRequest(testUsername, testPassword), testReceiverURL) + if err != nil { + t.Fatal(err) + } + if userInfo.Email != testEmail { + t.Fatalf("expected email %q, got %q", testEmail, userInfo.Email) + } + if userInfo.ExternalID != testExternalID { + t.Fatalf("expected external ID from entryUUID, got %q", userInfo.ExternalID) + } +} + +func TestConnector_ServiceAccountBindFailure(t *testing.T) { + withMockLDAP(t, &mockLDAPClient{ + bindFunc: func(username, password string) error { + return fmt.Errorf("invalid credentials") + }, + searchFunc: func(req *goldap.SearchRequest) (*goldap.SearchResult, error) { + t.Fatal("search should not be called when the service account bind fails") + return nil, nil + }, + }) + + c := &Connector{Config: &ConnectorConfig{ + Server: testServer, + BaseDN: testBaseDN, + BindDN: testBindDN, + BindPassword: "wrongPassword", + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + }} + + _, err := c.ConnectorReceiver(loginRequest(testUsername, "correctPassword"), testReceiverURL) + if err == nil { + t.Fatal("expected service account bind failure to produce an error") + } +} + +func TestConnector_UserNotFound(t *testing.T) { + withMockLDAP(t, &mockLDAPClient{ + bindFunc: func(username, password string) error { return nil }, + searchFunc: func(req *goldap.SearchRequest) (*goldap.SearchResult, error) { + return &goldap.SearchResult{Entries: nil}, nil + }, + }) + + c := &Connector{Config: &ConnectorConfig{ + Server: testServer, + BaseDN: testBaseDN, + BindDN: testBindDN, + BindPassword: testBindPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + }} + + _, err := c.ConnectorReceiver(loginRequest("foo", "bar"), testReceiverURL) + if err == nil { + t.Fatal("expected an error when the user search returns no entries") + } +} + +func TestConnector_WrongPassword(t *testing.T) { + entry := testEntry(testUsername, testEmail, DefaultExternalIDAttr, testExternalID) + withMockLDAP(t, &mockLDAPClient{ + bindFunc: func(username, password string) error { + if username == testBindDN { + return nil + } + return fmt.Errorf("invalid credentials") + }, + searchFunc: func(req *goldap.SearchRequest) (*goldap.SearchResult, error) { + return &goldap.SearchResult{Entries: []*goldap.Entry{entry}}, nil + }, + }) + + c := &Connector{Config: &ConnectorConfig{ + Server: testServer, + BaseDN: testBaseDN, + BindDN: testBindDN, + BindPassword: testBindPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + }} + + _, err := c.ConnectorReceiver(loginRequest(testUsername, "iAmAWrongPassword"), testReceiverURL) + if err == nil { + t.Fatal("expected wrong password to produce an error") + } +} + +func TestCheckSameOrigin_MatchingOrigin(t *testing.T) { + req := loginRequest(testUsername, testPassword).Request + if err := checkSameOrigin(req, testReceiverURL); err != nil { + t.Fatal(err) + } +} + +func TestCheckSameOrigin_MatchingReferer(t *testing.T) { + req := loginRequest(testUsername, testPassword).Request + req.Header.Del("Origin") + req.Header.Set("Referer", testReceiverURL+"?state=foo") + if err := checkSameOrigin(req, testReceiverURL); err != nil { + t.Fatal(err) + } +} + +func TestCheckSameOrigin_MismatchedOrigin(t *testing.T) { + req := loginRequest(testUsername, testPassword).Request + req.Header.Set("Origin", "https://definitely.not.evil.com") + if err := checkSameOrigin(req, testReceiverURL); err == nil { + t.Fatal("expected a mismatched Origin header to be rejected") + } +} + +func TestCheckSameOrigin_MissingHeaders(t *testing.T) { + req := loginRequest(testUsername, testPassword).Request + req.Header.Del("Origin") + if err := checkSameOrigin(req, testReceiverURL); err == nil { + t.Fatal("expected a request with no Origin or Referer header to be rejected") + } +} + +func TestExtractCredentials(t *testing.T) { + req := loginRequest(testUsername, testPassword).Request + username, password, err := extractCredentials(req) + if err != nil { + t.Fatal(err) + } + if username != testUsername || password != testPassword { + t.Fatalf("expected %q/%q, got %q/%q", testUsername, testPassword, username, password) + } +} + +func TestExtractCredentials_Missing(t *testing.T) { + req := loginRequest(testUsername, "").Request + if _, _, err := extractCredentials(req); err == nil { + t.Fatal("expected an error when the password is missing") + } +} + +func TestExtractUserInfo_MissingEmail(t *testing.T) { + entry := testEntry(testUsername, "", DefaultExternalIDAttr, testExternalID) + if _, err := extractUserInfo(entry, DefaultExternalIDAttr); err == nil { + t.Fatal("expected an error when the mail attribute is missing") + } +} + +func TestExtractExternalID_MissingAttribute(t *testing.T) { + entry := testEntry(testUsername, testEmail, DefaultExternalIDAttr, "") + entry.Attributes = entry.Attributes[:len(entry.Attributes)-1] + if _, err := extractExternalID(entry, DefaultExternalIDAttr); err == nil { + t.Fatal("expected an error when the configured external ID attribute is missing") + } +} + +func TestExtractExternalID_ObjectGUID(t *testing.T) { + entry := &goldap.Entry{ + DN: "cn=" + testUsername + "," + testBaseDN, + Attributes: []*goldap.EntryAttribute{ + { + Name: LdapAttributeObjectGUID, + ByteValues: [][]byte{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}, + }, + }, + } + + externalID, err := extractExternalID(entry, LdapAttributeObjectGUID) + if err != nil { + t.Fatal(err) + } + const expected = "04030201-0605-0807-090a-0b0c0d0e0f10" + if externalID != expected { + t.Fatalf("expected %q, got %q", expected, externalID) + } +} diff --git a/connector-ldap/login.html b/connector-ldap/login.html new file mode 100644 index 00000000..9605eb78 --- /dev/null +++ b/connector-ldap/login.html @@ -0,0 +1,139 @@ + + +
+ + +LOGIN_SUBTITLE_PLACEHOLDER
+