From ad4356b9fa8b6f5e1008bff5f26dec15cded672b Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 11:42:29 -0600 Subject: [PATCH 01/57] feat: ldap sync begin work --- backend/internal/ldap/ldap.go | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/internal/ldap/ldap.go diff --git a/backend/internal/ldap/ldap.go b/backend/internal/ldap/ldap.go new file mode 100644 index 0000000..c9405cc --- /dev/null +++ b/backend/internal/ldap/ldap.go @@ -0,0 +1 @@ +package ldap From 965191c53736d53b0800480c54d6aedab230d0b6 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 11:45:29 -0600 Subject: [PATCH 02/57] feat(ldap-sync): add go-ldap --- backend/go.mod | 13 +++++--- backend/go.sum | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index 3b61bfe..b342a61 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -15,7 +15,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/mileusna/useragent v1.3.5 github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.31.0 golang.org/x/time v0.6.0 gorm.io/driver/postgres v1.5.11 gorm.io/driver/sqlite v1.5.6 @@ -23,12 +23,15 @@ require ( ) require ( + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/bytedance/sonic v1.12.3 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.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-ldap/ldap/v3 v3.4.10 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-webauthn/x v0.1.14 // indirect @@ -61,10 +64,10 @@ require ( go.uber.org/atomic v1.11.0 // indirect golang.org/x/arch v0.10.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.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 ) diff --git a/backend/go.sum b/backend/go.sum index e975d96..fc345ad 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,7 +1,10 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -37,8 +40,12 @@ 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-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw= github.com/go-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w= +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-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -70,11 +77,15 @@ github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1/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/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -83,6 +94,12 @@ github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +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= @@ -146,6 +163,7 @@ 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= @@ -158,6 +176,7 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= @@ -172,27 +191,98 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +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-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.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +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-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.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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.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.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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +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/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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.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.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= From b6c4c0d5611d776462004b41bfdc28b150a394af Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 11:54:12 -0600 Subject: [PATCH 03/57] feat(ldap-sync): add env varibles for ldap --- backend/internal/common/env_config.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index b992c98..70efd07 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -1,9 +1,10 @@ package common import ( + "log" + "github.com/caarlos0/env/v11" _ "github.com/joho/godotenv/autoload" - "log" ) type DbProvider string @@ -24,6 +25,12 @@ type EnvConfigSchema struct { Host string `env:"HOST"` MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"` GeoLiteDBPath string `env:"GEOLITE_DB_PATH"` + LDAPServer string `env:"LDAP_SERVER"` + LDAPPort string `env:"LDAP_PORT"` + LDAPBindUser string `env:"LDAP_BIND_USER"` + LDAPBindPassword string `env:"LDAP_BIND_PASSWORD"` + LDAPSearchBase string `env:"LDAP_SEARCH_BASE"` + LDAPTLSVerify bool `env:"LDAP_TLS_VERIFY"` } var EnvConfig = &EnvConfigSchema{ From 9eccfa3473f256764c76613bad3762daa2346712 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 11:56:27 -0600 Subject: [PATCH 04/57] feat(ldap-sync): add env varibles for ldap --- backend/internal/common/env_config.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 70efd07..52d70c3 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -44,6 +44,12 @@ var EnvConfig = &EnvConfigSchema{ Host: "localhost", MaxMindLicenseKey: "", GeoLiteDBPath: "data/GeoLite2-City.mmdb", + LDAPServer: "", + LDAPPort: "", + LDAPBindUser: "", + LDAPBindPassword: "", + LDAPSearchBase: "", + LDAPTLSVerify: false, } func init() { From 324d83b1c6dc5c83e6f6b78270810323d3141d18 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 11:58:35 -0600 Subject: [PATCH 05/57] feat(ldap-sync): updated .env.example with ldap config --- backend/.env.example | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/.env.example b/backend/.env.example index c86f02f..f664185 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -5,4 +5,10 @@ SQLITE_DB_PATH=data/pocket-id.db POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id UPLOAD_PATH=data/uploads PORT=8080 -HOST=localhost \ No newline at end of file +HOST=localhost +LDAP_SERVER=ldaps://ldap.example.com +LDAP_PORT=636 +LDAP_BIND_USER=CN=user,DC=example,DC=com +LDAP_BIND_PASSWORD=securepasswordhere +LDAP_SEARCH_BASE=OU=Stuff,DC=example,DC=com +LDAP_TLS_VERIFY=false From a4f281e8792e5119293d70e1bacf82b4f313fde7 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 12:03:33 -0600 Subject: [PATCH 06/57] feat(ldap-sync): added basic ldapInit() function --- backend/internal/ldap/ldap.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/backend/internal/ldap/ldap.go b/backend/internal/ldap/ldap.go index c9405cc..75d57c4 100644 --- a/backend/internal/ldap/ldap.go +++ b/backend/internal/ldap/ldap.go @@ -1 +1,27 @@ package ldap + +import ( + "crypto/tls" + + "github.com/go-ldap/ldap/v3" + "github.com/stonith404/pocket-id/backend/internal/common" +) + + func ldapInit() *ldap.Conn { + // Setup AD Connection + ldapURL := common.EnvConfig.LDAPServer + common.EnvConfig.LDAPPort + client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: common.EnvConfig.LDAPTLSVerify})) + if err != nil { + //TODO Handle Errors Better + panic(err) + } + // defer client.Close() + + // Bind as Service Account + err = client.Bind(common.EnvConfig.LDAPBindUser, common.EnvConfig.LDAPBindPassword) + if err != nil { + //TODO Handle Errors Better + panic(err) + } + return client + } From 577de76bd7b47c0d17a4b7d7e2ec942cca37810a Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 17:24:10 -0600 Subject: [PATCH 07/57] feat(ldap-sync): added query function --- backend/.gitignore | 3 +- backend/cmd/main.go | 5 ++- backend/internal/ldap/ldap.go | 61 +++++++++++++++++++++++++++-- backend/internal/ldap/ldap_types.go | 11 ++++++ 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 backend/internal/ldap/ldap_types.go diff --git a/backend/.gitignore b/backend/.gitignore index 71f540d..d32f3a0 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -13,4 +13,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ -./data \ No newline at end of file +./data +.env diff --git a/backend/cmd/main.go b/backend/cmd/main.go index b2ebc11..200081f 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -1,9 +1,10 @@ package main import ( - "github.com/stonith404/pocket-id/backend/internal/bootstrap" + "github.com/stonith404/pocket-id/backend/internal/ldap" ) func main() { - bootstrap.Bootstrap() + // bootstrap.Bootstrap() + ldap.GetLdapUser() } diff --git a/backend/internal/ldap/ldap.go b/backend/internal/ldap/ldap.go index 75d57c4..5d95672 100644 --- a/backend/internal/ldap/ldap.go +++ b/backend/internal/ldap/ldap.go @@ -2,14 +2,15 @@ package ldap import ( "crypto/tls" + "fmt" "github.com/go-ldap/ldap/v3" "github.com/stonith404/pocket-id/backend/internal/common" ) - func ldapInit() *ldap.Conn { +func ldapInit() *ldap.Conn { // Setup AD Connection - ldapURL := common.EnvConfig.LDAPServer + common.EnvConfig.LDAPPort + ldapURL := common.EnvConfig.LDAPServer + ":" + common.EnvConfig.LDAPPort client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: common.EnvConfig.LDAPTLSVerify})) if err != nil { //TODO Handle Errors Better @@ -24,4 +25,58 @@ import ( panic(err) } return client - } +} + +func printGreen(text string) string { + return fmt.Sprintf("\033[92m%s\033[0m", text) +} + +func GetLdapUser() LDAPUserSeachResult { + client := ldapInit() + // user := username + baseDN := common.EnvConfig.LDAPSearchBase + filter := "(objectClass=person)" + + searchAttrs := []string{ + "sAMAccountName", + "mail", + "memberOf", + "userPrincipalName", + "givenName", + "sn", + } + + // Filters must start and finish with ()! + searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) + + result, err := client.Search(searchReq) + if err != nil { + fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) + } + + userResult := LDAPUserSeachResult{} + + if len(result.Entries) >= 1 { + + if err := result.Entries[0].Unmarshal(&userResult); err != nil { + panic(err) + } + + for _, value := range result.Entries { + if err := value.Unmarshal(&userResult); err != nil { + panic(err) + } + fmt.Println("\nUser Attributes:") + fmt.Printf("Full Name: %s\n", printGreen(userResult.GivenName+" "+userResult.LastName)) + fmt.Printf("Email: %s\n", printGreen(userResult.Mail)) + fmt.Printf("Username: %s\n", printGreen(userResult.Username)) + fmt.Printf("DN: %s\n", printGreen(userResult.DN)) + } + + return userResult + + } else { + fmt.Println("No Users Found") + panic(1) + } +} diff --git a/backend/internal/ldap/ldap_types.go b/backend/internal/ldap/ldap_types.go new file mode 100644 index 0000000..1c6a414 --- /dev/null +++ b/backend/internal/ldap/ldap_types.go @@ -0,0 +1,11 @@ +package ldap + +type LDAPUserSeachResult struct { + DN string `ldap:"dn"` + UserPrincipalName string `ldap:"userPrincipalName"` + Username string `ldap:"sAMAccountName"` + Mail string `ldap:"mail"` + MemberOf []string `ldap:"memberOf"` + GivenName string `ldap:"givenName"` + LastName string `ldap:"sn"` +} From 6fda6f4a1ad9702be75d84feecf02fb5fa1f437c Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 17:30:45 -0600 Subject: [PATCH 08/57] feat(ldap-sync): added a few error checks for my testing --- backend/internal/ldap/ldap.go | 11 ++++++++++- backend/internal/ldap/ldap_types.go | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/internal/ldap/ldap.go b/backend/internal/ldap/ldap.go index 5d95672..6109729 100644 --- a/backend/internal/ldap/ldap.go +++ b/backend/internal/ldap/ldap.go @@ -37,6 +37,7 @@ func GetLdapUser() LDAPUserSeachResult { baseDN := common.EnvConfig.LDAPSearchBase filter := "(objectClass=person)" + //TODO Make options in UI to configure what options should be synced etc etc, as this depends on what ldap backend is being used. searchAttrs := []string{ "sAMAccountName", "mail", @@ -44,6 +45,8 @@ func GetLdapUser() LDAPUserSeachResult { "userPrincipalName", "givenName", "sn", + "cn", + "uid", } // Filters must start and finish with ()! @@ -66,10 +69,16 @@ func GetLdapUser() LDAPUserSeachResult { if err := value.Unmarshal(&userResult); err != nil { panic(err) } + + // This temp username is just for my testing, until we can build out a full Web Config UI for this. + tempUsername := "" + if userResult.Username == "" { + tempUsername = userResult.UID + } fmt.Println("\nUser Attributes:") fmt.Printf("Full Name: %s\n", printGreen(userResult.GivenName+" "+userResult.LastName)) fmt.Printf("Email: %s\n", printGreen(userResult.Mail)) - fmt.Printf("Username: %s\n", printGreen(userResult.Username)) + fmt.Printf("Username: %s\n", printGreen(tempUsername)) fmt.Printf("DN: %s\n", printGreen(userResult.DN)) } diff --git a/backend/internal/ldap/ldap_types.go b/backend/internal/ldap/ldap_types.go index 1c6a414..26cee27 100644 --- a/backend/internal/ldap/ldap_types.go +++ b/backend/internal/ldap/ldap_types.go @@ -8,4 +8,6 @@ type LDAPUserSeachResult struct { MemberOf []string `ldap:"memberOf"` GivenName string `ldap:"givenName"` LastName string `ldap:"sn"` + CN string `ldap:"cn"` + UID string `ldap:"uid"` } From c019676c02dadab61a2a5676c2c79c523b67d99b Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 17:41:03 -0600 Subject: [PATCH 09/57] feat(ldap-sync): updated .env.example --- .env.example | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.env.example b/.env.example index 06acff4..c130a1c 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,9 @@ TRUST_PROXY=false MAXMIND_LICENSE_KEY= PUID=1000 PGID=1000 +LDAP_SERVER=ldaps://ldap.example.com +LDAP_PORT=636 +LDAP_BIND_USER=CN=user,DC=example,DC=com +LDAP_BIND_PASSWORD=securepasswordhere +LDAP_SEARCH_BASE=OU=Stuff,DC=example,DC=com +LDAP_TLS_VERIFY=false From 0dda25498fefbbd5a21f99d65c2508ff40dc406c Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 17:47:23 -0600 Subject: [PATCH 10/57] feat(ldap-sync): updated readme with the ldap env vars so far --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fd4f7fe..d93f148 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Additionally, what makes Pocket ID special is that it only supports [passkey](ht ## Setup -> [!WARNING] +> [!WARNING] > Pocket ID is in its early stages and may contain bugs. There might be OIDC features that are not yet implemented. If you encounter any issues, please open an issue. ### Before you start @@ -154,8 +154,15 @@ docker compose up -d | `INTERNAL_BACKEND_URL` | `http://localhost:8080` | no | The URL where the backend is accessible. | | `GEOLITE_DB_PATH` | `data/GeoLite2-City.mmdb` | no | The path where the GeoLite2 database should be stored. | | `CADDY_PORT` | `80` | no | The port on which Caddy should listen. Caddy is only active inside the Docker container. If you want to change the exposed port of the container then you sould change this variable. | -| `PORT` | `3000` | no | The port on which the frontend should listen. | -| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. | +| `PORT` | `3000` | no | The port on which the frontend should listen. +| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen +| `LDAP_SERVER` | `` | yes | The Server of your ldap instance with the protocol. ie: ldaps://ldap.example.com +| `LDAP_PORT` | `` | yes | The port that your ldap server listens on. +| `LDAP_BIND_USER` | `` | yes | The bind user for your ldap instance. +| `LDAP_BIND_PASSWORD` | `` | yes | The bind user password for your ldap instance. +| `LDAP_SEARCH_BASE` | `` | yes | The OU to search for all LDAP Objects +| `LDAP_TLS_VERIFY` | `false` | yes | Choose to Verify LDAPS Certifcates or ignore them. + ## Contribute From 839ff71009420e0987091d69ef08ee52daf99903 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 17:50:05 -0600 Subject: [PATCH 11/57] feat(ldap-sync): added a comment in main.go --- backend/cmd/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 200081f..08f75ab 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -6,5 +6,6 @@ import ( func main() { // bootstrap.Bootstrap() + // this is for testing only so its easier to debug ldap.GetLdapUser() } From 09f4c17ad1d8c32e90899e09f72dec8f47bc6b8d Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 18:13:32 -0600 Subject: [PATCH 12/57] feat(ldap-sync): fixed main.go to restore normal functionality --- backend/cmd/main.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 08f75ab..898aa7f 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -1,11 +1,9 @@ package main -import ( - "github.com/stonith404/pocket-id/backend/internal/ldap" -) +import "github.com/stonith404/pocket-id/backend/internal/bootstrap" func main() { - // bootstrap.Bootstrap() - // this is for testing only so its easier to debug - ldap.GetLdapUser() + bootstrap.Bootstrap() + // Uncomment the line below to only test the ldap functionality + // ldap.GetLdapUser() } From 72b269ec478e8d3a919819861a49c65bc41a4091 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 18:46:57 -0600 Subject: [PATCH 13/57] feat(ldap-sync): Merge Result into UserObject model --- backend/cmd/main.go | 2 +- backend/internal/ldap/ldap.go | 38 +++++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 898aa7f..bda269d 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -5,5 +5,5 @@ import "github.com/stonith404/pocket-id/backend/internal/bootstrap" func main() { bootstrap.Bootstrap() // Uncomment the line below to only test the ldap functionality - // ldap.GetLdapUser() + // ldap.GetLdapUsers() } diff --git a/backend/internal/ldap/ldap.go b/backend/internal/ldap/ldap.go index 6109729..bdee8c0 100644 --- a/backend/internal/ldap/ldap.go +++ b/backend/internal/ldap/ldap.go @@ -6,6 +6,7 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/stonith404/pocket-id/backend/internal/common" + "github.com/stonith404/pocket-id/backend/internal/model" ) func ldapInit() *ldap.Conn { @@ -31,7 +32,22 @@ func printGreen(text string) string { return fmt.Sprintf("\033[92m%s\033[0m", text) } -func GetLdapUser() LDAPUserSeachResult { +func MergeLdapUsers(result LDAPUserSeachResult) { + userObject := model.User{ + Username: result.Username, + Email: result.Mail, + FirstName: result.GivenName, + LastName: result.LastName, + IsAdmin: false, + } + fmt.Printf("First Name: %s\n", printGreen(userObject.FirstName)) + fmt.Printf("Last Name: %s\n", printGreen(userObject.LastName)) + fmt.Printf("Email: %s\n", printGreen(userObject.Email)) + fmt.Printf("Username: %s\n", printGreen(userObject.Username)) + fmt.Println("Admin Status:", userObject.IsAdmin) +} + +func GetLdapUsers() LDAPUserSeachResult { client := ldapInit() // user := username baseDN := common.EnvConfig.LDAPSearchBase @@ -69,17 +85,17 @@ func GetLdapUser() LDAPUserSeachResult { if err := value.Unmarshal(&userResult); err != nil { panic(err) } - + MergeLdapUsers(userResult) // This temp username is just for my testing, until we can build out a full Web Config UI for this. - tempUsername := "" - if userResult.Username == "" { - tempUsername = userResult.UID - } - fmt.Println("\nUser Attributes:") - fmt.Printf("Full Name: %s\n", printGreen(userResult.GivenName+" "+userResult.LastName)) - fmt.Printf("Email: %s\n", printGreen(userResult.Mail)) - fmt.Printf("Username: %s\n", printGreen(tempUsername)) - fmt.Printf("DN: %s\n", printGreen(userResult.DN)) + // tempUsername := "" + // if userResult.Username == "" { + // tempUsername = userResult.UID + // } + // // fmt.Println("\nUser Attributes:") + // // fmt.Printf("Full Name: %s\n", printGreen(userResult.GivenName+" "+userResult.LastName)) + // // fmt.Printf("Email: %s\n", printGreen(userResult.Mail)) + // // fmt.Printf("Username: %s\n", printGreen(tempUsername)) + // // fmt.Printf("DN: %s\n", printGreen(userResult.DN)) } return userResult From 16dceefa726742dfa4cbd1886b3e14ca36622ba5 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sat, 11 Jan 2025 18:57:56 -0600 Subject: [PATCH 14/57] feat(ldap-sync): added new env var for user attribute, need better logic on how to use that though --- backend/internal/common/env_config.go | 2 ++ backend/internal/ldap/ldap.go | 15 +++++++++------ backend/internal/ldap/ldap_types.go | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 52d70c3..7604787 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -31,6 +31,7 @@ type EnvConfigSchema struct { LDAPBindPassword string `env:"LDAP_BIND_PASSWORD"` LDAPSearchBase string `env:"LDAP_SEARCH_BASE"` LDAPTLSVerify bool `env:"LDAP_TLS_VERIFY"` + LDAPUsernameAttribute string `env:"LDAP_USERNAME_ATTRIBUTE"` } var EnvConfig = &EnvConfigSchema{ @@ -50,6 +51,7 @@ var EnvConfig = &EnvConfigSchema{ LDAPBindPassword: "", LDAPSearchBase: "", LDAPTLSVerify: false, + LDAPUsernameAttribute: "", } func init() { diff --git a/backend/internal/ldap/ldap.go b/backend/internal/ldap/ldap.go index bdee8c0..3b138c3 100644 --- a/backend/internal/ldap/ldap.go +++ b/backend/internal/ldap/ldap.go @@ -33,12 +33,15 @@ func printGreen(text string) string { } func MergeLdapUsers(result LDAPUserSeachResult) { - userObject := model.User{ - Username: result.Username, - Email: result.Mail, - FirstName: result.GivenName, - LastName: result.LastName, - IsAdmin: false, + userObject := model.User{} + if common.EnvConfig.LDAPUsernameAttribute == "uid" { + userObject = model.User{ + Username: result.UID, + Email: result.Mail, + FirstName: result.GivenName, + LastName: result.LastName, + IsAdmin: false, + } } fmt.Printf("First Name: %s\n", printGreen(userObject.FirstName)) fmt.Printf("Last Name: %s\n", printGreen(userObject.LastName)) diff --git a/backend/internal/ldap/ldap_types.go b/backend/internal/ldap/ldap_types.go index 26cee27..3c06070 100644 --- a/backend/internal/ldap/ldap_types.go +++ b/backend/internal/ldap/ldap_types.go @@ -3,7 +3,7 @@ package ldap type LDAPUserSeachResult struct { DN string `ldap:"dn"` UserPrincipalName string `ldap:"userPrincipalName"` - Username string `ldap:"sAMAccountName"` + SamAccountName string `ldap:"sAMAccountName"` Mail string `ldap:"mail"` MemberOf []string `ldap:"memberOf"` GivenName string `ldap:"givenName"` From 8d480dc932d5fe2f8072ce3732013811b7336a61 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 12:04:47 -0600 Subject: [PATCH 15/57] feat(ldap-sync): fixed the issue with username attributes, will map directly to the model.User struct now --- backend/cmd/main.go | 6 ++-- backend/internal/ldap/ldap.go | 64 ++++++++++------------------------- 2 files changed, 21 insertions(+), 49 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index bda269d..3413114 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -1,9 +1,9 @@ package main -import "github.com/stonith404/pocket-id/backend/internal/bootstrap" +import "github.com/stonith404/pocket-id/backend/internal/ldap" func main() { - bootstrap.Bootstrap() + // bootstrap.Bootstrap() // Uncomment the line below to only test the ldap functionality - // ldap.GetLdapUsers() + ldap.GetLdapUsers() } diff --git a/backend/internal/ldap/ldap.go b/backend/internal/ldap/ldap.go index 3b138c3..9e0642c 100644 --- a/backend/internal/ldap/ldap.go +++ b/backend/internal/ldap/ldap.go @@ -28,29 +28,7 @@ func ldapInit() *ldap.Conn { return client } -func printGreen(text string) string { - return fmt.Sprintf("\033[92m%s\033[0m", text) -} - -func MergeLdapUsers(result LDAPUserSeachResult) { - userObject := model.User{} - if common.EnvConfig.LDAPUsernameAttribute == "uid" { - userObject = model.User{ - Username: result.UID, - Email: result.Mail, - FirstName: result.GivenName, - LastName: result.LastName, - IsAdmin: false, - } - } - fmt.Printf("First Name: %s\n", printGreen(userObject.FirstName)) - fmt.Printf("Last Name: %s\n", printGreen(userObject.LastName)) - fmt.Printf("Email: %s\n", printGreen(userObject.Email)) - fmt.Printf("Username: %s\n", printGreen(userObject.Username)) - fmt.Println("Admin Status:", userObject.IsAdmin) -} - -func GetLdapUsers() LDAPUserSeachResult { +func GetLdapUsers() []model.User { client := ldapInit() // user := username baseDN := common.EnvConfig.LDAPSearchBase @@ -58,14 +36,12 @@ func GetLdapUsers() LDAPUserSeachResult { //TODO Make options in UI to configure what options should be synced etc etc, as this depends on what ldap backend is being used. searchAttrs := []string{ - "sAMAccountName", "mail", "memberOf", - "userPrincipalName", "givenName", "sn", "cn", - "uid", + common.EnvConfig.LDAPUsernameAttribute, // Search for the Username Attribute supplied by the user. } // Filters must start and finish with ()! @@ -76,32 +52,28 @@ func GetLdapUsers() LDAPUserSeachResult { fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) } - userResult := LDAPUserSeachResult{} - if len(result.Entries) >= 1 { - if err := result.Entries[0].Unmarshal(&userResult); err != nil { - panic(err) - } - + var ldapUsers []model.User for _, value := range result.Entries { - if err := value.Unmarshal(&userResult); err != nil { - panic(err) + user := model.User{ + Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), + Email: value.GetAttributeValue("mail"), + FirstName: value.GetAttributeValue("givenName"), + LastName: value.GetAttributeValue("sn"), } - MergeLdapUsers(userResult) - // This temp username is just for my testing, until we can build out a full Web Config UI for this. - // tempUsername := "" - // if userResult.Username == "" { - // tempUsername = userResult.UID - // } - // // fmt.Println("\nUser Attributes:") - // // fmt.Printf("Full Name: %s\n", printGreen(userResult.GivenName+" "+userResult.LastName)) - // // fmt.Printf("Email: %s\n", printGreen(userResult.Mail)) - // // fmt.Printf("Username: %s\n", printGreen(tempUsername)) - // // fmt.Printf("DN: %s\n", printGreen(userResult.DN)) + ldapUsers = append(ldapUsers, user) + } + + for _, user := range ldapUsers { + fmt.Printf("Username: %s\n", user.Username) + fmt.Printf("First Name: %s\n", user.FirstName) + fmt.Printf("Last Name: %s\n", user.LastName) + fmt.Printf("Email: %s\n", user.Email) + fmt.Printf("Admin: %t\n", user.IsAdmin) } - return userResult + return ldapUsers } else { fmt.Println("No Users Found") From 710b793451c62fabd7eb17010c13638562782b7e Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 12:05:27 -0600 Subject: [PATCH 16/57] feat(ldap-sync): removed unneeded ldap_types.go --- backend/internal/ldap/ldap_types.go | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 backend/internal/ldap/ldap_types.go diff --git a/backend/internal/ldap/ldap_types.go b/backend/internal/ldap/ldap_types.go deleted file mode 100644 index 3c06070..0000000 --- a/backend/internal/ldap/ldap_types.go +++ /dev/null @@ -1,13 +0,0 @@ -package ldap - -type LDAPUserSeachResult struct { - DN string `ldap:"dn"` - UserPrincipalName string `ldap:"userPrincipalName"` - SamAccountName string `ldap:"sAMAccountName"` - Mail string `ldap:"mail"` - MemberOf []string `ldap:"memberOf"` - GivenName string `ldap:"givenName"` - LastName string `ldap:"sn"` - CN string `ldap:"cn"` - UID string `ldap:"uid"` -} From 1e3b050cf73c385aceae84f1b7ea5fbb8b7b2de5 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 12:09:43 -0600 Subject: [PATCH 17/57] feat(ldap-sync): restored main.go functionality --- backend/cmd/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 3413114..bda269d 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -1,9 +1,9 @@ package main -import "github.com/stonith404/pocket-id/backend/internal/ldap" +import "github.com/stonith404/pocket-id/backend/internal/bootstrap" func main() { - // bootstrap.Bootstrap() + bootstrap.Bootstrap() // Uncomment the line below to only test the ldap functionality - ldap.GetLdapUsers() + // ldap.GetLdapUsers() } From 0d552a1fa7773dd4160aaf9e5094b9ccb465a291 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 13:44:06 -0600 Subject: [PATCH 18/57] feat(ldap-sync): add initial group logic --- backend/internal/common/env_config.go | 2 ++ backend/internal/ldap/ldap.go | 38 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 7604787..9c42b17 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -32,6 +32,7 @@ type EnvConfigSchema struct { LDAPSearchBase string `env:"LDAP_SEARCH_BASE"` LDAPTLSVerify bool `env:"LDAP_TLS_VERIFY"` LDAPUsernameAttribute string `env:"LDAP_USERNAME_ATTRIBUTE"` + LDAPGroupAttribute string `env:"LDAP_GROUP_ATTRIBUTE"` } var EnvConfig = &EnvConfigSchema{ @@ -52,6 +53,7 @@ var EnvConfig = &EnvConfigSchema{ LDAPSearchBase: "", LDAPTLSVerify: false, LDAPUsernameAttribute: "", + LDAPGroupAttribute: "", } func init() { diff --git a/backend/internal/ldap/ldap.go b/backend/internal/ldap/ldap.go index 9e0642c..d71d8fe 100644 --- a/backend/internal/ldap/ldap.go +++ b/backend/internal/ldap/ldap.go @@ -28,6 +28,42 @@ func ldapInit() *ldap.Conn { return client } +func GetLdapGroups() []model.UserGroup { + client := ldapInit() + baseDN := common.EnvConfig.LDAPSearchBase + filter := "(objectClass=groupOfUniqueNames)" + + searchAttrs := []string{ + common.EnvConfig.LDAPGroupAttribute, + "member", + } + + searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) + result, err := client.Search(searchReq) + if err != nil { + fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) + } + + if len(result.Entries) >= 1 { + + var ldapGroups []model.UserGroup + for _, value := range result.Entries { + group := model.UserGroup{ + Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + } + ldapGroups = append(ldapGroups, group) + } + + client.Close() + + return ldapGroups + } else { + fmt.Println("No Groups Found") + panic(1) + } + +} + func GetLdapUsers() []model.User { client := ldapInit() // user := username @@ -73,10 +109,12 @@ func GetLdapUsers() []model.User { fmt.Printf("Admin: %t\n", user.IsAdmin) } + client.Close() return ldapUsers } else { fmt.Println("No Users Found") panic(1) } + } From 224c2ab0a790f3ec406d3cc160e680e2296c1d95 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 13:51:08 -0600 Subject: [PATCH 19/57] feat(ldap-sync): added friendly name for group model --- backend/cmd/main.go | 1 + backend/internal/ldap/ldap.go | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index bda269d..c2b6b4d 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -6,4 +6,5 @@ func main() { bootstrap.Bootstrap() // Uncomment the line below to only test the ldap functionality // ldap.GetLdapUsers() + // ldap.GetLdapGroups() } diff --git a/backend/internal/ldap/ldap.go b/backend/internal/ldap/ldap.go index d71d8fe..1d67346 100644 --- a/backend/internal/ldap/ldap.go +++ b/backend/internal/ldap/ldap.go @@ -49,11 +49,17 @@ func GetLdapGroups() []model.UserGroup { var ldapGroups []model.UserGroup for _, value := range result.Entries { group := model.UserGroup{ - Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), } ldapGroups = append(ldapGroups, group) } + //Below Loop only for debug testing + for _, group := range ldapGroups { + fmt.Printf("Group Name: %s\n", group.Name) + } + client.Close() return ldapGroups @@ -101,6 +107,7 @@ func GetLdapUsers() []model.User { ldapUsers = append(ldapUsers, user) } + //Below Loop only for debug testing for _, user := range ldapUsers { fmt.Printf("Username: %s\n", user.Username) fmt.Printf("First Name: %s\n", user.FirstName) From ba6355ab83ef0de4afcc221658c7ff8e1e74dcef Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 13:52:58 -0600 Subject: [PATCH 20/57] feat(ldap-sync): updated readme with env vars --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d93f148..986a523 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,8 @@ docker compose up -d | `LDAP_BIND_PASSWORD` | `` | yes | The bind user password for your ldap instance. | `LDAP_SEARCH_BASE` | `` | yes | The OU to search for all LDAP Objects | `LDAP_TLS_VERIFY` | `false` | yes | Choose to Verify LDAPS Certifcates or ignore them. +| `LDAP_USERNAME_ATTRIBUTE` | `` | yes | The LDAP Attribute to use for the username of a user. +| `LDAP_GROUP_ATTRIBUTE` | `` | yes | The LDAP Attribute to use for the Name and Claim of a Group. ## Contribute From a7d1e7991d78935e9f3d4f61bcc31d96ba827c22 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 13:53:55 -0600 Subject: [PATCH 21/57] feat(ldap-sync): updated .env.examples --- .env.example | 2 ++ backend/.env.example | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.env.example b/.env.example index c130a1c..4a0bd8c 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,5 @@ LDAP_BIND_USER=CN=user,DC=example,DC=com LDAP_BIND_PASSWORD=securepasswordhere LDAP_SEARCH_BASE=OU=Stuff,DC=example,DC=com LDAP_TLS_VERIFY=false +LDAP_USERNAME_ATTRIBUTE=uid +LDAP_GROUP_ATTRIBUTE=uid diff --git a/backend/.env.example b/backend/.env.example index f664185..c09bc47 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -12,3 +12,5 @@ LDAP_BIND_USER=CN=user,DC=example,DC=com LDAP_BIND_PASSWORD=securepasswordhere LDAP_SEARCH_BASE=OU=Stuff,DC=example,DC=com LDAP_TLS_VERIFY=false +LDAP_USERNAME_ATTRIBUTE=uid +LDAP_GROUP_ATTRIBUTE=uid From 085a6f59b978d9a6f011b6972021a71522c1b642 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 16:07:07 -0600 Subject: [PATCH 22/57] feat(ldap-sync): added skel for ldap_service and ldap_job --- backend/internal/job/ldap_job.go | 1 + backend/internal/service/ldap_service.go | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 backend/internal/job/ldap_job.go create mode 100644 backend/internal/service/ldap_service.go diff --git a/backend/internal/job/ldap_job.go b/backend/internal/job/ldap_job.go new file mode 100644 index 0000000..8d7765a --- /dev/null +++ b/backend/internal/job/ldap_job.go @@ -0,0 +1 @@ +package job diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go new file mode 100644 index 0000000..ef65b73 --- /dev/null +++ b/backend/internal/service/ldap_service.go @@ -0,0 +1,11 @@ +package service + +import "gorm.io/gorm" + +type LdapService struct { + db *gorm.DB +} + +func NewLdapService(db *gorm.DB) *LdapService { + return &LdapService{db: db} +} From 7daf16da69e2fe5167b6f5a36e6e91967fcb1c77 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 16:28:31 -0600 Subject: [PATCH 23/57] feat(ldap-sync): first draft of the ldap_service.go --- backend/internal/service/ldap_service.go | 92 ++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index ef65b73..4b843f3 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -1,11 +1,95 @@ package service -import "gorm.io/gorm" +import ( + "crypto/tls" + "fmt" + + "github.com/go-ldap/ldap/v3" + "github.com/stonith404/pocket-id/backend/internal/common" + "github.com/stonith404/pocket-id/backend/internal/dto" + "github.com/stonith404/pocket-id/backend/internal/model" + "github.com/stonith404/pocket-id/backend/internal/service" + "gorm.io/gorm" +) type LdapService struct { - db *gorm.DB + db *gorm.DB + jwtService *JwtService + auditLogService *AuditLogService +} + +func NewLdapService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService) *LdapService { + return &LdapService{db: db, jwtService: jwtService, auditLogService: auditLogService} } -func NewLdapService(db *gorm.DB) *LdapService { - return &LdapService{db: db} +func ldapInit() *ldap.Conn { + // Setup AD Connection + ldapURL := common.EnvConfig.LDAPServer + ":" + common.EnvConfig.LDAPPort + client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: common.EnvConfig.LDAPTLSVerify})) + if err != nil { + //TODO Handle Errors Better + panic(err) + } + + // Bind as Service Account + err = client.Bind(common.EnvConfig.LDAPBindUser, common.EnvConfig.LDAPBindPassword) + if err != nil { + //TODO Handle Errors Better + panic(err) + } + return client +} + +func (s *LdapService) GetLdapUsers() (model.User, error) { + client := ldapInit() + baseDN := common.EnvConfig.LDAPSearchBase + filter := "(objectClass=person)" + + searchAttrs := []string{ + "mail", + "memberOf", + "givenName", + "sn", + "cn", + common.EnvConfig.LDAPUsernameAttribute, // Search for the Username Attribute supplied by the user. + } + + // Filters must start and finish with ()! + searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) + + result, err := client.Search(searchReq) + if err != nil { + fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) + } + + if len(result.Entries) >= 1 { + + var userModel model.User + var userError error + + for _, value := range result.Entries { + + newUser := dto.UserCreateDto{ + Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), + Email: value.GetAttributeValue("mail"), + FirstName: value.GetAttributeValue("givenName"), + LastName: value.GetAttributeValue("sn"), + IsAdmin: false, + } + + var userService *UserService + userService = service.NewUserService(s.db, s.jwtService, s.auditLogService) + + userModel, userError = userService.CreateUser(newUser) + } + + client.Close() + return userModel, userError + + } else { + fmt.Println("No Users Found") + //TODO Handle Errors Better + panic(1) + } + } From 7ddcb62941ebbf6036e8763a62bc046b72372dff Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 16:30:50 -0600 Subject: [PATCH 24/57] feat(ldap-sync): fix import issue --- backend/internal/service/ldap_service.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 4b843f3..63a40b4 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -8,7 +8,6 @@ import ( "github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/model" - "github.com/stonith404/pocket-id/backend/internal/service" "gorm.io/gorm" ) @@ -78,7 +77,7 @@ func (s *LdapService) GetLdapUsers() (model.User, error) { } var userService *UserService - userService = service.NewUserService(s.db, s.jwtService, s.auditLogService) + userService = NewUserService(s.db, s.jwtService, s.auditLogService) userModel, userError = userService.CreateUser(newUser) } From b4818d2a4d58758cb5374590940c2fe01f897017 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jan 2025 16:33:01 -0600 Subject: [PATCH 25/57] feat(ldap-sync): fixed return statment for else clause --- backend/internal/service/ldap_service.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 63a40b4..89dcc00 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -61,10 +61,11 @@ func (s *LdapService) GetLdapUsers() (model.User, error) { fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) } + var userError error + if len(result.Entries) >= 1 { var userModel model.User - var userError error for _, value := range result.Entries { @@ -87,8 +88,7 @@ func (s *LdapService) GetLdapUsers() (model.User, error) { } else { fmt.Println("No Users Found") - //TODO Handle Errors Better - panic(1) + return model.User{}, userError } } From 27d441ce645fce1e636ac371099d38c36ab8a8ac Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Mon, 13 Jan 2025 13:46:01 -0600 Subject: [PATCH 26/57] feat(ldap-sync): fixed userService to use depend injection --- backend/internal/service/ldap_service.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 89dcc00..e5532d5 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -12,13 +12,12 @@ import ( ) type LdapService struct { - db *gorm.DB - jwtService *JwtService - auditLogService *AuditLogService + db *gorm.DB + userService *UserService } -func NewLdapService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService) *LdapService { - return &LdapService{db: db, jwtService: jwtService, auditLogService: auditLogService} +func NewLdapService(db *gorm.DB, userService *UserService) *LdapService { + return &LdapService{db: db, userService: userService} } func ldapInit() *ldap.Conn { @@ -77,10 +76,7 @@ func (s *LdapService) GetLdapUsers() (model.User, error) { IsAdmin: false, } - var userService *UserService - userService = NewUserService(s.db, s.jwtService, s.auditLogService) - - userModel, userError = userService.CreateUser(newUser) + userModel, userError = s.userService.CreateUser(newUser) } client.Close() From 7e546c873662818823c37fa25a2d06b3ad563f01 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Mon, 13 Jan 2025 13:48:35 -0600 Subject: [PATCH 27/57] feat(ldap-sync): added ldapService to router bootstrap --- backend/internal/bootstrap/router_bootstrap.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index ba47459..cd163e7 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -38,6 +38,7 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) { jwtService := service.NewJwtService(appConfigService) webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService) userService := service.NewUserService(db, jwtService, auditLogService) + ldapService := service.NewLdapService(db, userService) customClaimService := service.NewCustomClaimService(db) oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService) testService := service.NewTestService(db, appConfigService) From 28520da1f3eb16147c87342e8d6bafbd4cbab261 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Tue, 14 Jan 2025 18:57:51 -0600 Subject: [PATCH 28/57] feat(ldap-sync): ldap service sycnign users succesfully --- .../internal/bootstrap/router_bootstrap.go | 3 +++ .../internal/controller/user_controller.go | 5 ++-- backend/internal/job/ldap_job.go | 27 +++++++++++++++++++ backend/internal/service/ldap_service.go | 11 +++----- frontend/package-lock.json | 4 +-- frontend/svelte.config.js | 4 +-- 6 files changed, 41 insertions(+), 13 deletions(-) diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index cd163e7..0058ef4 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/controller" + "github.com/stonith404/pocket-id/backend/internal/job" "github.com/stonith404/pocket-id/backend/internal/middleware" "github.com/stonith404/pocket-id/backend/internal/service" "golang.org/x/time/rate" @@ -49,6 +50,8 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) { r.Use(middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 60)) r.Use(middleware.NewJwtAuthMiddleware(jwtService, true).Add(false)) + job.RegisterLdapJobs(ldapService) + // Initialize middleware jwtAuthMiddleware := middleware.NewJwtAuthMiddleware(jwtService, false) fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware() diff --git a/backend/internal/controller/user_controller.go b/backend/internal/controller/user_controller.go index 8854cd9..ccfcef7 100644 --- a/backend/internal/controller/user_controller.go +++ b/backend/internal/controller/user_controller.go @@ -1,6 +1,9 @@ package controller import ( + "net/http" + "time" + "github.com/gin-gonic/gin" "github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/dto" @@ -8,8 +11,6 @@ import ( "github.com/stonith404/pocket-id/backend/internal/service" "github.com/stonith404/pocket-id/backend/internal/utils" "golang.org/x/time/rate" - "net/http" - "time" ) func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, appConfigService *service.AppConfigService) { diff --git a/backend/internal/job/ldap_job.go b/backend/internal/job/ldap_job.go index 8d7765a..359324a 100644 --- a/backend/internal/job/ldap_job.go +++ b/backend/internal/job/ldap_job.go @@ -1 +1,28 @@ package job + +import ( + "log" + + "github.com/go-co-op/gocron/v2" + "github.com/stonith404/pocket-id/backend/internal/service" +) + +type LdapJobs struct { + ldapService *service.LdapService +} + +func RegisterLdapJobs(ls *service.LdapService) { + scheduler, err := gocron.NewScheduler() + if err != nil { + log.Fatalf("Failed to create a new scheduler: %s", err) + } + + jobs := &LdapJobs{ldapService: ls} + + registerJob(scheduler, "ClearWebauthnSessions", "*/5 * * * *", jobs.ldapSyncJob) + scheduler.Start() +} + +func (j *LdapJobs) ldapSyncJob() error { + return j.ldapService.GetLdapUsers() +} diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index e5532d5..501d931 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -7,7 +7,6 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/dto" - "github.com/stonith404/pocket-id/backend/internal/model" "gorm.io/gorm" ) @@ -38,7 +37,7 @@ func ldapInit() *ldap.Conn { return client } -func (s *LdapService) GetLdapUsers() (model.User, error) { +func (s *LdapService) GetLdapUsers() error { client := ldapInit() baseDN := common.EnvConfig.LDAPSearchBase filter := "(objectClass=person)" @@ -64,8 +63,6 @@ func (s *LdapService) GetLdapUsers() (model.User, error) { if len(result.Entries) >= 1 { - var userModel model.User - for _, value := range result.Entries { newUser := dto.UserCreateDto{ @@ -76,15 +73,15 @@ func (s *LdapService) GetLdapUsers() (model.User, error) { IsAdmin: false, } - userModel, userError = s.userService.CreateUser(newUser) + _, userError = s.userService.CreateUser(newUser) } client.Close() - return userModel, userError + return userError } else { fmt.Println("No Users Found") - return model.User{}, userError + return userError } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index df274b5..8010a9d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "pocket-id-frontend", - "version": "0.10.0", + "version": "0.24.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pocket-id-frontend", - "version": "0.10.0", + "version": "0.24.1", "dependencies": { "@simplewebauthn/browser": "^10.0.0", "axios": "^1.7.7", diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js index 42d1a6c..eb463e8 100644 --- a/frontend/svelte.config.js +++ b/frontend/svelte.config.js @@ -1,6 +1,6 @@ import adapter from '@sveltejs/adapter-node'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; -import packageJson from "./package.json" assert { type: "json" }; +import packageJson from './package.json' with { type: 'json' }; /** @type {import('@sveltejs/kit').Config} */ const config = { @@ -14,7 +14,7 @@ const config = { // See https://kit.svelte.dev/docs/adapters for more information about adapters. adapter: adapter(), version: { - name: packageJson.version, + name: packageJson.version } } }; From 4a8adf7bb06f464cb00ca69bae0ee289613cfbe5 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Tue, 14 Jan 2025 19:01:08 -0600 Subject: [PATCH 29/57] feat(ldap-sync): changed name of the cron job for ldap user sync --- backend/internal/job/ldap_job.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/internal/job/ldap_job.go b/backend/internal/job/ldap_job.go index 359324a..add4587 100644 --- a/backend/internal/job/ldap_job.go +++ b/backend/internal/job/ldap_job.go @@ -19,7 +19,7 @@ func RegisterLdapJobs(ls *service.LdapService) { jobs := &LdapJobs{ldapService: ls} - registerJob(scheduler, "ClearWebauthnSessions", "*/5 * * * *", jobs.ldapSyncJob) + registerJob(scheduler, "SyncLdapUsers", "*/5 * * * *", jobs.ldapSyncJob) scheduler.Start() } From 44007db1ab9403d5f5fff2de169b2dbbb947cf10 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Tue, 14 Jan 2025 20:37:44 -0600 Subject: [PATCH 30/57] feat(ldap-sync): added group sync to ldap_service --- .../internal/bootstrap/router_bootstrap.go | 2 +- backend/internal/job/ldap_job.go | 9 +++- backend/internal/service/ldap_service.go | 49 +++++++++++++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index 0058ef4..bfe6fa1 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -39,11 +39,11 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) { jwtService := service.NewJwtService(appConfigService) webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService) userService := service.NewUserService(db, jwtService, auditLogService) - ldapService := service.NewLdapService(db, userService) customClaimService := service.NewCustomClaimService(db) oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService) testService := service.NewTestService(db, appConfigService) userGroupService := service.NewUserGroupService(db) + ldapService := service.NewLdapService(db, userService, userGroupService) r.Use(middleware.NewCorsMiddleware().Add()) r.Use(middleware.NewErrorHandlerMiddleware().Add()) diff --git a/backend/internal/job/ldap_job.go b/backend/internal/job/ldap_job.go index add4587..21b625d 100644 --- a/backend/internal/job/ldap_job.go +++ b/backend/internal/job/ldap_job.go @@ -19,10 +19,15 @@ func RegisterLdapJobs(ls *service.LdapService) { jobs := &LdapJobs{ldapService: ls} - registerJob(scheduler, "SyncLdapUsers", "*/5 * * * *", jobs.ldapSyncJob) + registerJob(scheduler, "SyncLdapUsers", "*/5 * * * *", jobs.ldapUserSyncJob) + registerJob(scheduler, "SyncLdapGroups", "*/5 * * * *", jobs.ldapGroupSyncJob) scheduler.Start() } -func (j *LdapJobs) ldapSyncJob() error { +func (j *LdapJobs) ldapUserSyncJob() error { return j.ldapService.GetLdapUsers() } + +func (j *LdapJobs) ldapGroupSyncJob() error { + return j.ldapService.GetLdapGroups() +} diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 501d931..0f73f7e 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -11,12 +11,13 @@ import ( ) type LdapService struct { - db *gorm.DB - userService *UserService + db *gorm.DB + userService *UserService + groupService *UserGroupService } -func NewLdapService(db *gorm.DB, userService *UserService) *LdapService { - return &LdapService{db: db, userService: userService} +func NewLdapService(db *gorm.DB, userService *UserService, groupService *UserGroupService) *LdapService { + return &LdapService{db: db, userService: userService, groupService: groupService} } func ldapInit() *ldap.Conn { @@ -37,6 +38,46 @@ func ldapInit() *ldap.Conn { return client } +func (s *LdapService) GetLdapGroups() error { + client := ldapInit() + baseDN := common.EnvConfig.LDAPSearchBase + filter := "(objectClass=groupOfUniqueNames)" + + searchAttrs := []string{ + common.EnvConfig.LDAPGroupAttribute, + "member", + } + + searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) + result, err := client.Search(searchReq) + if err != nil { + fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) + } + + var groupError error + + if len(result.Entries) >= 1 { + + for _, value := range result.Entries { + + syncGroup := dto.UserGroupCreateDto{ + Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + } + + _, groupError = s.groupService.Create(syncGroup) + + } + + client.Close() + return groupError + } else { + fmt.Println("No Groups Found") + return groupError + } + +} + func (s *LdapService) GetLdapUsers() error { client := ldapInit() baseDN := common.EnvConfig.LDAPSearchBase From e433a606cb1ad36b54889f35d18c3b88c85d9cbd Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Tue, 14 Jan 2025 21:27:27 -0600 Subject: [PATCH 31/57] feat(ldap-sync): user sync error logic --- backend/internal/service/ldap_service.go | 269 ++++++++++++----------- 1 file changed, 141 insertions(+), 128 deletions(-) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 0f73f7e..5ca70b0 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -1,128 +1,141 @@ -package service - -import ( - "crypto/tls" - "fmt" - - "github.com/go-ldap/ldap/v3" - "github.com/stonith404/pocket-id/backend/internal/common" - "github.com/stonith404/pocket-id/backend/internal/dto" - "gorm.io/gorm" -) - -type LdapService struct { - db *gorm.DB - userService *UserService - groupService *UserGroupService -} - -func NewLdapService(db *gorm.DB, userService *UserService, groupService *UserGroupService) *LdapService { - return &LdapService{db: db, userService: userService, groupService: groupService} -} - -func ldapInit() *ldap.Conn { - // Setup AD Connection - ldapURL := common.EnvConfig.LDAPServer + ":" + common.EnvConfig.LDAPPort - client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: common.EnvConfig.LDAPTLSVerify})) - if err != nil { - //TODO Handle Errors Better - panic(err) - } - - // Bind as Service Account - err = client.Bind(common.EnvConfig.LDAPBindUser, common.EnvConfig.LDAPBindPassword) - if err != nil { - //TODO Handle Errors Better - panic(err) - } - return client -} - -func (s *LdapService) GetLdapGroups() error { - client := ldapInit() - baseDN := common.EnvConfig.LDAPSearchBase - filter := "(objectClass=groupOfUniqueNames)" - - searchAttrs := []string{ - common.EnvConfig.LDAPGroupAttribute, - "member", - } - - searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) - result, err := client.Search(searchReq) - if err != nil { - fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) - } - - var groupError error - - if len(result.Entries) >= 1 { - - for _, value := range result.Entries { - - syncGroup := dto.UserGroupCreateDto{ - Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), - FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), - } - - _, groupError = s.groupService.Create(syncGroup) - - } - - client.Close() - return groupError - } else { - fmt.Println("No Groups Found") - return groupError - } - -} - -func (s *LdapService) GetLdapUsers() error { - client := ldapInit() - baseDN := common.EnvConfig.LDAPSearchBase - filter := "(objectClass=person)" - - searchAttrs := []string{ - "mail", - "memberOf", - "givenName", - "sn", - "cn", - common.EnvConfig.LDAPUsernameAttribute, // Search for the Username Attribute supplied by the user. - } - - // Filters must start and finish with ()! - searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) - - result, err := client.Search(searchReq) - if err != nil { - fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) - } - - var userError error - - if len(result.Entries) >= 1 { - - for _, value := range result.Entries { - - newUser := dto.UserCreateDto{ - Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), - Email: value.GetAttributeValue("mail"), - FirstName: value.GetAttributeValue("givenName"), - LastName: value.GetAttributeValue("sn"), - IsAdmin: false, - } - - _, userError = s.userService.CreateUser(newUser) - } - - client.Close() - return userError - - } else { - fmt.Println("No Users Found") - return userError - } - -} +package service + +import ( + "crypto/tls" + "fmt" + + "github.com/go-ldap/ldap/v3" + "github.com/stonith404/pocket-id/backend/internal/common" + "github.com/stonith404/pocket-id/backend/internal/dto" + "github.com/stonith404/pocket-id/backend/internal/model" + "gorm.io/gorm" +) + +type LdapService struct { + db *gorm.DB + userService *UserService + groupService *UserGroupService +} + +func NewLdapService(db *gorm.DB, userService *UserService, groupService *UserGroupService) *LdapService { + return &LdapService{db: db, userService: userService, groupService: groupService} +} + +func ldapInit() *ldap.Conn { + // Setup AD Connection + ldapURL := common.EnvConfig.LDAPServer + ":" + common.EnvConfig.LDAPPort + client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: common.EnvConfig.LDAPTLSVerify})) + if err != nil { + //TODO Handle Errors Better + panic(err) + } + + // Bind as Service Account + err = client.Bind(common.EnvConfig.LDAPBindUser, common.EnvConfig.LDAPBindPassword) + if err != nil { + //TODO Handle Errors Better + panic(err) + } + return client +} + +func (s *LdapService) GetLdapGroups() error { + client := ldapInit() + baseDN := common.EnvConfig.LDAPSearchBase + filter := "(objectClass=groupOfUniqueNames)" + + searchAttrs := []string{ + common.EnvConfig.LDAPGroupAttribute, + "member", + } + + searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) + result, err := client.Search(searchReq) + if err != nil { + fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) + } + + var groupError error + + if len(result.Entries) >= 1 { + + for _, value := range result.Entries { + + syncGroup := dto.UserGroupCreateDto{ + Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + } + + _, groupError = s.groupService.Create(syncGroup) + + } + + client.Close() + return groupError + } else { + fmt.Println("No Groups Found") + return groupError + } + +} + +func (s *LdapService) GetLdapUsers() error { + client := ldapInit() + baseDN := common.EnvConfig.LDAPSearchBase + filter := "(objectClass=person)" + + searchAttrs := []string{ + "mail", + "memberOf", + "givenName", + "sn", + "cn", + common.EnvConfig.LDAPUsernameAttribute, // Search for the Username Attribute supplied by the user. + } + + // Filters must start and finish with ()! + searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) + + result, err := client.Search(searchReq) + if err != nil { + fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) + } + + var userError error + + if len(result.Entries) >= 1 { + + for _, value := range result.Entries { + + newUserModel := model.User{ + Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), + Email: value.GetAttributeValue("mail"), + FirstName: value.GetAttributeValue("givenName"), + LastName: value.GetAttributeValue("sn"), + IsAdmin: false, + } + + if s.userService.checkDuplicatedFields(newUserModel) == nil { + newUser := dto.UserCreateDto{ + Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), + Email: value.GetAttributeValue("mail"), + FirstName: value.GetAttributeValue("givenName"), + LastName: value.GetAttributeValue("sn"), + IsAdmin: false, + } + _, userError = s.userService.CreateUser(newUser) + } else { + // Update Exsisting User Entry Logic here. + } + + } + + client.Close() + return userError + + } else { + fmt.Println("No Users Found") + return userError + } + +} From 95b0897f3c0fd39cbbc7fc443ebc1f6b3cde81ae Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Wed, 15 Jan 2025 14:29:10 +0100 Subject: [PATCH 32/57] replace LDAP_SERVER and LDAP_PORT with LDAP_URL --- README.md | 24 +- backend/.env.example | 2 +- backend/internal/common/env_config.go | 6 +- backend/internal/service/ldap_service.go | 281 +++++++++++------------ 4 files changed, 154 insertions(+), 159 deletions(-) diff --git a/README.md b/README.md index 4015abf..2b899b7 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Additionally, what makes Pocket ID special is that it only supports [passkey](ht - [Nginx Reverse Proxy](#nginx-reverse-proxy) - [Proxy Services with Pocket ID](#proxy-services-with-pocket-id) - [Update](#update) - - [Docker](#docker) - - [Stand-alone](#stand-alone) + - [Docker](#docker) + - [Stand-alone](#stand-alone) - [Environment variables](#environment-variables) - [Account recovery](#account-recovery) - [Contribute](#contribute) @@ -172,17 +172,15 @@ docker compose up -d | `INTERNAL_BACKEND_URL` | `http://localhost:8080` | no | The URL where the backend is accessible. | | `GEOLITE_DB_PATH` | `data/GeoLite2-City.mmdb` | no | The path where the GeoLite2 database should be stored. | | `CADDY_PORT` | `80` | no | The port on which Caddy should listen. Caddy is only active inside the Docker container. If you want to change the exposed port of the container then you sould change this variable. | -| `PORT` | `3000` | no | The port on which the frontend should listen. -| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen -| `LDAP_SERVER` | `` | yes | The Server of your ldap instance with the protocol. ie: ldaps://ldap.example.com -| `LDAP_PORT` | `` | yes | The port that your ldap server listens on. -| `LDAP_BIND_USER` | `` | yes | The bind user for your ldap instance. -| `LDAP_BIND_PASSWORD` | `` | yes | The bind user password for your ldap instance. -| `LDAP_SEARCH_BASE` | `` | yes | The OU to search for all LDAP Objects -| `LDAP_TLS_VERIFY` | `false` | yes | Choose to Verify LDAPS Certifcates or ignore them. -| `LDAP_USERNAME_ATTRIBUTE` | `` | yes | The LDAP Attribute to use for the username of a user. -| `LDAP_GROUP_ATTRIBUTE` | `` | yes | The LDAP Attribute to use for the Name and Claim of a Group. - +| `PORT` | `3000` | no | The port on which the frontend should listen. | +| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen | +| `LDAP_URL` | `-` | yes | The URL of your ldap instance with the protocol and port. ie: ldaps://ldap.example.com:3890 | +| `LDAP_BIND_USER` | `-` | yes | The bind user for your ldap instance. | +| `LDAP_BIND_PASSWORD` | `-` | yes | The bind user password for your ldap instance. | +| `LDAP_SEARCH_BASE` | `-` | yes | The OU to search for all LDAP Objects | +| `LDAP_TLS_VERIFY` | `false` | yes | Choose to Verify LDAPS Certifcates or ignore them. | +| `LDAP_USERNAME_ATTRIBUTE` | `-` | yes | The LDAP Attribute to use for the username of a user. | +| `LDAP_GROUP_ATTRIBUTE` | `-` | yes | The LDAP Attribute to use for the Name and Claim of a Group. | ## Account recovery diff --git a/backend/.env.example b/backend/.env.example index c09bc47..18d6b38 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -6,7 +6,7 @@ POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket- UPLOAD_PATH=data/uploads PORT=8080 HOST=localhost -LDAP_SERVER=ldaps://ldap.example.com +LDAP_URL=ldaps://ldap.example.com:3890 LDAP_PORT=636 LDAP_BIND_USER=CN=user,DC=example,DC=com LDAP_BIND_PASSWORD=securepasswordhere diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 9c42b17..40e5d49 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -25,8 +25,7 @@ type EnvConfigSchema struct { Host string `env:"HOST"` MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"` GeoLiteDBPath string `env:"GEOLITE_DB_PATH"` - LDAPServer string `env:"LDAP_SERVER"` - LDAPPort string `env:"LDAP_PORT"` + LDAPUrl string `env:"LDAP_URL"` LDAPBindUser string `env:"LDAP_BIND_USER"` LDAPBindPassword string `env:"LDAP_BIND_PASSWORD"` LDAPSearchBase string `env:"LDAP_SEARCH_BASE"` @@ -46,8 +45,7 @@ var EnvConfig = &EnvConfigSchema{ Host: "localhost", MaxMindLicenseKey: "", GeoLiteDBPath: "data/GeoLite2-City.mmdb", - LDAPServer: "", - LDAPPort: "", + LDAPUrl: "", LDAPBindUser: "", LDAPBindPassword: "", LDAPSearchBase: "", diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 5ca70b0..c723434 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -1,141 +1,140 @@ -package service - -import ( - "crypto/tls" - "fmt" - - "github.com/go-ldap/ldap/v3" - "github.com/stonith404/pocket-id/backend/internal/common" - "github.com/stonith404/pocket-id/backend/internal/dto" - "github.com/stonith404/pocket-id/backend/internal/model" - "gorm.io/gorm" -) - -type LdapService struct { - db *gorm.DB - userService *UserService - groupService *UserGroupService -} - -func NewLdapService(db *gorm.DB, userService *UserService, groupService *UserGroupService) *LdapService { - return &LdapService{db: db, userService: userService, groupService: groupService} -} - -func ldapInit() *ldap.Conn { - // Setup AD Connection - ldapURL := common.EnvConfig.LDAPServer + ":" + common.EnvConfig.LDAPPort - client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: common.EnvConfig.LDAPTLSVerify})) - if err != nil { - //TODO Handle Errors Better - panic(err) - } - - // Bind as Service Account - err = client.Bind(common.EnvConfig.LDAPBindUser, common.EnvConfig.LDAPBindPassword) - if err != nil { - //TODO Handle Errors Better - panic(err) - } - return client -} - -func (s *LdapService) GetLdapGroups() error { - client := ldapInit() - baseDN := common.EnvConfig.LDAPSearchBase - filter := "(objectClass=groupOfUniqueNames)" - - searchAttrs := []string{ - common.EnvConfig.LDAPGroupAttribute, - "member", - } - - searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) - result, err := client.Search(searchReq) - if err != nil { - fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) - } - - var groupError error - - if len(result.Entries) >= 1 { - - for _, value := range result.Entries { - - syncGroup := dto.UserGroupCreateDto{ - Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), - FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), - } - - _, groupError = s.groupService.Create(syncGroup) - - } - - client.Close() - return groupError - } else { - fmt.Println("No Groups Found") - return groupError - } - -} - -func (s *LdapService) GetLdapUsers() error { - client := ldapInit() - baseDN := common.EnvConfig.LDAPSearchBase - filter := "(objectClass=person)" - - searchAttrs := []string{ - "mail", - "memberOf", - "givenName", - "sn", - "cn", - common.EnvConfig.LDAPUsernameAttribute, // Search for the Username Attribute supplied by the user. - } - - // Filters must start and finish with ()! - searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) - - result, err := client.Search(searchReq) - if err != nil { - fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) - } - - var userError error - - if len(result.Entries) >= 1 { - - for _, value := range result.Entries { - - newUserModel := model.User{ - Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), - Email: value.GetAttributeValue("mail"), - FirstName: value.GetAttributeValue("givenName"), - LastName: value.GetAttributeValue("sn"), - IsAdmin: false, - } - - if s.userService.checkDuplicatedFields(newUserModel) == nil { - newUser := dto.UserCreateDto{ - Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), - Email: value.GetAttributeValue("mail"), - FirstName: value.GetAttributeValue("givenName"), - LastName: value.GetAttributeValue("sn"), - IsAdmin: false, - } - _, userError = s.userService.CreateUser(newUser) - } else { - // Update Exsisting User Entry Logic here. - } - - } - - client.Close() - return userError - - } else { - fmt.Println("No Users Found") - return userError - } - -} +package service + +import ( + "crypto/tls" + "fmt" + + "github.com/go-ldap/ldap/v3" + "github.com/stonith404/pocket-id/backend/internal/common" + "github.com/stonith404/pocket-id/backend/internal/dto" + "github.com/stonith404/pocket-id/backend/internal/model" + "gorm.io/gorm" +) + +type LdapService struct { + db *gorm.DB + userService *UserService + groupService *UserGroupService +} + +func NewLdapService(db *gorm.DB, userService *UserService, groupService *UserGroupService) *LdapService { + return &LdapService{db: db, userService: userService, groupService: groupService} +} + +func ldapInit() *ldap.Conn { + // Setup AD Connection + client, err := ldap.DialURL(common.EnvConfig.LDAPUrl, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: common.EnvConfig.LDAPTLSVerify})) + if err != nil { + //TODO Handle Errors Better + panic(err) + } + + // Bind as Service Account + err = client.Bind(common.EnvConfig.LDAPBindUser, common.EnvConfig.LDAPBindPassword) + if err != nil { + //TODO Handle Errors Better + panic(err) + } + return client +} + +func (s *LdapService) GetLdapGroups() error { + client := ldapInit() + baseDN := common.EnvConfig.LDAPSearchBase + filter := "(objectClass=groupOfUniqueNames)" + + searchAttrs := []string{ + common.EnvConfig.LDAPGroupAttribute, + "member", + } + + searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) + result, err := client.Search(searchReq) + if err != nil { + fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) + } + + var groupError error + + if len(result.Entries) >= 1 { + + for _, value := range result.Entries { + + syncGroup := dto.UserGroupCreateDto{ + Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + } + + _, groupError = s.groupService.Create(syncGroup) + + } + + client.Close() + return groupError + } else { + fmt.Println("No Groups Found") + return groupError + } + +} + +func (s *LdapService) GetLdapUsers() error { + client := ldapInit() + baseDN := common.EnvConfig.LDAPSearchBase + filter := "(objectClass=person)" + + searchAttrs := []string{ + "mail", + "memberOf", + "givenName", + "sn", + "cn", + common.EnvConfig.LDAPUsernameAttribute, // Search for the Username Attribute supplied by the user. + } + + // Filters must start and finish with ()! + searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) + + result, err := client.Search(searchReq) + if err != nil { + fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) + } + + var userError error + + if len(result.Entries) >= 1 { + + for _, value := range result.Entries { + + newUserModel := model.User{ + Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), + Email: value.GetAttributeValue("mail"), + FirstName: value.GetAttributeValue("givenName"), + LastName: value.GetAttributeValue("sn"), + IsAdmin: false, + } + + if s.userService.checkDuplicatedFields(newUserModel) == nil { + newUser := dto.UserCreateDto{ + Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), + Email: value.GetAttributeValue("mail"), + FirstName: value.GetAttributeValue("givenName"), + LastName: value.GetAttributeValue("sn"), + IsAdmin: false, + } + _, userError = s.userService.CreateUser(newUser) + } else { + // Update Exsisting User Entry Logic here. + } + + } + + client.Close() + return userError + + } else { + fmt.Println("No Users Found") + return userError + } + +} From 6ec1f981520740e86ab3f15f9ce0d6a533ccc9b8 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Wed, 15 Jan 2025 15:06:11 +0100 Subject: [PATCH 33/57] remove LDAP_PORT from `.env.example` --- backend/.env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/.env.example b/backend/.env.example index 18d6b38..edd9554 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -7,7 +7,6 @@ UPLOAD_PATH=data/uploads PORT=8080 HOST=localhost LDAP_URL=ldaps://ldap.example.com:3890 -LDAP_PORT=636 LDAP_BIND_USER=CN=user,DC=example,DC=com LDAP_BIND_PASSWORD=securepasswordhere LDAP_SEARCH_BASE=OU=Stuff,DC=example,DC=com From 49df72730c3fde24cf42e3ad1161389256bd40de Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Wed, 15 Jan 2025 16:07:46 +0100 Subject: [PATCH 34/57] update user if it already exists in the database --- backend/.env.example | 1 + backend/internal/common/env_config.go | 2 + backend/internal/dto/user_dto.go | 1 + backend/internal/model/user.go | 1 + backend/internal/service/ldap_service.go | 52 +++++++++---------- backend/internal/service/user_service.go | 1 + .../sqlite/20250115155817_ldap.down.sql | 1 + .../sqlite/20250115155817_ldap.up.sql | 1 + 8 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 backend/resources/migrations/sqlite/20250115155817_ldap.down.sql create mode 100644 backend/resources/migrations/sqlite/20250115155817_ldap.up.sql diff --git a/backend/.env.example b/backend/.env.example index edd9554..41c3b1f 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -11,5 +11,6 @@ LDAP_BIND_USER=CN=user,DC=example,DC=com LDAP_BIND_PASSWORD=securepasswordhere LDAP_SEARCH_BASE=OU=Stuff,DC=example,DC=com LDAP_TLS_VERIFY=false +LDAP_USER_ID_ATTRIBUTE=uid LDAP_USERNAME_ATTRIBUTE=uid LDAP_GROUP_ATTRIBUTE=uid diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 40e5d49..b4a72b0 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -30,6 +30,7 @@ type EnvConfigSchema struct { LDAPBindPassword string `env:"LDAP_BIND_PASSWORD"` LDAPSearchBase string `env:"LDAP_SEARCH_BASE"` LDAPTLSVerify bool `env:"LDAP_TLS_VERIFY"` + LDAPUserIdAttribute string `env:"LDAP_USER_ID_ATTRIBUTE"` LDAPUsernameAttribute string `env:"LDAP_USERNAME_ATTRIBUTE"` LDAPGroupAttribute string `env:"LDAP_GROUP_ATTRIBUTE"` } @@ -50,6 +51,7 @@ var EnvConfig = &EnvConfigSchema{ LDAPBindPassword: "", LDAPSearchBase: "", LDAPTLSVerify: false, + LDAPUserIdAttribute: "", LDAPUsernameAttribute: "", LDAPGroupAttribute: "", } diff --git a/backend/internal/dto/user_dto.go b/backend/internal/dto/user_dto.go index 7351bd1..41291db 100644 --- a/backend/internal/dto/user_dto.go +++ b/backend/internal/dto/user_dto.go @@ -18,6 +18,7 @@ type UserCreateDto struct { FirstName string `json:"firstName" binding:"required,min=1,max=50"` LastName string `json:"lastName" binding:"required,min=1,max=50"` IsAdmin bool `json:"isAdmin"` + LdapID string `json:"-"` } type OneTimeAccessTokenCreateDto struct { diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go index 8a8e619..4d551d5 100644 --- a/backend/internal/model/user.go +++ b/backend/internal/model/user.go @@ -14,6 +14,7 @@ type User struct { FirstName string `sortable:"true"` LastName string `sortable:"true"` IsAdmin bool `sortable:"true"` + LdapID *string CustomClaims []CustomClaim UserGroups []UserGroup `gorm:"many2many:user_groups_users;"` diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index c723434..dc26654 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -3,6 +3,7 @@ package service import ( "crypto/tls" "fmt" + "log" "github.com/go-ldap/ldap/v3" "github.com/stonith404/pocket-id/backend/internal/common" @@ -100,41 +101,38 @@ func (s *LdapService) GetLdapUsers() error { fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) } - var userError error + for _, value := range result.Entries { + ldapId := value.GetAttributeValue(common.EnvConfig.LDAPUserIdAttribute) - if len(result.Entries) >= 1 { + // Get the user from the database + var databaseUser model.User + s.db.Where("ldap_id = ?", ldapId).First(&databaseUser) - for _, value := range result.Entries { + newUser := dto.UserCreateDto{ + Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), + Email: value.GetAttributeValue("mail"), + FirstName: value.GetAttributeValue("givenName"), + LastName: value.GetAttributeValue("sn"), + IsAdmin: false, + LdapID: ldapId, + } - newUserModel := model.User{ - Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), - Email: value.GetAttributeValue("mail"), - FirstName: value.GetAttributeValue("givenName"), - LastName: value.GetAttributeValue("sn"), - IsAdmin: false, + if databaseUser.ID == "" { + _, err = s.userService.CreateUser(newUser) + if err != nil { + log.Printf("Error syncing user %s: %s", newUser.Username, err) } - - if s.userService.checkDuplicatedFields(newUserModel) == nil { - newUser := dto.UserCreateDto{ - Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), - Email: value.GetAttributeValue("mail"), - FirstName: value.GetAttributeValue("givenName"), - LastName: value.GetAttributeValue("sn"), - IsAdmin: false, - } - _, userError = s.userService.CreateUser(newUser) - } else { - // Update Exsisting User Entry Logic here. + } else { + _, err = s.userService.UpdateUser(databaseUser.ID, newUser, false) + if err != nil { + log.Printf("Error syncing user %s: %s", newUser.Username, err) } } - client.Close() - return userError - - } else { - fmt.Println("No Users Found") - return userError } + client.Close() + return nil + } diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index 1ffb349..bec2828 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -56,6 +56,7 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) { Email: input.Email, Username: input.Username, IsAdmin: input.IsAdmin, + LdapID: &input.LdapID, } if err := s.db.Create(&user).Error; err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { diff --git a/backend/resources/migrations/sqlite/20250115155817_ldap.down.sql b/backend/resources/migrations/sqlite/20250115155817_ldap.down.sql new file mode 100644 index 0000000..9dc84f3 --- /dev/null +++ b/backend/resources/migrations/sqlite/20250115155817_ldap.down.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP COLUMN ldap_id; \ No newline at end of file diff --git a/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql b/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql new file mode 100644 index 0000000..32a4437 --- /dev/null +++ b/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN ldap_id TEXT; \ No newline at end of file From 3c1d158283968e5b4546ddc33ebc9df3546f16e4 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Wed, 15 Jan 2025 10:35:56 -0600 Subject: [PATCH 35/57] remove old ldap.go file and folder --- backend/cmd/main.go | 3 - backend/internal/ldap/ldap.go | 127 ---------------------------------- 2 files changed, 130 deletions(-) delete mode 100644 backend/internal/ldap/ldap.go diff --git a/backend/cmd/main.go b/backend/cmd/main.go index c2b6b4d..2f29947 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -4,7 +4,4 @@ import "github.com/stonith404/pocket-id/backend/internal/bootstrap" func main() { bootstrap.Bootstrap() - // Uncomment the line below to only test the ldap functionality - // ldap.GetLdapUsers() - // ldap.GetLdapGroups() } diff --git a/backend/internal/ldap/ldap.go b/backend/internal/ldap/ldap.go deleted file mode 100644 index 1d67346..0000000 --- a/backend/internal/ldap/ldap.go +++ /dev/null @@ -1,127 +0,0 @@ -package ldap - -import ( - "crypto/tls" - "fmt" - - "github.com/go-ldap/ldap/v3" - "github.com/stonith404/pocket-id/backend/internal/common" - "github.com/stonith404/pocket-id/backend/internal/model" -) - -func ldapInit() *ldap.Conn { - // Setup AD Connection - ldapURL := common.EnvConfig.LDAPServer + ":" + common.EnvConfig.LDAPPort - client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: common.EnvConfig.LDAPTLSVerify})) - if err != nil { - //TODO Handle Errors Better - panic(err) - } - // defer client.Close() - - // Bind as Service Account - err = client.Bind(common.EnvConfig.LDAPBindUser, common.EnvConfig.LDAPBindPassword) - if err != nil { - //TODO Handle Errors Better - panic(err) - } - return client -} - -func GetLdapGroups() []model.UserGroup { - client := ldapInit() - baseDN := common.EnvConfig.LDAPSearchBase - filter := "(objectClass=groupOfUniqueNames)" - - searchAttrs := []string{ - common.EnvConfig.LDAPGroupAttribute, - "member", - } - - searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) - result, err := client.Search(searchReq) - if err != nil { - fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) - } - - if len(result.Entries) >= 1 { - - var ldapGroups []model.UserGroup - for _, value := range result.Entries { - group := model.UserGroup{ - Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), - FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), - } - ldapGroups = append(ldapGroups, group) - } - - //Below Loop only for debug testing - for _, group := range ldapGroups { - fmt.Printf("Group Name: %s\n", group.Name) - } - - client.Close() - - return ldapGroups - } else { - fmt.Println("No Groups Found") - panic(1) - } - -} - -func GetLdapUsers() []model.User { - client := ldapInit() - // user := username - baseDN := common.EnvConfig.LDAPSearchBase - filter := "(objectClass=person)" - - //TODO Make options in UI to configure what options should be synced etc etc, as this depends on what ldap backend is being used. - searchAttrs := []string{ - "mail", - "memberOf", - "givenName", - "sn", - "cn", - common.EnvConfig.LDAPUsernameAttribute, // Search for the Username Attribute supplied by the user. - } - - // Filters must start and finish with ()! - searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) - - result, err := client.Search(searchReq) - if err != nil { - fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) - } - - if len(result.Entries) >= 1 { - - var ldapUsers []model.User - for _, value := range result.Entries { - user := model.User{ - Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), - Email: value.GetAttributeValue("mail"), - FirstName: value.GetAttributeValue("givenName"), - LastName: value.GetAttributeValue("sn"), - } - ldapUsers = append(ldapUsers, user) - } - - //Below Loop only for debug testing - for _, user := range ldapUsers { - fmt.Printf("Username: %s\n", user.Username) - fmt.Printf("First Name: %s\n", user.FirstName) - fmt.Printf("Last Name: %s\n", user.LastName) - fmt.Printf("Email: %s\n", user.Email) - fmt.Printf("Admin: %t\n", user.IsAdmin) - } - - client.Close() - return ldapUsers - - } else { - fmt.Println("No Users Found") - panic(1) - } - -} From adf94816d55db155edd6856dfe8932880c4b0a1b Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Wed, 15 Jan 2025 10:39:50 -0600 Subject: [PATCH 36/57] added LDAP_USER_ID_ATTRIBUTE to README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b899b7..5ccf554 100644 --- a/README.md +++ b/README.md @@ -179,9 +179,11 @@ docker compose up -d | `LDAP_BIND_PASSWORD` | `-` | yes | The bind user password for your ldap instance. | | `LDAP_SEARCH_BASE` | `-` | yes | The OU to search for all LDAP Objects | | `LDAP_TLS_VERIFY` | `false` | yes | Choose to Verify LDAPS Certifcates or ignore them. | -| `LDAP_USERNAME_ATTRIBUTE` | `-` | yes | The LDAP Attribute to use for the username of a user. | +| `LDAP_USERNAME_ATTRIBUTE` | `-` | yes | The LDAP Attribute to use for the username of a user. +| `LDAP_USER_ID_ATTRIBUTE` | `-` | yes | The LDAP Attribute to uniquely identify a user from LDAP | `LDAP_GROUP_ATTRIBUTE` | `-` | yes | The LDAP Attribute to use for the Name and Claim of a Group. | + ## Account recovery There are two ways to create a one-time access link for a user: From 73e09dc0d7f8673f20d4136604859800d3fb5ea0 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Wed, 15 Jan 2025 13:40:15 -0600 Subject: [PATCH 37/57] update group if it already exists in the database --- backend/internal/service/ldap_service.go | 33 ++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index dc26654..6a31233 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -55,28 +55,35 @@ func (s *LdapService) GetLdapGroups() error { fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) } - var groupError error + for _, value := range result.Entries { - if len(result.Entries) >= 1 { + var databaseGroup model.UserGroup + groupUniqueName := value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute) + s.db.Where("name = ?", groupUniqueName).First(&databaseGroup) - for _, value := range result.Entries { + syncGroup := dto.UserGroupCreateDto{ + Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + } - syncGroup := dto.UserGroupCreateDto{ - Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), - FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + if databaseGroup.ID == "" { + _, err = s.groupService.Create(syncGroup) + if err != nil { + log.Printf("Error syncing group %s: %s", syncGroup.Name, err) + } + } else { + _, err := s.groupService.Update(databaseGroup.ID, syncGroup) + if err != nil { + log.Printf("Error syncing group %s: %s", syncGroup.Name, err) } - - _, groupError = s.groupService.Create(syncGroup) } - client.Close() - return groupError - } else { - fmt.Println("No Groups Found") - return groupError } + client.Close() + return nil + } func (s *LdapService) GetLdapUsers() error { From 704b2a88fc5958ab4cda9a89b42780668f5b6292 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Wed, 15 Jan 2025 16:51:11 -0600 Subject: [PATCH 38/57] sync ldap users into ldap groups (WIP) --- backend/internal/service/ldap_service.go | 35 ++++++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 6a31233..74fb678 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "fmt" "log" + "strings" "github.com/go-ldap/ldap/v3" "github.com/stonith404/pocket-id/backend/internal/common" @@ -57,24 +58,46 @@ func (s *LdapService) GetLdapGroups() error { for _, value := range result.Entries { + var usersToAddDto dto.UserGroupUpdateUsersDto + var userIDStrings []string + var groupMemberAddError error + + // Check if Group Already exsists var databaseGroup model.UserGroup groupUniqueName := value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute) s.db.Where("name = ?", groupUniqueName).First(&databaseGroup) + //Get Group Members and add to the correct Group + groupMembers := value.GetAttributeValues("member") + for _, member := range groupMembers { + //Normal output of this would be CN=username,ou=people,dc=example,dc=com + // Splitting at the = and , then just grabbing the username for that string + singleMember := strings.Split(strings.Split(member, "=")[1], ",")[0] + + var databaseUser model.User + s.db.Where("username = ?", singleMember).First(&databaseUser) + userIDStrings = append(userIDStrings, databaseUser.ID) + } + syncGroup := dto.UserGroupCreateDto{ Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), } if databaseGroup.ID == "" { - _, err = s.groupService.Create(syncGroup) - if err != nil { - log.Printf("Error syncing group %s: %s", syncGroup.Name, err) + var newGroup model.UserGroup + newGroup, groupMemberAddError = s.groupService.Create(syncGroup) + _, groupMemberAddError = s.groupService.UpdateUsers(newGroup.ID, usersToAddDto) + if groupMemberAddError != nil { + log.Printf("Error syncing group %s: %s", syncGroup.Name, groupMemberAddError) + return groupMemberAddError } } else { - _, err := s.groupService.Update(databaseGroup.ID, syncGroup) - if err != nil { - log.Printf("Error syncing group %s: %s", syncGroup.Name, err) + _, groupMemberAddError = s.groupService.Update(databaseGroup.ID, syncGroup) + _, groupMemberAddError = s.groupService.UpdateUsers(databaseGroup.ID, usersToAddDto) + if groupMemberAddError != nil { + log.Printf("Error syncing group %s: %s", syncGroup.Name, groupMemberAddError) + return groupMemberAddError } } From 2ed15f281adf868206a05a32341c573f0024cfa9 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Wed, 15 Jan 2025 17:53:01 -0600 Subject: [PATCH 39/57] sync ldap users into ldap groups implemented --- backend/cmd/main.go | 4 +- backend/internal/job/ldap_job.go | 66 ++++++++++++------------ backend/internal/service/ldap_service.go | 4 ++ 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 2f29947..b2ebc11 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -1,6 +1,8 @@ package main -import "github.com/stonith404/pocket-id/backend/internal/bootstrap" +import ( + "github.com/stonith404/pocket-id/backend/internal/bootstrap" +) func main() { bootstrap.Bootstrap() diff --git a/backend/internal/job/ldap_job.go b/backend/internal/job/ldap_job.go index 21b625d..b17a896 100644 --- a/backend/internal/job/ldap_job.go +++ b/backend/internal/job/ldap_job.go @@ -1,33 +1,33 @@ -package job - -import ( - "log" - - "github.com/go-co-op/gocron/v2" - "github.com/stonith404/pocket-id/backend/internal/service" -) - -type LdapJobs struct { - ldapService *service.LdapService -} - -func RegisterLdapJobs(ls *service.LdapService) { - scheduler, err := gocron.NewScheduler() - if err != nil { - log.Fatalf("Failed to create a new scheduler: %s", err) - } - - jobs := &LdapJobs{ldapService: ls} - - registerJob(scheduler, "SyncLdapUsers", "*/5 * * * *", jobs.ldapUserSyncJob) - registerJob(scheduler, "SyncLdapGroups", "*/5 * * * *", jobs.ldapGroupSyncJob) - scheduler.Start() -} - -func (j *LdapJobs) ldapUserSyncJob() error { - return j.ldapService.GetLdapUsers() -} - -func (j *LdapJobs) ldapGroupSyncJob() error { - return j.ldapService.GetLdapGroups() -} +package job + +import ( + "log" + + "github.com/go-co-op/gocron/v2" + "github.com/stonith404/pocket-id/backend/internal/service" +) + +type LdapJobs struct { + ldapService *service.LdapService +} + +func RegisterLdapJobs(ls *service.LdapService) { + scheduler, err := gocron.NewScheduler() + if err != nil { + log.Fatalf("Failed to create a new scheduler: %s", err) + } + + jobs := &LdapJobs{ldapService: ls} + + registerJob(scheduler, "SyncLdapUsers", "*/2 * * * *", jobs.ldapUserSyncJob) + registerJob(scheduler, "SyncLdapGroups", "*/3 * * * *", jobs.ldapGroupSyncJob) + scheduler.Start() +} + +func (j *LdapJobs) ldapUserSyncJob() error { + return j.ldapService.GetLdapUsers() +} + +func (j *LdapJobs) ldapGroupSyncJob() error { + return j.ldapService.GetLdapGroups() +} diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 74fb678..e1c0e11 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -84,6 +84,10 @@ func (s *LdapService) GetLdapGroups() error { FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), } + usersToAddDto = dto.UserGroupUpdateUsersDto{ + UserIDs: userIDStrings, + } + if databaseGroup.ID == "" { var newGroup model.UserGroup newGroup, groupMemberAddError = s.groupService.Create(syncGroup) From 2116ef7be42fd86d4e0899f65cc6e258f05fdac6 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Thu, 16 Jan 2025 14:59:57 +0100 Subject: [PATCH 40/57] undo type assertion changes in `package.json` --- frontend/svelte.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js index eb463e8..14125f4 100644 --- a/frontend/svelte.config.js +++ b/frontend/svelte.config.js @@ -1,6 +1,6 @@ import adapter from '@sveltejs/adapter-node'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; -import packageJson from './package.json' with { type: 'json' }; +import packageJson from './package.json' assert { type: 'json' }; /** @type {import('@sveltejs/kit').Config} */ const config = { From 8e6c54f119e8a1370f871d509de55a444ee7d223 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Thu, 16 Jan 2025 18:18:13 +0100 Subject: [PATCH 41/57] replace env configuration with UI configuration and disable LDAP user edit --- .../internal/bootstrap/router_bootstrap.go | 6 +- backend/internal/common/env_config.go | 16 -- backend/internal/common/errors.go | 11 +- .../controller/app_config_controller.go | 13 ++ .../internal/controller/user_controller.go | 2 +- .../controller/user_group_controller.go | 2 +- backend/internal/dto/app_config_dto.go | 37 ++-- backend/internal/dto/user_dto.go | 1 + backend/internal/dto/user_group_dto.go | 3 + backend/internal/job/ldap_job.go | 72 ++++---- backend/internal/model/app_config.go | 19 ++- backend/internal/model/user_group.go | 3 +- .../internal/service/app_config_service.go | 58 +++++++ backend/internal/service/ldap_service.go | 147 ++++++++++------ .../internal/service/user_group_service.go | 8 +- backend/internal/service/user_service.go | 7 +- .../sqlite/20250115155817_ldap.down.sql | 3 +- .../sqlite/20250115155817_ldap.up.sql | 3 +- .../lib/components/checkbox-with-label.svelte | 2 +- frontend/src/lib/components/form-input.svelte | 4 +- .../src/lib/services/app-config-service.ts | 4 + .../lib/types/application-configuration.ts | 16 ++ frontend/src/lib/types/user-group.type.ts | 3 +- frontend/src/lib/types/user.type.ts | 3 +- .../application-configuration/+page.svelte | 21 ++- .../forms/app-config-email-form.svelte | 1 - .../forms/app-config-ldap-form.svelte | 161 ++++++++++++++++++ .../admin/user-groups/[id]/+page.svelte | 6 +- .../admin/user-groups/user-group-form.svelte | 3 + .../settings/admin/users/[id]/+page.svelte | 6 +- .../settings/admin/users/user-form.svelte | 51 +++--- 31 files changed, 531 insertions(+), 161 deletions(-) create mode 100644 frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index bfe6fa1..6ee940e 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -43,14 +43,14 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) { oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService) testService := service.NewTestService(db, appConfigService) userGroupService := service.NewUserGroupService(db) - ldapService := service.NewLdapService(db, userService, userGroupService) + ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService) r.Use(middleware.NewCorsMiddleware().Add()) r.Use(middleware.NewErrorHandlerMiddleware().Add()) r.Use(middleware.NewRateLimitMiddleware().Add(rate.Every(time.Second), 60)) r.Use(middleware.NewJwtAuthMiddleware(jwtService, true).Add(false)) - job.RegisterLdapJobs(ldapService) + job.RegisterLdapJobs(ldapService, appConfigService) // Initialize middleware jwtAuthMiddleware := middleware.NewJwtAuthMiddleware(jwtService, false) @@ -61,7 +61,7 @@ func initRouter(db *gorm.DB, appConfigService *service.AppConfigService) { controller.NewWebauthnController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), webauthnService) controller.NewOidcController(apiGroup, jwtAuthMiddleware, fileSizeLimitMiddleware, oidcService, jwtService) controller.NewUserController(apiGroup, jwtAuthMiddleware, middleware.NewRateLimitMiddleware(), userService, appConfigService) - controller.NewAppConfigController(apiGroup, jwtAuthMiddleware, appConfigService, emailService) + controller.NewAppConfigController(apiGroup, jwtAuthMiddleware, appConfigService, emailService, ldapService) controller.NewAuditLogController(apiGroup, auditLogService, jwtAuthMiddleware) controller.NewUserGroupController(apiGroup, jwtAuthMiddleware, userGroupService) controller.NewCustomClaimController(apiGroup, jwtAuthMiddleware, customClaimService) diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index b4a72b0..6071cc3 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -25,14 +25,6 @@ type EnvConfigSchema struct { Host string `env:"HOST"` MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"` GeoLiteDBPath string `env:"GEOLITE_DB_PATH"` - LDAPUrl string `env:"LDAP_URL"` - LDAPBindUser string `env:"LDAP_BIND_USER"` - LDAPBindPassword string `env:"LDAP_BIND_PASSWORD"` - LDAPSearchBase string `env:"LDAP_SEARCH_BASE"` - LDAPTLSVerify bool `env:"LDAP_TLS_VERIFY"` - LDAPUserIdAttribute string `env:"LDAP_USER_ID_ATTRIBUTE"` - LDAPUsernameAttribute string `env:"LDAP_USERNAME_ATTRIBUTE"` - LDAPGroupAttribute string `env:"LDAP_GROUP_ATTRIBUTE"` } var EnvConfig = &EnvConfigSchema{ @@ -46,14 +38,6 @@ var EnvConfig = &EnvConfigSchema{ Host: "localhost", MaxMindLicenseKey: "", GeoLiteDBPath: "data/GeoLite2-City.mmdb", - LDAPUrl: "", - LDAPBindUser: "", - LDAPBindPassword: "", - LDAPSearchBase: "", - LDAPTLSVerify: false, - LDAPUserIdAttribute: "", - LDAPUsernameAttribute: "", - LDAPGroupAttribute: "", } func init() { diff --git a/backend/internal/common/errors.go b/backend/internal/common/errors.go index 55773eb..eb756de 100644 --- a/backend/internal/common/errors.go +++ b/backend/internal/common/errors.go @@ -58,7 +58,9 @@ func (e *OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return 400 } type OidcInvalidCallbackURLError struct{} -func (e *OidcInvalidCallbackURLError) Error() string { return "invalid callback URL, it might be necessary for an admin to fix this" } +func (e *OidcInvalidCallbackURLError) Error() string { + return "invalid callback URL, it might be necessary for an admin to fix this" +} func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return 400 } type FileTypeNotSupportedError struct{} @@ -160,3 +162,10 @@ func (e *OidcMissingCodeChallengeError) Error() string { return "Missing code challenge" } func (e *OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest } + +type LdapUserUpdateError struct{} + +func (e *LdapUserUpdateError) Error() string { + return "LDAP users can't be updated" +} +func (e *LdapUserUpdateError) HttpStatusCode() int { return http.StatusForbidden } diff --git a/backend/internal/controller/app_config_controller.go b/backend/internal/controller/app_config_controller.go index 6bfe8c2..b065388 100644 --- a/backend/internal/controller/app_config_controller.go +++ b/backend/internal/controller/app_config_controller.go @@ -16,11 +16,13 @@ func NewAppConfigController( jwtAuthMiddleware *middleware.JwtAuthMiddleware, appConfigService *service.AppConfigService, emailService *service.EmailService, + ldapService *service.LdapService, ) { acc := &AppConfigController{ appConfigService: appConfigService, emailService: emailService, + ldapService: ldapService, } group.GET("/application-configuration", acc.listAppConfigHandler) group.GET("/application-configuration/all", jwtAuthMiddleware.Add(true), acc.listAllAppConfigHandler) @@ -34,11 +36,13 @@ func NewAppConfigController( group.PUT("/application-configuration/background-image", jwtAuthMiddleware.Add(true), acc.updateBackgroundImageHandler) group.POST("/application-configuration/test-email", jwtAuthMiddleware.Add(true), acc.testEmailHandler) + group.POST("/application-configuration/sync-ldap", jwtAuthMiddleware.Add(true), acc.syncLdapHandler) } type AppConfigController struct { appConfigService *service.AppConfigService emailService *service.EmailService + ldapService *service.LdapService } func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) { @@ -182,6 +186,15 @@ func (acc *AppConfigController) updateImage(c *gin.Context, imageName string, ol c.Status(http.StatusNoContent) } +func (acc *AppConfigController) syncLdapHandler(c *gin.Context) { + err := acc.ldapService.SyncAll() + if err != nil { + c.Error(err) + return + } + + c.Status(http.StatusNoContent) +} func (acc *AppConfigController) testEmailHandler(c *gin.Context) { userID := c.GetString("userID") diff --git a/backend/internal/controller/user_controller.go b/backend/internal/controller/user_controller.go index ccfcef7..dbf4a4f 100644 --- a/backend/internal/controller/user_controller.go +++ b/backend/internal/controller/user_controller.go @@ -202,7 +202,7 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) { userID = c.Param("id") } - user, err := uc.UserService.UpdateUser(userID, input, updateOwnUser) + user, err := uc.UserService.UpdateUser(userID, input, updateOwnUser, false) if err != nil { c.Error(err) return diff --git a/backend/internal/controller/user_group_controller.go b/backend/internal/controller/user_group_controller.go index ebdf727..c25df94 100644 --- a/backend/internal/controller/user_group_controller.go +++ b/backend/internal/controller/user_group_controller.go @@ -107,7 +107,7 @@ func (ugc *UserGroupController) update(c *gin.Context) { return } - group, err := ugc.UserGroupService.Update(c.Param("id"), input) + group, err := ugc.UserGroupService.Update(c.Param("id"), input, false) if err != nil { c.Error(err) return diff --git a/backend/internal/dto/app_config_dto.go b/backend/internal/dto/app_config_dto.go index a10e4d7..c2f8002 100644 --- a/backend/internal/dto/app_config_dto.go +++ b/backend/internal/dto/app_config_dto.go @@ -12,16 +12,29 @@ type AppConfigVariableDto struct { } type AppConfigUpdateDto struct { - AppName string `json:"appName" binding:"required,min=1,max=30"` - SessionDuration string `json:"sessionDuration" binding:"required"` - EmailsVerified string `json:"emailsVerified" binding:"required"` - AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"` - EmailEnabled string `json:"emailEnabled" binding:"required"` - SmtHost string `json:"smtpHost"` - SmtpPort string `json:"smtpPort"` - SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"` - SmtpUser string `json:"smtpUser"` - SmtpPassword string `json:"smtpPassword"` - SmtpTls string `json:"smtpTls"` - SmtpSkipCertVerify string `json:"smtpSkipCertVerify"` + AppName string `json:"appName" binding:"required,min=1,max=30"` + SessionDuration string `json:"sessionDuration" binding:"required"` + EmailsVerified string `json:"emailsVerified" binding:"required"` + AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"` + EmailEnabled string `json:"emailEnabled" binding:"required"` + SmtHost string `json:"smtpHost"` + SmtpPort string `json:"smtpPort"` + SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"` + SmtpUser string `json:"smtpUser"` + SmtpPassword string `json:"smtpPassword"` + SmtpTls string `json:"smtpTls"` + SmtpSkipCertVerify string `json:"smtpSkipCertVerify"` + LdapEnabled string `json:"ldapEnabled" binding:"required"` + LdapUrl string `json:"ldapUrl"` + LdapBindDn string `json:"ldapBindDn"` + LdapBindPassword string `json:"ldapBindPassword"` + LdapBase string `json:"ldapBase"` + LdapSkipCertVerify string `json:"ldapSkipCertVerify"` + LdapAttributeUserUniqueIdentifier string `json:"ldapAttributeUserUniqueIdentifier"` + LdapAttributeUserUsername string `json:"ldapAttributeUserUsername"` + LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"` + LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"` + LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"` + LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"` + LdapAttributeGroupName string `json:"ldapAttributeGroupName"` } diff --git a/backend/internal/dto/user_dto.go b/backend/internal/dto/user_dto.go index 41291db..9d93da6 100644 --- a/backend/internal/dto/user_dto.go +++ b/backend/internal/dto/user_dto.go @@ -10,6 +10,7 @@ type UserDto struct { LastName string `json:"lastName"` IsAdmin bool `json:"isAdmin"` CustomClaims []CustomClaimDto `json:"customClaims"` + LdapID *string `json:"ldapId"` } type UserCreateDto struct { diff --git a/backend/internal/dto/user_group_dto.go b/backend/internal/dto/user_group_dto.go index 90d3fb1..d4d87d6 100644 --- a/backend/internal/dto/user_group_dto.go +++ b/backend/internal/dto/user_group_dto.go @@ -10,6 +10,7 @@ type UserGroupDtoWithUsers struct { Name string `json:"name"` CustomClaims []CustomClaimDto `json:"customClaims"` Users []UserDto `json:"users"` + LdapID *string `json:"ldapId"` CreatedAt datatype.DateTime `json:"createdAt"` } @@ -19,12 +20,14 @@ type UserGroupDtoWithUserCount struct { Name string `json:"name"` CustomClaims []CustomClaimDto `json:"customClaims"` UserCount int64 `json:"userCount"` + LdapID *string `json:"ldapId"` CreatedAt datatype.DateTime `json:"createdAt"` } type UserGroupCreateDto struct { FriendlyName string `json:"friendlyName" binding:"required,min=2,max=50"` Name string `json:"name" binding:"required,min=2,max=255"` + LdapID string `json:"-"` } type UserGroupUpdateUsersDto struct { diff --git a/backend/internal/job/ldap_job.go b/backend/internal/job/ldap_job.go index b17a896..415bec9 100644 --- a/backend/internal/job/ldap_job.go +++ b/backend/internal/job/ldap_job.go @@ -1,33 +1,39 @@ -package job - -import ( - "log" - - "github.com/go-co-op/gocron/v2" - "github.com/stonith404/pocket-id/backend/internal/service" -) - -type LdapJobs struct { - ldapService *service.LdapService -} - -func RegisterLdapJobs(ls *service.LdapService) { - scheduler, err := gocron.NewScheduler() - if err != nil { - log.Fatalf("Failed to create a new scheduler: %s", err) - } - - jobs := &LdapJobs{ldapService: ls} - - registerJob(scheduler, "SyncLdapUsers", "*/2 * * * *", jobs.ldapUserSyncJob) - registerJob(scheduler, "SyncLdapGroups", "*/3 * * * *", jobs.ldapGroupSyncJob) - scheduler.Start() -} - -func (j *LdapJobs) ldapUserSyncJob() error { - return j.ldapService.GetLdapUsers() -} - -func (j *LdapJobs) ldapGroupSyncJob() error { - return j.ldapService.GetLdapGroups() -} +package job + +import ( + "log" + + "github.com/go-co-op/gocron/v2" + "github.com/stonith404/pocket-id/backend/internal/service" +) + +type LdapJobs struct { + ldapService *service.LdapService + appConfigService *service.AppConfigService +} + +func RegisterLdapJobs(ldapService *service.LdapService, appConfigService *service.AppConfigService) { + jobs := &LdapJobs{ldapService: ldapService, appConfigService: appConfigService} + + scheduler, err := gocron.NewScheduler() + if err != nil { + log.Fatalf("Failed to create a new scheduler: %s", err) + } + + // Register the job to run every hour + registerJob(scheduler, "SyncLdap", "0 * * * *", jobs.syncLdap) + + // Run the job immediately on startup + if err := jobs.syncLdap(); err != nil { + log.Fatalf("Failed to sync LDAP: %s", err) + } + + scheduler.Start() +} + +func (j *LdapJobs) syncLdap() error { + if j.appConfigService.DbConfig.LdapEnabled.Value == "true" { + return j.ldapService.SyncAll() + } + return nil +} diff --git a/backend/internal/model/app_config.go b/backend/internal/model/app_config.go index b8e4471..6052ae8 100644 --- a/backend/internal/model/app_config.go +++ b/backend/internal/model/app_config.go @@ -10,15 +10,16 @@ type AppConfigVariable struct { } type AppConfig struct { + // General AppName AppConfigVariable SessionDuration AppConfigVariable EmailsVerified AppConfigVariable AllowOwnAccountEdit AppConfigVariable - + // Internal BackgroundImageType AppConfigVariable LogoLightImageType AppConfigVariable LogoDarkImageType AppConfigVariable - + // Email EmailEnabled AppConfigVariable SmtpHost AppConfigVariable SmtpPort AppConfigVariable @@ -27,4 +28,18 @@ type AppConfig struct { SmtpPassword AppConfigVariable SmtpTls AppConfigVariable SmtpSkipCertVerify AppConfigVariable + // LDAP + LdapEnabled AppConfigVariable + LdapUrl AppConfigVariable + LdapBindDn AppConfigVariable + LdapBindPassword AppConfigVariable + LdapBase AppConfigVariable + LdapSkipCertVerify AppConfigVariable + LdapAttributeUserUniqueIdentifier AppConfigVariable + LdapAttributeUserUsername AppConfigVariable + LdapAttributeUserEmail AppConfigVariable + LdapAttributeUserFirstName AppConfigVariable + LdapAttributeUserLastName AppConfigVariable + LdapAttributeGroupUniqueIdentifier AppConfigVariable + LdapAttributeGroupName AppConfigVariable } diff --git a/backend/internal/model/user_group.go b/backend/internal/model/user_group.go index 786a8b0..caa670b 100644 --- a/backend/internal/model/user_group.go +++ b/backend/internal/model/user_group.go @@ -3,7 +3,8 @@ package model type UserGroup struct { Base FriendlyName string `sortable:"true"` - Name string `gorm:"unique" sortable:"true"` + Name string `sortable:"true"` + LdapID *string Users []User `gorm:"many2many:user_groups_users;"` CustomClaims []CustomClaim } diff --git a/backend/internal/service/app_config_service.go b/backend/internal/service/app_config_service.go index 84f2dfd..8bf26ea 100644 --- a/backend/internal/service/app_config_service.go +++ b/backend/internal/service/app_config_service.go @@ -30,6 +30,7 @@ func NewAppConfigService(db *gorm.DB) *AppConfigService { } var defaultDbConfig = model.AppConfig{ + // General AppName: model.AppConfigVariable{ Key: "appName", Type: "string", @@ -52,6 +53,7 @@ var defaultDbConfig = model.AppConfig{ IsPublic: true, DefaultValue: "true", }, + // Internal BackgroundImageType: model.AppConfigVariable{ Key: "backgroundImageType", Type: "string", @@ -70,6 +72,7 @@ var defaultDbConfig = model.AppConfig{ IsInternal: true, DefaultValue: "svg", }, + // Email EmailEnabled: model.AppConfigVariable{ Key: "emailEnabled", Type: "bool", @@ -105,6 +108,61 @@ var defaultDbConfig = model.AppConfig{ Type: "bool", DefaultValue: "false", }, + // LDAP + LdapEnabled: model.AppConfigVariable{ + Key: "ldapEnabled", + Type: "bool", + DefaultValue: "false", + }, + LdapUrl: model.AppConfigVariable{ + Key: "ldapUrl", + Type: "string", + }, + LdapBindDn: model.AppConfigVariable{ + Key: "ldapBindDn", + Type: "string", + }, + LdapBindPassword: model.AppConfigVariable{ + Key: "ldapBindPassword", + Type: "string", + }, + LdapBase: model.AppConfigVariable{ + Key: "ldapBase", + Type: "string", + }, + LdapSkipCertVerify: model.AppConfigVariable{ + Key: "ldapSkipCertVerify", + Type: "bool", + DefaultValue: "false", + }, + LdapAttributeUserUniqueIdentifier: model.AppConfigVariable{ + Key: "ldapAttributeUserUniqueIdentifier", + Type: "string", + }, + LdapAttributeUserUsername: model.AppConfigVariable{ + Key: "ldapAttributeUserUsername", + Type: "string", + }, + LdapAttributeUserEmail: model.AppConfigVariable{ + Key: "ldapAttributeUserEmail", + Type: "string", + }, + LdapAttributeUserFirstName: model.AppConfigVariable{ + Key: "ldapAttributeUserFirstName", + Type: "string", + }, + LdapAttributeUserLastName: model.AppConfigVariable{ + Key: "ldapAttributeUserLastName", + Type: "string", + }, + LdapAttributeGroupUniqueIdentifier: model.AppConfigVariable{ + Key: "ldapAttributeGroupUniqueIdentifier", + Type: "string", + }, + LdapAttributeGroupName: model.AppConfigVariable{ + Key: "ldapAttributeGroupName", + Type: "string", + }, } func (s *AppConfigService) UpdateAppConfig(input dto.AppConfigUpdateDto) ([]model.AppConfigVariable, error) { diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index e1c0e11..1607658 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -7,71 +7,97 @@ import ( "strings" "github.com/go-ldap/ldap/v3" - "github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/model" "gorm.io/gorm" ) type LdapService struct { - db *gorm.DB - userService *UserService - groupService *UserGroupService + db *gorm.DB + appConfigService *AppConfigService + userService *UserService + groupService *UserGroupService } -func NewLdapService(db *gorm.DB, userService *UserService, groupService *UserGroupService) *LdapService { - return &LdapService{db: db, userService: userService, groupService: groupService} +func NewLdapService(db *gorm.DB, appConfigService *AppConfigService, userService *UserService, groupService *UserGroupService) *LdapService { + return &LdapService{db: db, appConfigService: appConfigService, userService: userService, groupService: groupService} } -func ldapInit() *ldap.Conn { +func (s *LdapService) createClient() (*ldap.Conn, error) { + if s.appConfigService.DbConfig.LdapEnabled.Value != "true" { + return nil, fmt.Errorf("LDAP is not enabled") + } // Setup AD Connection - client, err := ldap.DialURL(common.EnvConfig.LDAPUrl, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: common.EnvConfig.LDAPTLSVerify})) + ldapURL := s.appConfigService.DbConfig.LdapUrl.Value + skipTLSVerify := s.appConfigService.DbConfig.LdapSkipCertVerify.Value == "true" + client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: skipTLSVerify})) if err != nil { - //TODO Handle Errors Better - panic(err) + return nil, fmt.Errorf("failed to connect to LDAP: %w", err) } // Bind as Service Account - err = client.Bind(common.EnvConfig.LDAPBindUser, common.EnvConfig.LDAPBindPassword) + bindDn := s.appConfigService.DbConfig.LdapBindDn.Value + bindPassword := s.appConfigService.DbConfig.LdapBindPassword.Value + err = client.Bind(bindDn, bindPassword) if err != nil { - //TODO Handle Errors Better - panic(err) + return nil, fmt.Errorf("failed to bind to LDAP: %w", err) } - return client + return client, nil } -func (s *LdapService) GetLdapGroups() error { - client := ldapInit() - baseDN := common.EnvConfig.LDAPSearchBase +func (s *LdapService) SyncAll() error { + err := s.SyncUsers() + if err != nil { + return fmt.Errorf("failed to sync users: %w", err) + } + + err = s.SyncGroups() + if err != nil { + return fmt.Errorf("failed to sync groups: %w", err) + } + + return nil +} + +func (s *LdapService) SyncGroups() error { + // Setup LDAP Connection + client, err := s.createClient() + if err != nil { + return fmt.Errorf("failed to create LDAP client: %w", err) + } + defer client.Close() + + baseDN := s.appConfigService.DbConfig.LdapBase.Value + nameAttribute := s.appConfigService.DbConfig.LdapAttributeGroupName.Value + uniqueIdentifierAttribute := s.appConfigService.DbConfig.LdapAttributeGroupUniqueIdentifier.Value filter := "(objectClass=groupOfUniqueNames)" searchAttrs := []string{ - common.EnvConfig.LDAPGroupAttribute, + nameAttribute, + uniqueIdentifierAttribute, "member", } searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, searchAttrs, []ldap.Control{}) result, err := client.Search(searchReq) if err != nil { - fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) + return fmt.Errorf("failed to query LDAP: %w", err) } for _, value := range result.Entries { - var usersToAddDto dto.UserGroupUpdateUsersDto var userIDStrings []string - var groupMemberAddError error - // Check if Group Already exsists + // Try to find the group in the database + ldapId := value.GetAttributeValue(uniqueIdentifierAttribute) var databaseGroup model.UserGroup - groupUniqueName := value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute) - s.db.Where("name = ?", groupUniqueName).First(&databaseGroup) + s.db.Where("ldap_id = ?", ldapId).First(&databaseGroup) //Get Group Members and add to the correct Group groupMembers := value.GetAttributeValues("member") for _, member := range groupMembers { - //Normal output of this would be CN=username,ou=people,dc=example,dc=com - // Splitting at the = and , then just grabbing the username for that string + // Normal output of this would be CN=username,ou=people,dc=example,dc=com + // Splitting at the "=" and "," then just grabbing the username for that string singleMember := strings.Split(strings.Split(member, "=")[1], ",")[0] var databaseUser model.User @@ -80,8 +106,9 @@ func (s *LdapService) GetLdapGroups() error { } syncGroup := dto.UserGroupCreateDto{ - Name: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), - FriendlyName: value.GetAttributeValue(common.EnvConfig.LDAPGroupAttribute), + Name: value.GetAttributeValue(nameAttribute), + FriendlyName: value.GetAttributeValue(nameAttribute), + LdapID: value.GetAttributeValue(uniqueIdentifierAttribute), } usersToAddDto = dto.UserGroupUpdateUsersDto{ @@ -89,42 +116,56 @@ func (s *LdapService) GetLdapGroups() error { } if databaseGroup.ID == "" { - var newGroup model.UserGroup - newGroup, groupMemberAddError = s.groupService.Create(syncGroup) - _, groupMemberAddError = s.groupService.UpdateUsers(newGroup.ID, usersToAddDto) - if groupMemberAddError != nil { - log.Printf("Error syncing group %s: %s", syncGroup.Name, groupMemberAddError) - return groupMemberAddError + newGroup, err := s.groupService.Create(syncGroup) + if err != nil { + log.Printf("Error syncing group %s: %s", syncGroup.Name, err) + } else { + if _, err = s.groupService.UpdateUsers(newGroup.ID, usersToAddDto); err != nil { + log.Printf("Error syncing group %s: %s", syncGroup.Name, err) + } } } else { - _, groupMemberAddError = s.groupService.Update(databaseGroup.ID, syncGroup) - _, groupMemberAddError = s.groupService.UpdateUsers(databaseGroup.ID, usersToAddDto) - if groupMemberAddError != nil { - log.Printf("Error syncing group %s: %s", syncGroup.Name, groupMemberAddError) - return groupMemberAddError + _, err = s.groupService.Update(databaseGroup.ID, syncGroup, true) + _, err = s.groupService.UpdateUsers(databaseGroup.ID, usersToAddDto) + if err != nil { + log.Printf("Error syncing group %s: %s", syncGroup.Name, err) + return err } } } - client.Close() return nil } -func (s *LdapService) GetLdapUsers() error { - client := ldapInit() - baseDN := common.EnvConfig.LDAPSearchBase +func (s *LdapService) SyncUsers() error { + // Setup LDAP Connection + client, err := s.createClient() + if err != nil { + return fmt.Errorf("failed to create LDAP client: %w", err) + } + defer client.Close() + + baseDN := s.appConfigService.DbConfig.LdapBase.Value + uniqueIdentifierAttribute := s.appConfigService.DbConfig.LdapAttributeUserUniqueIdentifier.Value + usernameAttribute := s.appConfigService.DbConfig.LdapAttributeUserUsername.Value + emailAttribute := s.appConfigService.DbConfig.LdapAttributeUserEmail.Value + firstNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserFirstName.Value + lastNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserLastName.Value + filter := "(objectClass=person)" searchAttrs := []string{ - "mail", "memberOf", - "givenName", "sn", "cn", - common.EnvConfig.LDAPUsernameAttribute, // Search for the Username Attribute supplied by the user. + uniqueIdentifierAttribute, + usernameAttribute, + emailAttribute, + firstNameAttribute, + lastNameAttribute, } // Filters must start and finish with ()! @@ -136,17 +177,17 @@ func (s *LdapService) GetLdapUsers() error { } for _, value := range result.Entries { - ldapId := value.GetAttributeValue(common.EnvConfig.LDAPUserIdAttribute) + ldapId := value.GetAttributeValue(uniqueIdentifierAttribute) // Get the user from the database var databaseUser model.User s.db.Where("ldap_id = ?", ldapId).First(&databaseUser) newUser := dto.UserCreateDto{ - Username: value.GetAttributeValue(common.EnvConfig.LDAPUsernameAttribute), - Email: value.GetAttributeValue("mail"), - FirstName: value.GetAttributeValue("givenName"), - LastName: value.GetAttributeValue("sn"), + Username: value.GetAttributeValue(usernameAttribute), + Email: value.GetAttributeValue(emailAttribute), + FirstName: value.GetAttributeValue(firstNameAttribute), + LastName: value.GetAttributeValue(lastNameAttribute), IsAdmin: false, LdapID: ldapId, } @@ -157,7 +198,7 @@ func (s *LdapService) GetLdapUsers() error { log.Printf("Error syncing user %s: %s", newUser.Username, err) } } else { - _, err = s.userService.UpdateUser(databaseUser.ID, newUser, false) + _, err = s.userService.UpdateUser(databaseUser.ID, newUser, false, true) if err != nil { log.Printf("Error syncing user %s: %s", newUser.Username, err) } @@ -166,7 +207,5 @@ func (s *LdapService) GetLdapUsers() error { } - client.Close() return nil - } diff --git a/backend/internal/service/user_group_service.go b/backend/internal/service/user_group_service.go index 0d97132..489760d 100644 --- a/backend/internal/service/user_group_service.go +++ b/backend/internal/service/user_group_service.go @@ -58,6 +58,7 @@ func (s *UserGroupService) Create(input dto.UserGroupCreateDto) (group model.Use group = model.UserGroup{ FriendlyName: input.FriendlyName, Name: input.Name, + LdapID: &input.LdapID, } if err := s.db.Preload("Users").Create(&group).Error; err != nil { @@ -69,14 +70,19 @@ func (s *UserGroupService) Create(input dto.UserGroupCreateDto) (group model.Use return group, nil } -func (s *UserGroupService) Update(id string, input dto.UserGroupCreateDto) (group model.UserGroup, err error) { +func (s *UserGroupService) Update(id string, input dto.UserGroupCreateDto, allowLdapUpdate bool) (group model.UserGroup, err error) { group, err = s.Get(id) if err != nil { return model.UserGroup{}, err } + if group.LdapID != nil && !allowLdapUpdate { + return model.UserGroup{}, &common.LdapUserUpdateError{} + } + group.Name = input.Name group.FriendlyName = input.FriendlyName + group.LdapID = &input.LdapID if err := s.db.Preload("Users").Save(&group).Error; err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index bec2828..4b7c3be 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -67,11 +67,16 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) { return user, nil } -func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, updateOwnUser bool) (model.User, error) { +func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, allowLdapUpdate bool) (model.User, error) { var user model.User if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil { return model.User{}, err } + + if user.LdapID != nil && !allowLdapUpdate { + return model.User{}, &common.LdapUserUpdateError{} + } + user.FirstName = updatedUser.FirstName user.LastName = updatedUser.LastName user.Email = updatedUser.Email diff --git a/backend/resources/migrations/sqlite/20250115155817_ldap.down.sql b/backend/resources/migrations/sqlite/20250115155817_ldap.down.sql index 9dc84f3..894ce56 100644 --- a/backend/resources/migrations/sqlite/20250115155817_ldap.down.sql +++ b/backend/resources/migrations/sqlite/20250115155817_ldap.down.sql @@ -1 +1,2 @@ -ALTER TABLE users DROP COLUMN ldap_id; \ No newline at end of file +ALTER TABLE users DROP COLUMN ldap_id; +ALTER TABLE user_groups DROP COLUMN ldap_id; \ No newline at end of file diff --git a/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql b/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql index 32a4437..15107f5 100644 --- a/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql +++ b/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql @@ -1 +1,2 @@ -ALTER TABLE users ADD COLUMN ldap_id TEXT; \ No newline at end of file +ALTER TABLE users ADD COLUMN ldap_id TEXT UNIQUE; +ALTER TABLE user_groups ADD COLUMN ldap_id TEXT UNIQUE; \ No newline at end of file diff --git a/frontend/src/lib/components/checkbox-with-label.svelte b/frontend/src/lib/components/checkbox-with-label.svelte index 571ff02..3f2ffff 100644 --- a/frontend/src/lib/components/checkbox-with-label.svelte +++ b/frontend/src/lib/components/checkbox-with-label.svelte @@ -19,7 +19,7 @@ } = $props(); -
+
; label?: string; description?: string; + placeholder?: string; disabled?: boolean; type?: 'text' | 'password' | 'email' | 'number' | 'checkbox'; onInput?: (e: FormInputEvent) => void; @@ -38,7 +40,7 @@ {#if children} {@render children()} {:else if input} - onInput?.(e)} /> + onInput?.(e)} /> {/if} {#if input?.error}

{input.error}

diff --git a/frontend/src/lib/services/app-config-service.ts b/frontend/src/lib/services/app-config-service.ts index bb7013a..e5a8f04 100644 --- a/frontend/src/lib/services/app-config-service.ts +++ b/frontend/src/lib/services/app-config-service.ts @@ -57,6 +57,10 @@ export default class AppConfigService extends APIService { await this.api.post('/application-configuration/test-email'); } + async syncLdap() { + await this.api.post('/application-configuration/sync-ldap'); + } + async getVersionInformation() { const response = ( await axios.get('https://api.github.com/repos/stonith404/pocket-id/releases/latest') diff --git a/frontend/src/lib/types/application-configuration.ts b/frontend/src/lib/types/application-configuration.ts index bc28de3..b9f8a97 100644 --- a/frontend/src/lib/types/application-configuration.ts +++ b/frontend/src/lib/types/application-configuration.ts @@ -4,8 +4,10 @@ export type AppConfig = { }; export type AllAppConfig = AppConfig & { + // General sessionDuration: number; emailsVerified: boolean; + // Email emailEnabled: boolean; smtpHost: string; smtpPort: number; @@ -14,6 +16,20 @@ export type AllAppConfig = AppConfig & { smtpPassword: string; smtpTls: boolean; smtpSkipCertVerify: boolean; + // LDAP + ldapEnabled: boolean; + ldapUrl: string; + ldapBindDn: string; + ldapBindPassword: string; + ldapBase: string; + ldapSkipCertVerify: boolean; + ldapAttributeUserUniqueIdentifier: string; + ldapAttributeUserUsername: string; + ldapAttributeUserEmail: string; + ldapAttributeUserFirstName: string; + ldapAttributeUserLastName: string; + ldapAttributeGroupUniqueIdentifier: string; + ldapAttributeGroupName: string; }; export type AppConfigRawResponse = { diff --git a/frontend/src/lib/types/user-group.type.ts b/frontend/src/lib/types/user-group.type.ts index da02635..8306774 100644 --- a/frontend/src/lib/types/user-group.type.ts +++ b/frontend/src/lib/types/user-group.type.ts @@ -7,6 +7,7 @@ export type UserGroup = { name: string; createdAt: string; customClaims: CustomClaim[]; + ldapId?: string; }; export type UserGroupWithUsers = UserGroup & { @@ -17,4 +18,4 @@ export type UserGroupWithUserCount = UserGroup & { userCount: number; }; -export type UserGroupCreate = Pick; +export type UserGroupCreate = Pick; diff --git a/frontend/src/lib/types/user.type.ts b/frontend/src/lib/types/user.type.ts index 1157ecf..5366e52 100644 --- a/frontend/src/lib/types/user.type.ts +++ b/frontend/src/lib/types/user.type.ts @@ -8,6 +8,7 @@ export type User = { lastName: string; isAdmin: boolean; customClaims: CustomClaim[]; + ldapId?: string; }; -export type UserCreate = Omit; +export type UserCreate = Omit; diff --git a/frontend/src/routes/settings/admin/application-configuration/+page.svelte b/frontend/src/routes/settings/admin/application-configuration/+page.svelte index b8a6ad0..4ba4203 100644 --- a/frontend/src/routes/settings/admin/application-configuration/+page.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/+page.svelte @@ -7,6 +7,7 @@ import { toast } from 'svelte-sonner'; import AppConfigEmailForm from './forms/app-config-email-form.svelte'; import AppConfigGeneralForm from './forms/app-config-general-form.svelte'; + import AppConfigLdapForm from './forms/app-config-ldap-form.svelte'; import UpdateApplicationImages from './update-application-images.svelte'; let { data } = $props(); @@ -34,8 +35,12 @@ favicon: File | null ) { const faviconPromise = favicon ? appConfigService.updateFavicon(favicon) : Promise.resolve(); - const lightLogoPromise = logoLight ? appConfigService.updateLogo(logoLight, true) : Promise.resolve(); - const darkLogoPromise = logoDark ? appConfigService.updateLogo(logoDark, false) : Promise.resolve(); + const lightLogoPromise = logoLight + ? appConfigService.updateLogo(logoLight, true) + : Promise.resolve(); + const darkLogoPromise = logoDark + ? appConfigService.updateLogo(logoDark, false) + : Promise.resolve(); const backgroundImagePromise = backgroundImage ? appConfigService.updateBackgroundImage(backgroundImage) : Promise.resolve(); @@ -72,6 +77,18 @@ + + + LDAP + + Configure LDAP settings to sync users and groups from an LDAP server. + + + + + + + Images diff --git a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte index 191cce4..6321edc 100644 --- a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-email-form.svelte @@ -45,7 +45,6 @@ const { inputs, ...form } = createForm(formSchema, updatedAppConfig); async function onSubmit() { - console.log('submit'); const data = form.validate(); if (!data) return false; await callback({ diff --git a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte new file mode 100644 index 0000000..a9e4876 --- /dev/null +++ b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte @@ -0,0 +1,161 @@ + + +
+

Client Configuration

+
+ + + + + +
+

Attribute Mapping

+
+ + + + + + + +
+ +
+ {#if ldapEnabled} + + + + {:else} + + {/if} +
+
diff --git a/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte b/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte index a2abcb6..a84b6d6 100644 --- a/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte +++ b/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte @@ -11,6 +11,7 @@ import { toast } from 'svelte-sonner'; import UserGroupForm from '../user-group-form.svelte'; import UserSelection from '../user-selection.svelte'; + import { Badge } from '$lib/components/ui/badge'; let { data } = $props(); let userGroup = $state({ @@ -58,10 +59,13 @@ User Group Details {userGroup.name} -
+
Back + {#if !!userGroup.ldapId} + LDAP +{/if}
diff --git a/frontend/src/routes/settings/admin/user-groups/user-group-form.svelte b/frontend/src/routes/settings/admin/user-groups/user-group-form.svelte index 478173d..ea434da 100644 --- a/frontend/src/routes/settings/admin/user-groups/user-group-form.svelte +++ b/frontend/src/routes/settings/admin/user-groups/user-group-form.svelte @@ -14,6 +14,7 @@ } = $props(); let isLoading = $state(false); + let inputDisabled = $derived(!!existingUserGroup?.ldapId); let hasManualNameEdit = $state(!!existingUserGroup?.friendlyName); const userGroup = { @@ -57,6 +58,7 @@
+
+
diff --git a/frontend/src/routes/settings/admin/users/[id]/+page.svelte b/frontend/src/routes/settings/admin/users/[id]/+page.svelte index 657ae91..731f924 100644 --- a/frontend/src/routes/settings/admin/users/[id]/+page.svelte +++ b/frontend/src/routes/settings/admin/users/[id]/+page.svelte @@ -1,4 +1,5 @@
-
-
- +
+
+
+ +
+
+ +
-
- +
+
+ +
+
+ +
-
-
-
- + +
+
-
- -
-
- -
- -
+
From d497f3d6572b2b2a2f61c4c19a02d8c47e95063d Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Thu, 16 Jan 2025 22:00:02 +0100 Subject: [PATCH 42/57] fix sqlite migration --- .../resources/migrations/sqlite/20250115155817_ldap.up.sql | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql b/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql index 15107f5..9a6446f 100644 --- a/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql +++ b/backend/resources/migrations/sqlite/20250115155817_ldap.up.sql @@ -1,2 +1,5 @@ -ALTER TABLE users ADD COLUMN ldap_id TEXT UNIQUE; -ALTER TABLE user_groups ADD COLUMN ldap_id TEXT UNIQUE; \ No newline at end of file +ALTER TABLE users ADD COLUMN ldap_id TEXT; +ALTER TABLE user_groups ADD COLUMN ldap_id TEXT; + +CREATE UNIQUE INDEX users_ldap_id ON users (ldap_id); +CREATE UNIQUE INDEX user_groups_ldap_id ON user_groups (ldap_id); \ No newline at end of file From 59bf762b633794bbb392de59b066b41ad913f27a Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Thu, 16 Jan 2025 20:16:51 -0600 Subject: [PATCH 43/57] remove users that no longer exsist in LDAP from the pocket-id database --- backend/internal/service/ldap_service.go | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 1607658..64870ef 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -176,9 +176,22 @@ func (s *LdapService) SyncUsers() error { fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) } + //Get all Current Database Users + var databaseUsers []model.User + if err := s.db.Find(&databaseUsers).Error; err != nil { + fmt.Println(fmt.Errorf("Failed to Fetch Users from Database: %v", err)) + } + + //Create Mapping for Users that exsist + ldapUsers := make(map[*string]bool) + missingUsers := []model.User{} + for _, value := range result.Entries { ldapId := value.GetAttributeValue(uniqueIdentifierAttribute) + //This Maps the Users to this array if they exsist + ldapUsers[&ldapId] = true + // Get the user from the database var databaseUser model.User s.db.Where("ldap_id = ?", ldapId).First(&databaseUser) @@ -207,5 +220,27 @@ func (s *LdapService) SyncUsers() error { } + dbUserCount := len(databaseUsers) - 1 //Accounting for the built in Admin User + //Compare Database Users with LDAP Users + if dbUserCount > len(ldapUsers) { + for _, dbUser := range databaseUsers { + if dbUser.LdapID == nil { + continue + } + if _, exists := ldapUsers[dbUser.LdapID]; !exists { + missingUsers = append(missingUsers, dbUser) + } + } + + //Remove Users from Database if they no longer exsist in LDAP + for _, missingUser := range missingUsers { + if err := s.db.Delete(&missingUser).Error; err != nil { + log.Printf("Failed to delete user %s: %v", missingUser.Username, err) + } else { + fmt.Printf("Removed missing user: %s\n", missingUser.Username) + } + } + } + return nil } From dbc8e3822935da149570c7fbab9cf2553fcc1056 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Fri, 17 Jan 2025 10:39:35 -0600 Subject: [PATCH 44/57] remove users that no longer exsist in LDAP from the pocket-id database (fix pointer mismatch) --- backend/internal/service/ldap_service.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 64870ef..985e209 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -183,14 +183,14 @@ func (s *LdapService) SyncUsers() error { } //Create Mapping for Users that exsist - ldapUsers := make(map[*string]bool) + ldapUsers := make(map[string]bool) missingUsers := []model.User{} for _, value := range result.Entries { ldapId := value.GetAttributeValue(uniqueIdentifierAttribute) //This Maps the Users to this array if they exsist - ldapUsers[&ldapId] = true + ldapUsers[ldapId] = true // Get the user from the database var databaseUser model.User @@ -227,7 +227,8 @@ func (s *LdapService) SyncUsers() error { if dbUser.LdapID == nil { continue } - if _, exists := ldapUsers[dbUser.LdapID]; !exists { + if _, exists := ldapUsers[*dbUser.LdapID]; !exists { + fmt.Printf("Ldap id: %s, username: %s\n", *dbUser.LdapID, dbUser.Username) missingUsers = append(missingUsers, dbUser) } } From cc579c558e5b02317eb4a2bf57470040ff61c646 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Fri, 17 Jan 2025 11:44:09 -0600 Subject: [PATCH 45/57] remove ldap values from .env.example --- .env.example | 8 -------- backend/.env.example | 8 -------- 2 files changed, 16 deletions(-) diff --git a/.env.example b/.env.example index 4a0bd8c..06acff4 100644 --- a/.env.example +++ b/.env.example @@ -4,11 +4,3 @@ TRUST_PROXY=false MAXMIND_LICENSE_KEY= PUID=1000 PGID=1000 -LDAP_SERVER=ldaps://ldap.example.com -LDAP_PORT=636 -LDAP_BIND_USER=CN=user,DC=example,DC=com -LDAP_BIND_PASSWORD=securepasswordhere -LDAP_SEARCH_BASE=OU=Stuff,DC=example,DC=com -LDAP_TLS_VERIFY=false -LDAP_USERNAME_ATTRIBUTE=uid -LDAP_GROUP_ATTRIBUTE=uid diff --git a/backend/.env.example b/backend/.env.example index 41c3b1f..5185e92 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -6,11 +6,3 @@ POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket- UPLOAD_PATH=data/uploads PORT=8080 HOST=localhost -LDAP_URL=ldaps://ldap.example.com:3890 -LDAP_BIND_USER=CN=user,DC=example,DC=com -LDAP_BIND_PASSWORD=securepasswordhere -LDAP_SEARCH_BASE=OU=Stuff,DC=example,DC=com -LDAP_TLS_VERIFY=false -LDAP_USER_ID_ATTRIBUTE=uid -LDAP_USERNAME_ATTRIBUTE=uid -LDAP_GROUP_ATTRIBUTE=uid From fc0239fa20030d01939718bedfc4ea5657977550 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Fri, 17 Jan 2025 11:46:04 -0600 Subject: [PATCH 46/57] remove ldap env values from readme.md --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 5ccf554..770b782 100644 --- a/README.md +++ b/README.md @@ -173,15 +173,7 @@ docker compose up -d | `GEOLITE_DB_PATH` | `data/GeoLite2-City.mmdb` | no | The path where the GeoLite2 database should be stored. | | `CADDY_PORT` | `80` | no | The port on which Caddy should listen. Caddy is only active inside the Docker container. If you want to change the exposed port of the container then you sould change this variable. | | `PORT` | `3000` | no | The port on which the frontend should listen. | -| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen | -| `LDAP_URL` | `-` | yes | The URL of your ldap instance with the protocol and port. ie: ldaps://ldap.example.com:3890 | -| `LDAP_BIND_USER` | `-` | yes | The bind user for your ldap instance. | -| `LDAP_BIND_PASSWORD` | `-` | yes | The bind user password for your ldap instance. | -| `LDAP_SEARCH_BASE` | `-` | yes | The OU to search for all LDAP Objects | -| `LDAP_TLS_VERIFY` | `false` | yes | Choose to Verify LDAPS Certifcates or ignore them. | -| `LDAP_USERNAME_ATTRIBUTE` | `-` | yes | The LDAP Attribute to use for the username of a user. -| `LDAP_USER_ID_ATTRIBUTE` | `-` | yes | The LDAP Attribute to uniquely identify a user from LDAP -| `LDAP_GROUP_ATTRIBUTE` | `-` | yes | The LDAP Attribute to use for the Name and Claim of a Group. | +| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. | ## Account recovery From 957af5b8dc02503700acbc2a48615f8eda2d198a Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Fri, 17 Jan 2025 12:56:21 -0600 Subject: [PATCH 47/57] add admin group claim ui elements --- backend/internal/dto/app_config_dto.go | 1 + backend/internal/model/app_config.go | 1 + backend/internal/service/app_config_service.go | 13 +++++++++---- .../src/lib/types/application-configuration.ts | 1 + .../forms/app-config-ldap-form.svelte | 14 +++++++++++--- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/backend/internal/dto/app_config_dto.go b/backend/internal/dto/app_config_dto.go index c2f8002..6a66a61 100644 --- a/backend/internal/dto/app_config_dto.go +++ b/backend/internal/dto/app_config_dto.go @@ -37,4 +37,5 @@ type AppConfigUpdateDto struct { LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"` LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"` LdapAttributeGroupName string `json:"ldapAttributeGroupName"` + LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"` } diff --git a/backend/internal/model/app_config.go b/backend/internal/model/app_config.go index 6052ae8..59475bd 100644 --- a/backend/internal/model/app_config.go +++ b/backend/internal/model/app_config.go @@ -42,4 +42,5 @@ type AppConfig struct { LdapAttributeUserLastName AppConfigVariable LdapAttributeGroupUniqueIdentifier AppConfigVariable LdapAttributeGroupName AppConfigVariable + LdapAttributeAdminGroup AppConfigVariable } diff --git a/backend/internal/service/app_config_service.go b/backend/internal/service/app_config_service.go index 8bf26ea..dd33e16 100644 --- a/backend/internal/service/app_config_service.go +++ b/backend/internal/service/app_config_service.go @@ -2,15 +2,16 @@ package service import ( "fmt" + "log" + "mime/multipart" + "os" + "reflect" + "github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/dto" "github.com/stonith404/pocket-id/backend/internal/model" "github.com/stonith404/pocket-id/backend/internal/utils" "gorm.io/gorm" - "log" - "mime/multipart" - "os" - "reflect" ) type AppConfigService struct { @@ -163,6 +164,10 @@ var defaultDbConfig = model.AppConfig{ Key: "ldapAttributeGroupName", Type: "string", }, + LdapAttributeAdminGroup: model.AppConfigVariable{ + Key: "ldapAttributeAdminGroup", + Type: "string", + }, } func (s *AppConfigService) UpdateAppConfig(input dto.AppConfigUpdateDto) ([]model.AppConfigVariable, error) { diff --git a/frontend/src/lib/types/application-configuration.ts b/frontend/src/lib/types/application-configuration.ts index b9f8a97..28475fd 100644 --- a/frontend/src/lib/types/application-configuration.ts +++ b/frontend/src/lib/types/application-configuration.ts @@ -30,6 +30,7 @@ export type AllAppConfig = AppConfig & { ldapAttributeUserLastName: string; ldapAttributeGroupUniqueIdentifier: string; ldapAttributeGroupName: string; + ldapAttributeAdminGroup: string; }; export type AppConfigRawResponse = { diff --git a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte index a9e4876..ceb4a2a 100644 --- a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte @@ -35,7 +35,8 @@ ldapAttributeUserFirstName: appConfig.ldapAttributeUserFirstName, ldapAttributeUserLastName: appConfig.ldapAttributeUserLastName, ldapAttributeGroupUniqueIdentifier: appConfig.ldapAttributeGroupUniqueIdentifier, - ldapAttributeGroupName: appConfig.ldapAttributeGroupName + ldapAttributeGroupName: appConfig.ldapAttributeGroupName, + ldapAttributeAdminGroup: appConfig.ldapAttributeAdminGroup }; const formSchema = z.object({ @@ -50,7 +51,8 @@ ldapAttributeUserFirstName: z.string().min(1), ldapAttributeUserLastName: z.string().min(1), ldapAttributeGroupUniqueIdentifier: z.string().min(1), - ldapAttributeGroupName: z.string().min(1) + ldapAttributeGroupName: z.string().min(1), + ldapAttributeAdminGroup: z.string().min(1) }); const { inputs, ...form } = createForm(formSchema, updatedAppConfig); @@ -85,7 +87,7 @@ .catch(axiosErrorToast); ldapSyncing = false; - + } @@ -147,6 +149,12 @@ placeholder="cn" bind:input={$inputs.ldapAttributeGroupName} /> +
From 3443d4a7112079c9b3d7a06da9a77acd65fba02e Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Fri, 17 Jan 2025 13:16:59 -0600 Subject: [PATCH 48/57] add admin group claim backend logic --- backend/internal/service/ldap_service.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 985e209..b5913a4 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -148,12 +148,15 @@ func (s *LdapService) SyncUsers() error { } defer client.Close() + var adminStatus bool + baseDN := s.appConfigService.DbConfig.LdapBase.Value uniqueIdentifierAttribute := s.appConfigService.DbConfig.LdapAttributeUserUniqueIdentifier.Value usernameAttribute := s.appConfigService.DbConfig.LdapAttributeUserUsername.Value emailAttribute := s.appConfigService.DbConfig.LdapAttributeUserEmail.Value firstNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserFirstName.Value lastNameAttribute := s.appConfigService.DbConfig.LdapAttributeUserLastName.Value + adminGroupAttribute := s.appConfigService.DbConfig.LdapAttributeAdminGroup.Value filter := "(objectClass=person)" @@ -196,12 +199,18 @@ func (s *LdapService) SyncUsers() error { var databaseUser model.User s.db.Where("ldap_id = ?", ldapId).First(&databaseUser) + for _, group := range value.GetAttributeValues("memberOf") { + if strings.Contains(group, adminGroupAttribute) { + adminStatus = true + } + } + newUser := dto.UserCreateDto{ Username: value.GetAttributeValue(usernameAttribute), Email: value.GetAttributeValue(emailAttribute), FirstName: value.GetAttributeValue(firstNameAttribute), LastName: value.GetAttributeValue(lastNameAttribute), - IsAdmin: false, + IsAdmin: adminStatus, LdapID: ldapId, } From 04222f77ab2c6799f8de563370c78298e3adece0 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Fri, 17 Jan 2025 13:46:56 -0600 Subject: [PATCH 49/57] ldap users can not be deleted from the UI --- frontend/src/routes/settings/admin/users/user-list.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/routes/settings/admin/users/user-list.svelte b/frontend/src/routes/settings/admin/users/user-list.svelte index aaad2e4..7e619c7 100644 --- a/frontend/src/routes/settings/admin/users/user-list.svelte +++ b/frontend/src/routes/settings/admin/users/user-list.svelte @@ -96,6 +96,7 @@ > Edit deleteUser(item)} >Delete Date: Fri, 17 Jan 2025 13:50:56 -0600 Subject: [PATCH 50/57] ldap users can not be deleted from the UI --- frontend/src/routes/settings/admin/users/user-list.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/routes/settings/admin/users/user-list.svelte b/frontend/src/routes/settings/admin/users/user-list.svelte index 7e619c7..aaad2e4 100644 --- a/frontend/src/routes/settings/admin/users/user-list.svelte +++ b/frontend/src/routes/settings/admin/users/user-list.svelte @@ -96,7 +96,6 @@ > Edit deleteUser(item)} >Delete Date: Fri, 17 Jan 2025 13:51:50 -0600 Subject: [PATCH 51/57] Reverted ldap users can not be deleted from the UI --- frontend/src/routes/settings/admin/users/user-list.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/routes/settings/admin/users/user-list.svelte b/frontend/src/routes/settings/admin/users/user-list.svelte index aaad2e4..6985624 100644 --- a/frontend/src/routes/settings/admin/users/user-list.svelte +++ b/frontend/src/routes/settings/admin/users/user-list.svelte @@ -96,6 +96,7 @@ > Edit deleteUser(item)} >Delete Date: Fri, 17 Jan 2025 16:47:14 -0600 Subject: [PATCH 52/57] add postgresql migration for ldap sync --- .zed/settings.json | 0 .../migrations/postgres/20250117164229_ldap.down.sql | 5 +++++ .../resources/migrations/postgres/20250117164229_ldap.up.sql | 5 +++++ 3 files changed, 10 insertions(+) create mode 100644 .zed/settings.json create mode 100644 backend/resources/migrations/postgres/20250117164229_ldap.down.sql create mode 100644 backend/resources/migrations/postgres/20250117164229_ldap.up.sql diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/backend/resources/migrations/postgres/20250117164229_ldap.down.sql b/backend/resources/migrations/postgres/20250117164229_ldap.down.sql new file mode 100644 index 0000000..facc73f --- /dev/null +++ b/backend/resources/migrations/postgres/20250117164229_ldap.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE users +DROP COLUMN ldap_id; + +ALTER TABLE user_groups +DROP COLUMN ldap_id; diff --git a/backend/resources/migrations/postgres/20250117164229_ldap.up.sql b/backend/resources/migrations/postgres/20250117164229_ldap.up.sql new file mode 100644 index 0000000..c763ff5 --- /dev/null +++ b/backend/resources/migrations/postgres/20250117164229_ldap.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE users ADD COLUMN ldap_id TEXT; +ALTER TABLE user_groups ADD COLUMN ldap_id TEXT; + +CREATE UNIQUE INDEX users_ldap_id ON users (ldap_id); +CREATE UNIQUE INDEX user_groups_ldap_id ON user_groups (ldap_id); From fafe88fbf2206c6b8a79d0a0fb6e194b4e2f6dc4 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Fri, 17 Jan 2025 16:47:40 -0600 Subject: [PATCH 53/57] remove .zed --- .zed/settings.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .zed/settings.json diff --git a/.zed/settings.json b/.zed/settings.json deleted file mode 100644 index e69de29..0000000 From ba88754ab8a4862f6c96a09038cc865487e89d41 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sat, 18 Jan 2025 21:57:06 +0100 Subject: [PATCH 54/57] clean up ldap service --- backend/internal/service/ldap_service.go | 93 +++++++++++++----------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index b5913a4..a0a1e0b 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -27,7 +27,7 @@ func (s *LdapService) createClient() (*ldap.Conn, error) { if s.appConfigService.DbConfig.LdapEnabled.Value != "true" { return nil, fmt.Errorf("LDAP is not enabled") } - // Setup AD Connection + // Setup LDAP connection ldapURL := s.appConfigService.DbConfig.LdapUrl.Value skipTLSVerify := s.appConfigService.DbConfig.LdapSkipCertVerify.Value == "true" client, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: skipTLSVerify})) @@ -35,7 +35,7 @@ func (s *LdapService) createClient() (*ldap.Conn, error) { return nil, fmt.Errorf("failed to connect to LDAP: %w", err) } - // Bind as Service Account + // Bind as service account bindDn := s.appConfigService.DbConfig.LdapBindDn.Value bindPassword := s.appConfigService.DbConfig.LdapBindPassword.Value err = client.Bind(bindDn, bindPassword) @@ -60,7 +60,7 @@ func (s *LdapService) SyncAll() error { } func (s *LdapService) SyncGroups() error { - // Setup LDAP Connection + // Setup LDAP connection client, err := s.createClient() if err != nil { return fmt.Errorf("failed to create LDAP client: %w", err) @@ -84,16 +84,21 @@ func (s *LdapService) SyncGroups() error { return fmt.Errorf("failed to query LDAP: %w", err) } + // Create a mapping for groups that exist + ldapGroupIDs := make(map[string]bool) + for _, value := range result.Entries { var usersToAddDto dto.UserGroupUpdateUsersDto - var userIDStrings []string + var membersUserId []string - // Try to find the group in the database ldapId := value.GetAttributeValue(uniqueIdentifierAttribute) + ldapGroupIDs[ldapId] = true + + // Try to find the group in the database var databaseGroup model.UserGroup s.db.Where("ldap_id = ?", ldapId).First(&databaseGroup) - //Get Group Members and add to the correct Group + // Get group members and add to the correct Group groupMembers := value.GetAttributeValues("member") for _, member := range groupMembers { // Normal output of this would be CN=username,ou=people,dc=example,dc=com @@ -102,7 +107,7 @@ func (s *LdapService) SyncGroups() error { var databaseUser model.User s.db.Where("username = ?", singleMember).First(&databaseUser) - userIDStrings = append(userIDStrings, databaseUser.ID) + membersUserId = append(membersUserId, databaseUser.ID) } syncGroup := dto.UserGroupCreateDto{ @@ -112,7 +117,7 @@ func (s *LdapService) SyncGroups() error { } usersToAddDto = dto.UserGroupUpdateUsersDto{ - UserIDs: userIDStrings, + UserIDs: membersUserId, } if databaseGroup.ID == "" { @@ -136,20 +141,34 @@ func (s *LdapService) SyncGroups() error { } - return nil + // Get all LDAP groups from the database + var ldapGroupsInDb []model.UserGroup + if err := s.db.Find(&ldapGroupsInDb, "ldap_id IS NOT NULL").Select("ldap_id").Error; err != nil { + fmt.Println(fmt.Errorf("failed to fetch groups from database: %v", err)) + } + + // Delete groups that no longer exist in LDAP + for _, group := range ldapGroupsInDb { + if _, exists := ldapGroupIDs[*group.LdapID]; !exists { + if err := s.db.Delete(&model.UserGroup{}, "ldap_id = ?", group.LdapID).Error; err != nil { + log.Printf("Failed to delete group %s with: %v", group.Name, err) + } else { + log.Printf("Deleted group %s", group.Name) + } + } + } + return nil } func (s *LdapService) SyncUsers() error { - // Setup LDAP Connection + // Setup LDAP connection client, err := s.createClient() if err != nil { return fmt.Errorf("failed to create LDAP client: %w", err) } defer client.Close() - var adminStatus bool - baseDN := s.appConfigService.DbConfig.LdapBase.Value uniqueIdentifierAttribute := s.appConfigService.DbConfig.LdapAttributeUserUniqueIdentifier.Value usernameAttribute := s.appConfigService.DbConfig.LdapAttributeUserUsername.Value @@ -179,29 +198,22 @@ func (s *LdapService) SyncUsers() error { fmt.Println(fmt.Errorf("failed to query LDAP: %w", err)) } - //Get all Current Database Users - var databaseUsers []model.User - if err := s.db.Find(&databaseUsers).Error; err != nil { - fmt.Println(fmt.Errorf("Failed to Fetch Users from Database: %v", err)) - } - - //Create Mapping for Users that exsist - ldapUsers := make(map[string]bool) - missingUsers := []model.User{} + // Create a mapping for users that exist + ldapUserIDs := make(map[string]bool) for _, value := range result.Entries { ldapId := value.GetAttributeValue(uniqueIdentifierAttribute) - - //This Maps the Users to this array if they exsist - ldapUsers[ldapId] = true + ldapUserIDs[ldapId] = true // Get the user from the database var databaseUser model.User s.db.Where("ldap_id = ?", ldapId).First(&databaseUser) + // Check if user is admin by checking if they are in the admin group + isAdmin := false for _, group := range value.GetAttributeValues("memberOf") { if strings.Contains(group, adminGroupAttribute) { - adminStatus = true + isAdmin = true } } @@ -210,7 +222,7 @@ func (s *LdapService) SyncUsers() error { Email: value.GetAttributeValue(emailAttribute), FirstName: value.GetAttributeValue(firstNameAttribute), LastName: value.GetAttributeValue(lastNameAttribute), - IsAdmin: adminStatus, + IsAdmin: isAdmin, LdapID: ldapId, } @@ -229,28 +241,21 @@ func (s *LdapService) SyncUsers() error { } - dbUserCount := len(databaseUsers) - 1 //Accounting for the built in Admin User - //Compare Database Users with LDAP Users - if dbUserCount > len(ldapUsers) { - for _, dbUser := range databaseUsers { - if dbUser.LdapID == nil { - continue - } - if _, exists := ldapUsers[*dbUser.LdapID]; !exists { - fmt.Printf("Ldap id: %s, username: %s\n", *dbUser.LdapID, dbUser.Username) - missingUsers = append(missingUsers, dbUser) - } - } + // Get all LDAP users from the database + var ldapUsersInDb []model.User + if err := s.db.Find(&ldapUsersInDb, "ldap_id IS NOT NULL").Select("ldap_id").Error; err != nil { + fmt.Println(fmt.Errorf("failed to fetch users from database: %v", err)) + } - //Remove Users from Database if they no longer exsist in LDAP - for _, missingUser := range missingUsers { - if err := s.db.Delete(&missingUser).Error; err != nil { - log.Printf("Failed to delete user %s: %v", missingUser.Username, err) + // Delete users that no longer exist in LDAP + for _, user := range ldapUsersInDb { + if _, exists := ldapUserIDs[*user.LdapID]; !exists { + if err := s.db.Delete(&model.User{}, "ldap_id = ?", user.LdapID).Error; err != nil { + log.Printf("Failed to delete user %s with: %v", user.Username, err) } else { - fmt.Printf("Removed missing user: %s\n", missingUser.Username) + log.Printf("Deleted user %s", user.Username) } } } - return nil } From 4d72018fe58a8fe6d1fd925d0de131a6aa2c2c88 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sat, 18 Jan 2025 21:58:06 +0100 Subject: [PATCH 55/57] improve disabled ldap state in UI --- .../src/lib/components/advanced-table.svelte | 5 +- .../forms/app-config-ldap-form.svelte | 4 +- .../admin/user-groups/[id]/+page.svelte | 17 +++++-- .../admin/user-groups/user-group-form.svelte | 47 +++++++++---------- .../admin/user-groups/user-selection.svelte | 6 ++- .../settings/admin/users/user-form.svelte | 34 +++++--------- 6 files changed, 57 insertions(+), 56 deletions(-) diff --git a/frontend/src/lib/components/advanced-table.svelte b/frontend/src/lib/components/advanced-table.svelte index c7ef605..6539445 100644 --- a/frontend/src/lib/components/advanced-table.svelte +++ b/frontend/src/lib/components/advanced-table.svelte @@ -17,6 +17,7 @@ requestOptions = $bindable(), selectedIds = $bindable(), withoutSearch = false, + selectionDisabled = false, defaultSort, onRefresh, columns, @@ -26,6 +27,7 @@ requestOptions?: SearchPaginationSortRequest; selectedIds?: string[]; withoutSearch?: boolean; + selectionDisabled?: boolean; defaultSort?: { column: string; direction: 'asc' | 'desc' }; onRefresh: (requestOptions: SearchPaginationSortRequest) => Promise>; columns: { label: string; hidden?: boolean; sortColumn?: string }[]; @@ -116,7 +118,7 @@ {#if selectedIds} - onAllCheck(c as boolean)} /> + onAllCheck(c as boolean)} /> {/if} {#each columns as column} @@ -154,6 +156,7 @@ {#if selectedIds} onCheck(c as boolean, item.id)} /> diff --git a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte index ceb4a2a..3535047 100644 --- a/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte +++ b/frontend/src/routes/settings/admin/application-configuration/forms/app-config-ldap-form.svelte @@ -52,7 +52,7 @@ ldapAttributeUserLastName: z.string().min(1), ldapAttributeGroupUniqueIdentifier: z.string().min(1), ldapAttributeGroupName: z.string().min(1), - ldapAttributeAdminGroup: z.string().min(1) + ldapAttributeAdminGroup: z.string() }); const { inputs, ...form } = createForm(formSchema, updatedAppConfig); @@ -151,7 +151,7 @@ /> diff --git a/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte b/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte index a84b6d6..e36bec9 100644 --- a/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte +++ b/frontend/src/routes/settings/admin/user-groups/[id]/+page.svelte @@ -1,5 +1,6 @@
-
-
-
- +
+
+
+ +
+
+ +
-
- +
+
-
-
- -
-
+
diff --git a/frontend/src/routes/settings/admin/user-groups/user-selection.svelte b/frontend/src/routes/settings/admin/user-groups/user-selection.svelte index c5a0fab..fb9fdae 100644 --- a/frontend/src/routes/settings/admin/user-groups/user-selection.svelte +++ b/frontend/src/routes/settings/admin/user-groups/user-selection.svelte @@ -7,8 +7,11 @@ let { users: initialUsers, + selectionDisabled = false, selectedUserIds = $bindable() - }: { users: Paginated; selectedUserIds: string[] } = $props(); + }: { users: Paginated; + selectionDisabled?: boolean; + selectedUserIds: string[] } = $props(); const userService = new UserService(); @@ -23,6 +26,7 @@ { label: 'Email', sortColumn: 'email' } ]} bind:selectedIds={selectedUserIds} + {selectionDisabled} > {#snippet rows({ item })} {item.firstName} {item.lastName} diff --git a/frontend/src/routes/settings/admin/users/user-form.svelte b/frontend/src/routes/settings/admin/users/user-form.svelte index 5e538a9..604c780 100644 --- a/frontend/src/routes/settings/admin/users/user-form.svelte +++ b/frontend/src/routes/settings/admin/users/user-form.svelte @@ -54,29 +54,19 @@
-
-
-
- -
-
- -
+
+
+ + + + +
-
-
- -
-
- -
-
-
From 1871ad21c5091081679c476cf0db0785a447b2b1 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sat, 18 Jan 2025 22:47:56 +0100 Subject: [PATCH 56/57] fix existing tests --- frontend/tests/application-configuration.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/tests/application-configuration.spec.ts b/frontend/tests/application-configuration.spec.ts index 270013d..4dd929d 100644 --- a/frontend/tests/application-configuration.spec.ts +++ b/frontend/tests/application-configuration.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(cleanupBackend); test('Update general configuration', async ({ page }) => { await page.goto('/settings/admin/application-configuration'); - await page.getByLabel('Name').fill('Updated Name'); + await page.getByLabel('Application Name', { exact: true }).fill('Updated Name'); await page.getByLabel('Session Duration').fill('30'); await page.getByRole('button', { name: 'Save' }).first().click(); @@ -17,7 +17,7 @@ test('Update general configuration', async ({ page }) => { await page.reload(); - await expect(page.getByLabel('Name')).toHaveValue('Updated Name'); + await expect(page.getByLabel('Application Name', { exact: true })).toHaveValue('Updated Name'); await expect(page.getByLabel('Session Duration')).toHaveValue('30'); }); @@ -29,7 +29,7 @@ test('Update email configuration', async ({ page }) => { await page.getByLabel('SMTP User').fill('test@gmail.com'); await page.getByLabel('SMTP Password').fill('password'); await page.getByLabel('SMTP From').fill('test@gmail.com'); - await page.getByRole('button', { name: 'Enable' }).click(); + await page.getByRole('button', { name: 'Enable' }).nth(0).click(); await page.getByRole('status').click(); await expect(page.getByRole('status')).toHaveText('Email configuration updated successfully'); From 03dcd0d375e7ec662beeaecad0dea088f274dae1 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sat, 18 Jan 2025 23:16:03 +0100 Subject: [PATCH 57/57] disable deletion of user and user group if LDAP enabled --- backend/internal/common/errors.go | 7 +++++++ backend/internal/service/user_group_service.go | 6 +++++- backend/internal/service/user_service.go | 4 ++++ .../admin/user-groups/user-group-list.svelte | 12 +++++++----- .../routes/settings/admin/users/user-list.svelte | 13 +++++++------ 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/backend/internal/common/errors.go b/backend/internal/common/errors.go index eb756de..84393d6 100644 --- a/backend/internal/common/errors.go +++ b/backend/internal/common/errors.go @@ -169,3 +169,10 @@ func (e *LdapUserUpdateError) Error() string { return "LDAP users can't be updated" } func (e *LdapUserUpdateError) HttpStatusCode() int { return http.StatusForbidden } + +type LdapUserGroupUpdateError struct{} + +func (e *LdapUserGroupUpdateError) Error() string { + return "LDAP user groups can't be updated" +} +func (e *LdapUserGroupUpdateError) HttpStatusCode() int { return http.StatusForbidden } diff --git a/backend/internal/service/user_group_service.go b/backend/internal/service/user_group_service.go index 489760d..bc3d966 100644 --- a/backend/internal/service/user_group_service.go +++ b/backend/internal/service/user_group_service.go @@ -51,6 +51,10 @@ func (s *UserGroupService) Delete(id string) error { return err } + if group.LdapID != nil { + return &common.LdapUserGroupUpdateError{} + } + return s.db.Delete(&group).Error } @@ -77,7 +81,7 @@ func (s *UserGroupService) Update(id string, input dto.UserGroupCreateDto, allow } if group.LdapID != nil && !allowLdapUpdate { - return model.UserGroup{}, &common.LdapUserUpdateError{} + return model.UserGroup{}, &common.LdapUserGroupUpdateError{} } group.Name = input.Name diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index 4b7c3be..0fe2709 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -46,6 +46,10 @@ func (s *UserService) DeleteUser(userID string) error { return err } + if user.LdapID != nil { + return &common.LdapUserUpdateError{} + } + return s.db.Delete(&user).Error } diff --git a/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte b/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte index a342a04..91392f6 100644 --- a/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte +++ b/frontend/src/routes/settings/admin/user-groups/user-group-list.svelte @@ -68,11 +68,13 @@ Edit - deleteUserGroup(item)} - >Delete + {#if !item.ldapId} + deleteUserGroup(item)} + >Delete + {/if} diff --git a/frontend/src/routes/settings/admin/users/user-list.svelte b/frontend/src/routes/settings/admin/users/user-list.svelte index 6985624..89ab1ad 100644 --- a/frontend/src/routes/settings/admin/users/user-list.svelte +++ b/frontend/src/routes/settings/admin/users/user-list.svelte @@ -95,12 +95,13 @@ goto(`/settings/admin/users/${item.id}`)} > Edit - deleteUser(item)} - >Delete + {#if !item.ldapId} + deleteUser(item)} + >Delete + {/if}