From 897dc43790e1e02128af223b2846e6fd297a0390 Mon Sep 17 00:00:00 2001 From: Thomas Miceli <27960254+thomiceli@users.noreply.github.com> Date: Fri, 9 May 2025 19:32:22 +0200 Subject: [PATCH] Add LDAP authentication (#470) * Introduce basic LDAP authentication. * Reformat LDAP code; use ldap in Git HTTP * lint --------- Co-authored-by: Santhosh Raju --- .gitignore | 4 +- config.yml | 12 ++++ docs/configuration/cheat-sheet.md | 7 ++- go.mod | 3 + go.sum | 73 +++++++++++++++++++++++ internal/auth/ldap/ldap.go | 64 +++++++++++++++++++++ internal/config/config.go | 6 ++ internal/web/handlers/auth/password.go | 80 ++++++++++++++++++++------ internal/web/handlers/git/http.go | 51 ++++++++++++---- templates/pages/admin_config.html | 13 +++++ 10 files changed, 284 insertions(+), 29 deletions(-) create mode 100644 internal/auth/ldap/ldap.go diff --git a/.gitignore b/.gitignore index ca7f8ea..d301008 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ gist.db .idea/ +.vscode/ .DS_Store /**/.DS_Store public/assets/* @@ -10,4 +11,5 @@ opengist build/ docs/.vitepress/dist/ docs/.vitepress/cache/ -helm/opengist/charts/ \ No newline at end of file +helm/opengist/charts/ +vendor/ diff --git a/config.yml b/config.yml index 5b47413..cde43f6 100644 --- a/config.yml +++ b/config.yml @@ -111,6 +111,18 @@ oidc.group-claim-name: # The name of the group that should receive admin rights oidc.admin-group: +# LDAP authentication configuration +# URL of the LDAP instance e.g: ldap://ldap.example.com:389 ; if not set, LDAP authentication is disabled +ldap.url: +# Bind DN to authenticate against the LDAP e.g: cn=read-only-admin,dc=example,dc=com +ldap.bind-dn: +# The password for the Bind DN. +ldap.bind-credentials: +# The Base DN to start search from e.g: ou=People,dc=example,dc=com +ldap.search-base: +# The filter to search against (the format string %s will be replaced with the username) e.g: (uid=%s) +ldap.search-filter: + # Instance name # Set your own custom name to be displayed instead of 'Opengist' custom.name: diff --git a/docs/configuration/cheat-sheet.md b/docs/configuration/cheat-sheet.md index af8eb83..ea319a2 100644 --- a/docs/configuration/cheat-sheet.md +++ b/docs/configuration/cheat-sheet.md @@ -36,10 +36,15 @@ aside: false | gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. | | gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. | | gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. | -| oidc.provider-name | OG_OIDC_PROVIDER_NAME | none | The name of the OIDC provider | +| oidc.provider-name | OG_OIDC_PROVIDER_NAME | none | The name of the OIDC provider | | oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. | | oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. | | oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. | +| ldap.url | OG_LDAP_URL | none | URL of the LDAP instance; if not set, LDAP authentication is disabled | +| ldap.bind-dn | OG_LDAP_BIND_DN | none | Bind DN to authenticate against the LDAP. e.g: cn=read-only-admin,dc=example,dc=com | +| ldap.bind-credentials | OG_LDAP_BIND_CREDENTIALS | none | The password for the Bind DN. | +| ldap.search-base | OG_LDAP_SEARCH_BASE | none | The Base DN to start search from. e.g: ou=People,dc=example,dc=com | +| ldap.search-filter | OG_LDAP_SEARCH_FILTER | none | The filter to search against (the format string %s will be replaced with the username). e.g: (uid=%s) | | custom.name | OG_CUSTOM_NAME | none | The name of your instance, to be displayed in the tab title | | custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. | | custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. | diff --git a/go.mod b/go.mod index c8acf74..86228df 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/blevesearch/bleve/v2 v2.5.0 github.com/dustin/go-humanize v1.0.1 github.com/glebarez/sqlite v1.11.0 + github.com/go-ldap/ldap/v3 v3.4.8 github.com/go-playground/validator/v10 v10.26.0 github.com/go-webauthn/webauthn v0.12.3 github.com/google/uuid v1.6.0 @@ -37,6 +38,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -66,6 +68,7 @@ require ( github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-chi/chi/v5 v5.2.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect diff --git a/go.sum b/go.sum index 2f6216d..d9a23a3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= @@ -12,6 +14,8 @@ github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3a github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +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/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -87,8 +91,12 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= +github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= +github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= 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= @@ -137,10 +145,15 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -151,6 +164,18 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -232,9 +257,11 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ 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.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.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -250,6 +277,7 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= @@ -262,37 +290,82 @@ go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= +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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-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.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/auth/ldap/ldap.go b/internal/auth/ldap/ldap.go new file mode 100644 index 0000000..964ed5c --- /dev/null +++ b/internal/auth/ldap/ldap.go @@ -0,0 +1,64 @@ +package ldap + +import ( + "fmt" + "github.com/go-ldap/ldap/v3" + "github.com/thomiceli/opengist/internal/config" +) + +func Enabled() bool { + return config.C.LDAPUrl != "" +} + +// Authenticate attempts to authenticate a user against the configured LDAP instance. +func Authenticate(username, password string) (bool, error) { + l, err := ldap.DialURL(config.C.LDAPUrl) + if err != nil { + return false, fmt.Errorf("unable to connect to URI: %v", config.C.LDAPUrl) + } + defer func(l *ldap.Conn) { + _ = l.Close() + }(l) + + // First bind with a read only user + err = l.Bind(config.C.LDAPBindDn, config.C.LDAPBindCredentials) + if err != nil { + return false, err + } + + searchFilter := fmt.Sprintf(config.C.LDAPSearchFilter, username) + searchRequest := ldap.NewSearchRequest( + config.C.LDAPSearchBase, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, + 0, + false, + searchFilter, + []string{"dn"}, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + return false, err + } + + if len(sr.Entries) != 1 { + return false, nil + } + + // Bind as the user to verify their password + err = l.Bind(sr.Entries[0].DN, password) + if err != nil { + return false, nil + } + + // Rebind as the read only user for any further queries + err = l.Bind(config.C.LDAPBindDn, config.C.LDAPBindCredentials) + if err != nil { + return false, err + } + + return true, nil +} diff --git a/internal/config/config.go b/internal/config/config.go index fabac84..1925fdb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -79,6 +79,12 @@ type config struct { MetricsEnabled bool `yaml:"metrics.enabled" env:"OG_METRICS_ENABLED"` + LDAPUrl string `yaml:"ldap.url" env:"OG_LDAP_URL"` + LDAPBindDn string `yaml:"ldap.bind-dn" env:"OG_LDAP_BIND_DN"` + LDAPBindCredentials string `yaml:"ldap.bind-credentials" env:"OG_LDAP_BIND_CREDENTIALS"` + LDAPSearchBase string `yaml:"ldap.search-base" env:"OG_LDAP_SEARCH_BASE"` + LDAPSearchFilter string `yaml:"ldap.search-filter" env:"OG_LDAP_SEARCH_FILTER"` + CustomName string `yaml:"custom.name" env:"OG_CUSTOM_NAME"` CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"` CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"` diff --git a/internal/web/handlers/auth/password.go b/internal/web/handlers/auth/password.go index 6405fa4..ff8ab5c 100644 --- a/internal/web/handlers/auth/password.go +++ b/internal/web/handlers/auth/password.go @@ -3,6 +3,7 @@ package auth import ( "errors" "github.com/rs/zerolog/log" + "github.com/thomiceli/opengist/internal/auth/ldap" passwordpkg "github.com/thomiceli/opengist/internal/auth/password" "github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/i18n" @@ -114,6 +115,7 @@ func ProcessLogin(ctx *context.Context) error { return ctx.ErrorRes(403, ctx.Tr("error.login-disabled-form"), nil) } + var user *db.User var err error sess := ctx.GetSession() @@ -121,26 +123,16 @@ func ProcessLogin(ctx *context.Context) error { if err = ctx.Bind(dto); err != nil { return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err) } - password := dto.Password - var user *db.User - - if user, err = db.GetUserByUsername(dto.Username); err != nil { - if !errors.Is(err, gorm.ErrRecordNotFound) { - return ctx.ErrorRes(500, "Cannot get user", err) + if ldap.Enabled() { + if user, err = tryLdapLogin(ctx, dto.Username, dto.Password); err != nil { + return err } - log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) - ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error") - return ctx.RedirectTo("/login") } - - if ok, err := passwordpkg.VerifyPassword(password, user.Password); !ok { - if err != nil { - return ctx.ErrorRes(500, "Cannot check for password", err) + if user == nil { + if user, err = tryDbLogin(ctx, dto.Username, dto.Password); user == nil { + return err } - log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) - ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error") - return ctx.RedirectTo("/login") } // handle MFA @@ -168,3 +160,59 @@ func Logout(ctx *context.Context) error { ctx.DeleteCsrfCookie() return ctx.RedirectTo("/all") } + +func tryDbLogin(ctx *context.Context, username, password string) (user *db.User, err error) { + if user, err = db.GetUserByUsername(username); err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ctx.ErrorRes(500, "Cannot get user", err) + } + + log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) + ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error") + return nil, ctx.RedirectTo("/login") + } + + if ok, err := passwordpkg.VerifyPassword(password, user.Password); !ok { + if err != nil { + return nil, ctx.ErrorRes(500, "Cannot check for password", err) + } + log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) + ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error") + return nil, ctx.RedirectTo("/login") + } + + return user, nil +} + +func tryLdapLogin(ctx *context.Context, username, password string) (user *db.User, err error) { + ok, err := ldap.Authenticate(username, password) + if err != nil { + log.Info().Err(err).Msgf("LDAP authentication error") + return nil, ctx.ErrorRes(500, "Cannot get user", err) + } + + if !ok { + log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP()) + return nil, nil + } + + if user, err = db.GetUserByUsername(username); err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ctx.ErrorRes(500, "Cannot get user", err) + } + } + + if errors.Is(err, gorm.ErrRecordNotFound) { + user = &db.User{ + Username: username, + } + if err = user.Create(); err != nil { + log.Warn().Err(err).Msg("Cannot create user after LDAP authentication") + return nil, ctx.ErrorRes(500, "Cannot create user", err) + } + + return user, nil + } + + return user, nil +} diff --git a/internal/web/handlers/git/http.go b/internal/web/handlers/git/http.go index 6f7aac8..0158ad7 100644 --- a/internal/web/handlers/git/http.go +++ b/internal/web/handlers/git/http.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "errors" "fmt" + "github.com/thomiceli/opengist/internal/auth/ldap" "github.com/thomiceli/opengist/internal/auth/password" "github.com/thomiceli/opengist/internal/web/context" "github.com/thomiceli/opengist/internal/web/handlers" @@ -112,12 +113,28 @@ func GitHttp(ctx *context.Context) error { userToCheckPermissions = &gist.User } - if ok, err := password.VerifyPassword(authPassword, userToCheckPermissions.Password); !ok { - if err != nil { - return ctx.ErrorRes(500, "Cannot verify password", err) + // ldap + ldapSuccess := false + if ldap.Enabled() { + if ok, err := ldap.Authenticate(userToCheckPermissions.Username, authPassword); !ok { + if err != nil { + log.Warn().Err(err).Msg("LDAP authentication error") + } + log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP()) + } else { + ldapSuccess = true + } + } + + // password + if !ldapSuccess { + if ok, err := password.VerifyPassword(authPassword, userToCheckPermissions.Password); !ok { + if err != nil { + return ctx.ErrorRes(500, "Cannot verify password", err) + } + log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) + return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist") } - log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) - return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist") } } else { var user *db.User @@ -128,13 +145,25 @@ func GitHttp(ctx *context.Context) error { log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) return ctx.ErrorRes(401, "Invalid credentials", nil) } - - if ok, err := password.VerifyPassword(authPassword, user.Password); !ok { - if err != nil { - return ctx.ErrorRes(500, "Cannot check for password", err) + ldapSuccess := false + if ldap.Enabled() { + if ok, err := ldap.Authenticate(user.Username, authPassword); !ok { + if err != nil { + log.Warn().Err(err).Msg("LDAP authentication error") + } + log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP()) + } else { + ldapSuccess = true + } + } + if !ldapSuccess { + if ok, err := password.VerifyPassword(authPassword, user.Password); !ok { + if err != nil { + return ctx.ErrorRes(500, "Cannot check for password", err) + } + log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) + return ctx.ErrorRes(401, "Invalid credentials", nil) } - log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) - return ctx.ErrorRes(401, "Invalid credentials", nil) } if isInit { diff --git a/templates/pages/admin_config.html b/templates/pages/admin_config.html index 5cfdc7d..209cb1d 100644 --- a/templates/pages/admin_config.html +++ b/templates/pages/admin_config.html @@ -71,6 +71,19 @@
OIDC Discovery URL
{{ if .c.OIDCDiscoveryUrl }}<defined>{{ end }}
OIDC Group Claim Name
{{ .c.OIDCGroupClaimName }}
OIDC Admin Group
{{ .c.OIDCAdminGroup }}
+
+ +
+ LDAP +
+
+
LDAP URL
{{ .c.LDAPUrl }}
+
LDAP Bind DN
{{ .c.LDAPBindDn }}
+
LDAP Bind Credentials
{{ if .c.LDAPBindCredentials }}<defined>{{ end }}
+
LDAP Search Base
{{ .c.LDAPSearchBase }}
+
LDAP Search Filter
{{ .c.LDAPSearchFilter }}