mirror of
https://github.com/netbirdio/netbird.git
synced 2026-03-31 06:34:14 -04:00
Add DEX IdP Support (#4949)
This commit is contained in:
17
go.mod
17
go.mod
@@ -22,7 +22,7 @@ require (
|
||||
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||
google.golang.org/grpc v1.73.0
|
||||
google.golang.org/grpc v1.75.0
|
||||
google.golang.org/protobuf v1.36.8
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
@@ -41,6 +41,7 @@ require (
|
||||
github.com/coder/websocket v1.8.13
|
||||
github.com/coreos/go-iptables v0.7.0
|
||||
github.com/creack/pty v1.1.18
|
||||
github.com/dexidp/dex/api/v2 v2.4.0
|
||||
github.com/eko/gocache/lib/v4 v4.2.0
|
||||
github.com/eko/gocache/store/go_cache/v4 v4.2.2
|
||||
github.com/eko/gocache/store/redis/v4 v4.2.2
|
||||
@@ -97,10 +98,10 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4
|
||||
github.com/zcalusic/sysinfo v1.1.3
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel v1.37.0
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.48.0
|
||||
go.opentelemetry.io/otel/metric v1.35.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0
|
||||
go.opentelemetry.io/otel/metric v1.37.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0
|
||||
go.uber.org/mock v0.5.0
|
||||
go.uber.org/zap v1.27.0
|
||||
goauthentik.io/api/v3 v3.2023051.3
|
||||
@@ -124,7 +125,7 @@ require (
|
||||
require (
|
||||
cloud.google.com/go/auth v0.3.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
@@ -170,7 +171,7 @@ require (
|
||||
github.com/fyne-io/oksvg v0.2.0 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
@@ -248,8 +249,8 @@ require (
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/image v0.33.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
|
||||
40
go.sum
40
go.sum
@@ -4,8 +4,8 @@ cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
|
||||
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
cunicu.li/go-rosenpass v0.4.0 h1:LtPtBgFWY/9emfgC4glKLEqS0MJTylzV6+ChRhiZERw=
|
||||
cunicu.li/go-rosenpass v0.4.0/go.mod h1:MPbjH9nxV4l3vEagKVdFNwHOketqgS5/To1VYJplf/M=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
@@ -117,6 +117,8 @@ github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6/go.mod h1:+CauBF6R70J
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dexidp/dex/api/v2 v2.4.0 h1:gNba7n6BKVp8X4Jp24cxYn5rIIGhM6kDOXcZoL6tr9A=
|
||||
github.com/dexidp/dex/api/v2 v2.4.0/go.mod h1:/p550ADvFFh7K95VmhUD+jgm15VdaNnab9td8DHOpyI=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
@@ -164,8 +166,8 @@ github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVin
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
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/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
@@ -561,22 +563,22 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.48.0 h1:sBQe3VNGUjY9IKWQC6z2lNqa5iGbDSxhs60ABwK4y0s=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.48.0/go.mod h1:DtrbMzoZWwQHyrQmCfLam5DZbnmorsGbOtTbYHycU5o=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -761,6 +763,8 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk=
|
||||
google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
@@ -770,8 +774,8 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -779,8 +783,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
||||
554
infrastructure_files/getting-started-with-dex.sh
Executable file
554
infrastructure_files/getting-started-with-dex.sh
Executable file
@@ -0,0 +1,554 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# NetBird Getting Started with Dex IDP
|
||||
# This script sets up NetBird with Dex as the identity provider
|
||||
|
||||
# Sed pattern to strip base64 padding characters
|
||||
SED_STRIP_PADDING='s/=//g'
|
||||
|
||||
check_docker_compose() {
|
||||
if command -v docker-compose &> /dev/null
|
||||
then
|
||||
echo "docker-compose"
|
||||
return
|
||||
fi
|
||||
if docker compose --help &> /dev/null
|
||||
then
|
||||
echo "docker compose"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "docker-compose is not installed or not in PATH. Please follow the steps from the official guide: https://docs.docker.com/engine/install/" > /dev/stderr
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_jq() {
|
||||
if ! command -v jq &> /dev/null
|
||||
then
|
||||
echo "jq is not installed or not in PATH, please install with your package manager. e.g. sudo apt install jq" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
get_main_ip_address() {
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
interface=$(route -n get default | grep 'interface:' | awk '{print $2}')
|
||||
ip_address=$(ifconfig "$interface" | grep 'inet ' | awk '{print $2}')
|
||||
else
|
||||
interface=$(ip route | grep default | awk '{print $5}' | head -n 1)
|
||||
ip_address=$(ip addr show "$interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1)
|
||||
fi
|
||||
|
||||
echo "$ip_address"
|
||||
return 0
|
||||
}
|
||||
|
||||
check_nb_domain() {
|
||||
DOMAIN=$1
|
||||
if [[ "$DOMAIN-x" == "-x" ]]; then
|
||||
echo "The NETBIRD_DOMAIN variable cannot be empty." > /dev/stderr
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$DOMAIN" == "netbird.example.com" ]]; then
|
||||
echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
read_nb_domain() {
|
||||
READ_NETBIRD_DOMAIN=""
|
||||
echo -n "Enter the domain you want to use for NetBird (e.g. netbird.my-domain.com): " > /dev/stderr
|
||||
read -r READ_NETBIRD_DOMAIN < /dev/tty
|
||||
if ! check_nb_domain "$READ_NETBIRD_DOMAIN"; then
|
||||
read_nb_domain
|
||||
fi
|
||||
echo "$READ_NETBIRD_DOMAIN"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_turn_external_ip() {
|
||||
TURN_EXTERNAL_IP_CONFIG="#external-ip="
|
||||
IP=$(curl -s -4 https://jsonip.com | jq -r '.ip')
|
||||
if [[ "x-$IP" != "x-" ]]; then
|
||||
TURN_EXTERNAL_IP_CONFIG="external-ip=$IP"
|
||||
fi
|
||||
echo "$TURN_EXTERNAL_IP_CONFIG"
|
||||
return 0
|
||||
}
|
||||
|
||||
wait_dex() {
|
||||
set +e
|
||||
echo -n "Waiting for Dex to become ready (via $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN)"
|
||||
counter=1
|
||||
while true; do
|
||||
# Check Dex through Caddy proxy (also validates TLS is working)
|
||||
if curl -sk -f -o /dev/null "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/dex/.well-known/openid-configuration" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
if [[ $counter -eq 60 ]]; then
|
||||
echo ""
|
||||
echo "Taking too long. Checking logs..."
|
||||
$DOCKER_COMPOSE_COMMAND logs --tail=20 caddy
|
||||
$DOCKER_COMPOSE_COMMAND logs --tail=20 dex
|
||||
fi
|
||||
echo -n " ."
|
||||
sleep 2
|
||||
counter=$((counter + 1))
|
||||
done
|
||||
echo " done"
|
||||
set -e
|
||||
return 0
|
||||
}
|
||||
|
||||
init_environment() {
|
||||
CADDY_SECURE_DOMAIN=""
|
||||
NETBIRD_PORT=80
|
||||
NETBIRD_HTTP_PROTOCOL="http"
|
||||
NETBIRD_RELAY_PROTO="rel"
|
||||
TURN_USER="self"
|
||||
TURN_PASSWORD=$(openssl rand -base64 32 | sed "$SED_STRIP_PADDING")
|
||||
NETBIRD_RELAY_AUTH_SECRET=$(openssl rand -base64 32 | sed "$SED_STRIP_PADDING")
|
||||
TURN_MIN_PORT=49152
|
||||
TURN_MAX_PORT=65535
|
||||
TURN_EXTERNAL_IP_CONFIG=$(get_turn_external_ip)
|
||||
|
||||
# Generate secrets for Dex
|
||||
DEX_DASHBOARD_CLIENT_SECRET=$(openssl rand -base64 32 | sed "$SED_STRIP_PADDING")
|
||||
|
||||
# Generate admin password
|
||||
NETBIRD_ADMIN_PASSWORD=$(openssl rand -base64 16 | sed "$SED_STRIP_PADDING")
|
||||
|
||||
if ! check_nb_domain "$NETBIRD_DOMAIN"; then
|
||||
NETBIRD_DOMAIN=$(read_nb_domain)
|
||||
fi
|
||||
|
||||
if [[ "$NETBIRD_DOMAIN" == "use-ip" ]]; then
|
||||
NETBIRD_DOMAIN=$(get_main_ip_address)
|
||||
else
|
||||
NETBIRD_PORT=443
|
||||
CADDY_SECURE_DOMAIN=", $NETBIRD_DOMAIN:$NETBIRD_PORT"
|
||||
NETBIRD_HTTP_PROTOCOL="https"
|
||||
NETBIRD_RELAY_PROTO="rels"
|
||||
fi
|
||||
|
||||
check_jq
|
||||
|
||||
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
|
||||
|
||||
if [[ -f dex.yaml ]]; then
|
||||
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
|
||||
echo "You can use the following commands:"
|
||||
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
|
||||
echo " rm -f docker-compose.yml Caddyfile dex.yaml dashboard.env turnserver.conf management.json relay.env"
|
||||
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo Rendering initial files...
|
||||
render_docker_compose > docker-compose.yml
|
||||
render_caddyfile > Caddyfile
|
||||
render_dex_config > dex.yaml
|
||||
render_dashboard_env > dashboard.env
|
||||
render_management_json > management.json
|
||||
render_turn_server_conf > turnserver.conf
|
||||
render_relay_env > relay.env
|
||||
|
||||
echo -e "\nStarting Dex IDP\n"
|
||||
$DOCKER_COMPOSE_COMMAND up -d caddy dex
|
||||
|
||||
# Wait for Dex to be ready (through caddy proxy)
|
||||
sleep 3
|
||||
wait_dex
|
||||
|
||||
echo -e "\nStarting NetBird services\n"
|
||||
$DOCKER_COMPOSE_COMMAND up -d
|
||||
|
||||
echo -e "\nDone!\n"
|
||||
echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN"
|
||||
echo ""
|
||||
echo "Login with the following credentials:"
|
||||
echo "Email: admin@$NETBIRD_DOMAIN" | tee .env
|
||||
echo "Password: $NETBIRD_ADMIN_PASSWORD" | tee -a .env
|
||||
echo ""
|
||||
echo "Dex admin UI is not available (Dex has no built-in UI)."
|
||||
echo "To add more users, edit dex.yaml and restart: $DOCKER_COMPOSE_COMMAND restart dex"
|
||||
return 0
|
||||
}
|
||||
|
||||
render_caddyfile() {
|
||||
cat <<EOF
|
||||
{
|
||||
debug
|
||||
servers :80,:443 {
|
||||
protocols h1 h2c h2 h3
|
||||
}
|
||||
}
|
||||
|
||||
(security_headers) {
|
||||
header * {
|
||||
Strict-Transport-Security "max-age=3600; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
-Server
|
||||
Referrer-Policy strict-origin-when-cross-origin
|
||||
}
|
||||
}
|
||||
|
||||
:80${CADDY_SECURE_DOMAIN} {
|
||||
import security_headers
|
||||
# Relay
|
||||
reverse_proxy /relay* relay:80
|
||||
# Signal
|
||||
reverse_proxy /signalexchange.SignalExchange/* h2c://signal:10000
|
||||
# Management
|
||||
reverse_proxy /api/* management:80
|
||||
reverse_proxy /management.ManagementService/* h2c://management:80
|
||||
# Dex
|
||||
reverse_proxy /dex/* dex:5556
|
||||
# Dashboard
|
||||
reverse_proxy /* dashboard:80
|
||||
}
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
render_dex_config() {
|
||||
# Generate bcrypt hash of the admin password
|
||||
# Using a simple approach - htpasswd or python if available
|
||||
ADMIN_PASSWORD_HASH=""
|
||||
if command -v htpasswd &> /dev/null; then
|
||||
ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" "$NETBIRD_ADMIN_PASSWORD" | tr -d ':\n')
|
||||
elif command -v python3 &> /dev/null; then
|
||||
ADMIN_PASSWORD_HASH=$(python3 -c "import bcrypt; print(bcrypt.hashpw('$NETBIRD_ADMIN_PASSWORD'.encode(), bcrypt.gensalt(rounds=10)).decode())" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
# Fallback to a known hash if we can't generate one
|
||||
if [[ -z "$ADMIN_PASSWORD_HASH" ]]; then
|
||||
# This is hash of "password" - user should change it
|
||||
ADMIN_PASSWORD_HASH='$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W'
|
||||
NETBIRD_ADMIN_PASSWORD="password"
|
||||
echo "Warning: Could not generate password hash. Using default password: password. Please change it in dex.yaml" > /dev/stderr
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
# Dex configuration for NetBird
|
||||
# Generated by getting-started-with-dex.sh
|
||||
|
||||
issuer: $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/dex
|
||||
|
||||
storage:
|
||||
type: sqlite3
|
||||
config:
|
||||
file: /var/dex/dex.db
|
||||
|
||||
web:
|
||||
http: 0.0.0.0:5556
|
||||
|
||||
# gRPC API for user management (used by NetBird IDP manager)
|
||||
grpc:
|
||||
addr: 0.0.0.0:5557
|
||||
|
||||
oauth2:
|
||||
skipApprovalScreen: true
|
||||
|
||||
# Static OAuth2 clients for NetBird
|
||||
staticClients:
|
||||
# Dashboard client
|
||||
- id: netbird-dashboard
|
||||
name: NetBird Dashboard
|
||||
secret: $DEX_DASHBOARD_CLIENT_SECRET
|
||||
redirectURIs:
|
||||
- $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/nb-auth
|
||||
- $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/nb-silent-auth
|
||||
|
||||
# CLI client (public - uses PKCE)
|
||||
- id: netbird-cli
|
||||
name: NetBird CLI
|
||||
public: true
|
||||
redirectURIs:
|
||||
- http://localhost:53000/
|
||||
- http://localhost:54000/
|
||||
|
||||
# Enable password database for static users
|
||||
enablePasswordDB: true
|
||||
|
||||
# Static users - add more users here as needed
|
||||
staticPasswords:
|
||||
- email: "admin@$NETBIRD_DOMAIN"
|
||||
hash: "$ADMIN_PASSWORD_HASH"
|
||||
username: "admin"
|
||||
userID: "$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "admin-user-id-001")"
|
||||
|
||||
# Optional: Add external identity provider connectors
|
||||
# connectors:
|
||||
# - type: github
|
||||
# id: github
|
||||
# name: GitHub
|
||||
# config:
|
||||
# clientID: \$GITHUB_CLIENT_ID
|
||||
# clientSecret: \$GITHUB_CLIENT_SECRET
|
||||
# redirectURI: $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/dex/callback
|
||||
#
|
||||
# - type: ldap
|
||||
# id: ldap
|
||||
# name: LDAP
|
||||
# config:
|
||||
# host: ldap.example.com:636
|
||||
# insecureNoSSL: false
|
||||
# bindDN: cn=admin,dc=example,dc=com
|
||||
# bindPW: admin
|
||||
# userSearch:
|
||||
# baseDN: ou=users,dc=example,dc=com
|
||||
# filter: "(objectClass=person)"
|
||||
# username: uid
|
||||
# idAttr: uid
|
||||
# emailAttr: mail
|
||||
# nameAttr: cn
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
render_turn_server_conf() {
|
||||
cat <<EOF
|
||||
listening-port=3478
|
||||
$TURN_EXTERNAL_IP_CONFIG
|
||||
tls-listening-port=5349
|
||||
min-port=$TURN_MIN_PORT
|
||||
max-port=$TURN_MAX_PORT
|
||||
fingerprint
|
||||
lt-cred-mech
|
||||
user=$TURN_USER:$TURN_PASSWORD
|
||||
realm=wiretrustee.com
|
||||
cert=/etc/coturn/certs/cert.pem
|
||||
pkey=/etc/coturn/private/privkey.pem
|
||||
log-file=stdout
|
||||
no-software-attribute
|
||||
pidfile="/var/tmp/turnserver.pid"
|
||||
no-cli
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
render_management_json() {
|
||||
cat <<EOF
|
||||
{
|
||||
"Stuns": [
|
||||
{
|
||||
"Proto": "udp",
|
||||
"URI": "stun:$NETBIRD_DOMAIN:3478"
|
||||
}
|
||||
],
|
||||
"Relay": {
|
||||
"Addresses": ["$NETBIRD_RELAY_PROTO://$NETBIRD_DOMAIN:$NETBIRD_PORT"],
|
||||
"CredentialsTTL": "24h",
|
||||
"Secret": "$NETBIRD_RELAY_AUTH_SECRET"
|
||||
},
|
||||
"Signal": {
|
||||
"Proto": "$NETBIRD_HTTP_PROTOCOL",
|
||||
"URI": "$NETBIRD_DOMAIN:$NETBIRD_PORT"
|
||||
},
|
||||
"HttpConfig": {
|
||||
"AuthIssuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/dex",
|
||||
"AuthAudience": "netbird-dashboard",
|
||||
"OIDCConfigEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/dex/.well-known/openid-configuration"
|
||||
},
|
||||
"IdpManagerConfig": {
|
||||
"ManagerType": "dex",
|
||||
"ClientConfig": {
|
||||
"Issuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/dex"
|
||||
},
|
||||
"ExtraConfig": {
|
||||
"GRPCAddr": "dex:5557"
|
||||
}
|
||||
},
|
||||
"DeviceAuthorizationFlow": {
|
||||
"Provider": "hosted",
|
||||
"ProviderConfig": {
|
||||
"Audience": "netbird-cli",
|
||||
"ClientID": "netbird-cli",
|
||||
"Scope": "openid profile email offline_access",
|
||||
"DeviceAuthEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/dex/device/code",
|
||||
"TokenEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/dex/token"
|
||||
}
|
||||
},
|
||||
"PKCEAuthorizationFlow": {
|
||||
"ProviderConfig": {
|
||||
"Audience": "netbird-cli",
|
||||
"ClientID": "netbird-cli",
|
||||
"Scope": "openid profile email offline_access",
|
||||
"RedirectURLs": ["http://localhost:53000/", "http://localhost:54000/"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
render_dashboard_env() {
|
||||
cat <<EOF
|
||||
# Endpoints
|
||||
NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
|
||||
NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
|
||||
# OIDC
|
||||
AUTH_AUDIENCE=netbird-dashboard
|
||||
AUTH_CLIENT_ID=netbird-dashboard
|
||||
AUTH_CLIENT_SECRET=$DEX_DASHBOARD_CLIENT_SECRET
|
||||
AUTH_AUTHORITY=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/dex
|
||||
USE_AUTH0=false
|
||||
AUTH_SUPPORTED_SCOPES=openid profile email offline_access
|
||||
AUTH_REDIRECT_URI=/nb-auth
|
||||
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
|
||||
# SSL
|
||||
NGINX_SSL_PORT=443
|
||||
# Letsencrypt
|
||||
LETSENCRYPT_DOMAIN=none
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
render_relay_env() {
|
||||
cat <<EOF
|
||||
NB_LOG_LEVEL=info
|
||||
NB_LISTEN_ADDRESS=:80
|
||||
NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_PROTO://$NETBIRD_DOMAIN:$NETBIRD_PORT
|
||||
NB_AUTH_SECRET=$NETBIRD_RELAY_AUTH_SECRET
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
render_docker_compose() {
|
||||
cat <<EOF
|
||||
services:
|
||||
# Caddy reverse proxy
|
||||
caddy:
|
||||
image: caddy
|
||||
container_name: netbird-caddy
|
||||
restart: unless-stopped
|
||||
networks: [netbird]
|
||||
ports:
|
||||
- '443:443'
|
||||
- '443:443/udp'
|
||||
- '80:80'
|
||||
volumes:
|
||||
- netbird_caddy_data:/data
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
|
||||
# Dex - identity provider
|
||||
dex:
|
||||
image: ghcr.io/dexidp/dex:v2.38.0
|
||||
container_name: netbird-dex
|
||||
restart: unless-stopped
|
||||
networks: [netbird]
|
||||
volumes:
|
||||
- ./dex.yaml:/etc/dex/config.docker.yaml:ro
|
||||
- netbird_dex_data:/var/dex
|
||||
command: ["dex", "serve", "/etc/dex/config.docker.yaml"]
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
|
||||
# UI dashboard
|
||||
dashboard:
|
||||
image: netbirdio/dashboard:latest
|
||||
container_name: netbird-dashboard
|
||||
restart: unless-stopped
|
||||
networks: [netbird]
|
||||
env_file:
|
||||
- ./dashboard.env
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
|
||||
# Signal
|
||||
signal:
|
||||
image: netbirdio/signal:latest
|
||||
container_name: netbird-signal
|
||||
restart: unless-stopped
|
||||
networks: [netbird]
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
|
||||
# Relay
|
||||
relay:
|
||||
image: netbirdio/relay:latest
|
||||
container_name: netbird-relay
|
||||
restart: unless-stopped
|
||||
networks: [netbird]
|
||||
env_file:
|
||||
- ./relay.env
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
|
||||
# Management
|
||||
management:
|
||||
image: netbirdio/management:latest
|
||||
container_name: netbird-management
|
||||
restart: unless-stopped
|
||||
networks: [netbird]
|
||||
volumes:
|
||||
- netbird_management:/var/lib/netbird
|
||||
- ./management.json:/etc/netbird/management.json
|
||||
command: [
|
||||
"--port", "80",
|
||||
"--log-file", "console",
|
||||
"--log-level", "info",
|
||||
"--disable-anonymous-metrics=false",
|
||||
"--single-account-mode-domain=netbird.selfhosted",
|
||||
"--dns-domain=netbird.selfhosted",
|
||||
"--idp-sign-key-refresh-enabled",
|
||||
]
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
|
||||
# Coturn, AKA TURN server
|
||||
coturn:
|
||||
image: coturn/coturn
|
||||
container_name: netbird-coturn
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./turnserver.conf:/etc/turnserver.conf:ro
|
||||
network_mode: host
|
||||
command:
|
||||
- -c /etc/turnserver.conf
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
|
||||
volumes:
|
||||
netbird_caddy_data:
|
||||
netbird_dex_data:
|
||||
netbird_management:
|
||||
|
||||
networks:
|
||||
netbird:
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
init_environment
|
||||
445
management/server/idp/dex.go
Normal file
445
management/server/idp/dex.go
Normal file
@@ -0,0 +1,445 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dexidp/dex/api/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
// DexManager implements the Manager interface for Dex IDP.
|
||||
// It uses Dex's gRPC API to manage users in the password database.
|
||||
type DexManager struct {
|
||||
grpcAddr string
|
||||
httpClient ManagerHTTPClient
|
||||
helper ManagerHelper
|
||||
appMetrics telemetry.AppMetrics
|
||||
mux sync.Mutex
|
||||
conn *grpc.ClientConn
|
||||
}
|
||||
|
||||
// DexClientConfig Dex manager client configuration.
|
||||
type DexClientConfig struct {
|
||||
// GRPCAddr is the address of Dex's gRPC API (e.g., "localhost:5557")
|
||||
GRPCAddr string
|
||||
// Issuer is the Dex issuer URL (e.g., "https://dex.example.com/dex")
|
||||
Issuer string
|
||||
}
|
||||
|
||||
// NewDexManager creates a new instance of DexManager.
|
||||
func NewDexManager(config DexClientConfig, appMetrics telemetry.AppMetrics) (*DexManager, error) {
|
||||
if config.GRPCAddr == "" {
|
||||
return nil, fmt.Errorf("dex IdP configuration is incomplete, GRPCAddr is missing")
|
||||
}
|
||||
|
||||
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
httpTransport.MaxIdleConns = 5
|
||||
|
||||
httpClient := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: httpTransport,
|
||||
}
|
||||
helper := JsonParser{}
|
||||
|
||||
return &DexManager{
|
||||
grpcAddr: config.GRPCAddr,
|
||||
httpClient: httpClient,
|
||||
helper: helper,
|
||||
appMetrics: appMetrics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getConnection returns a gRPC connection to Dex, creating one if necessary.
|
||||
// It also checks if an existing connection is still healthy and reconnects if needed.
|
||||
func (dm *DexManager) getConnection(ctx context.Context) (*grpc.ClientConn, error) {
|
||||
dm.mux.Lock()
|
||||
defer dm.mux.Unlock()
|
||||
|
||||
if dm.conn != nil {
|
||||
state := dm.conn.GetState()
|
||||
// If connection is shutdown or in a transient failure, close and reconnect
|
||||
if state == connectivity.Shutdown || state == connectivity.TransientFailure {
|
||||
log.WithContext(ctx).Debugf("Dex gRPC connection in state %s, reconnecting", state)
|
||||
_ = dm.conn.Close()
|
||||
dm.conn = nil
|
||||
} else {
|
||||
return dm.conn, nil
|
||||
}
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("connecting to Dex gRPC API at %s", dm.grpcAddr)
|
||||
|
||||
conn, err := grpc.NewClient(dm.grpcAddr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Dex gRPC API: %w", err)
|
||||
}
|
||||
|
||||
dm.conn = conn
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// getDexClient returns a Dex API client.
|
||||
func (dm *DexManager) getDexClient(ctx context.Context) (api.DexClient, error) {
|
||||
conn, err := dm.getConnection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return api.NewDexClient(conn), nil
|
||||
}
|
||||
|
||||
// encodeDexUserID encodes a user ID and connector ID into Dex's composite format.
|
||||
// This is the reverse of parseDexUserID - it creates the base64-encoded protobuf
|
||||
// format that Dex uses in JWT tokens.
|
||||
func encodeDexUserID(userID, connectorID string) string {
|
||||
// Build simple protobuf structure:
|
||||
// Field 1 (tag 0x0a): user ID string
|
||||
// Field 2 (tag 0x12): connector ID string
|
||||
buf := make([]byte, 0, 2+len(userID)+2+len(connectorID))
|
||||
|
||||
// Field 1: user ID
|
||||
buf = append(buf, 0x0a) // tag for field 1, wire type 2 (length-delimited)
|
||||
buf = append(buf, byte(len(userID))) // length
|
||||
buf = append(buf, []byte(userID)...) // value
|
||||
|
||||
// Field 2: connector ID
|
||||
buf = append(buf, 0x12) // tag for field 2, wire type 2 (length-delimited)
|
||||
buf = append(buf, byte(len(connectorID))) // length
|
||||
buf = append(buf, []byte(connectorID)...) // value
|
||||
|
||||
return base64.StdEncoding.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// parseDexUserID extracts the actual user ID from Dex's composite user ID.
|
||||
// Dex encodes user IDs in JWT tokens as base64-encoded protobuf with format:
|
||||
// - Field 1 (string): actual user ID
|
||||
// - Field 2 (string): connector ID (e.g., "local")
|
||||
// If the ID is not in this format, it returns the original ID.
|
||||
func parseDexUserID(compositeID string) string {
|
||||
// Try to decode as standard base64
|
||||
decoded, err := base64.StdEncoding.DecodeString(compositeID)
|
||||
if err != nil {
|
||||
// Try URL-safe base64
|
||||
decoded, err = base64.RawURLEncoding.DecodeString(compositeID)
|
||||
if err != nil {
|
||||
// Not base64 encoded, return as-is
|
||||
return compositeID
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the simple protobuf structure
|
||||
// Field 1 (tag 0x0a): user ID string
|
||||
// Field 2 (tag 0x12): connector ID string
|
||||
if len(decoded) < 2 {
|
||||
return compositeID
|
||||
}
|
||||
|
||||
// Check for field 1 tag (0x0a = field 1, wire type 2/length-delimited)
|
||||
if decoded[0] != 0x0a {
|
||||
return compositeID
|
||||
}
|
||||
|
||||
// Read the length of the user ID string
|
||||
length := int(decoded[1])
|
||||
if len(decoded) < 2+length {
|
||||
return compositeID
|
||||
}
|
||||
|
||||
// Extract the user ID
|
||||
userID := string(decoded[2 : 2+length])
|
||||
return userID
|
||||
}
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||
// Dex doesn't support app metadata, so this is a no-op.
|
||||
func (dm *DexManager) UpdateUserAppMetadata(_ context.Context, _ string, _ AppMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserDataByID requests user data from Dex via user ID.
|
||||
func (dm *DexManager) GetUserDataByID(ctx context.Context, userID string, _ AppMetadata) (*UserData, error) {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountGetUserDataByID()
|
||||
}
|
||||
|
||||
client, err := dm.getDexClient(ctx)
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.ListPasswords(ctx, &api.ListPasswordReq{})
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to list passwords from Dex: %w", err)
|
||||
}
|
||||
|
||||
// Try to parse the composite user ID from Dex JWT token
|
||||
actualUserID := parseDexUserID(userID)
|
||||
|
||||
for _, p := range resp.Passwords {
|
||||
// Match against both the raw userID and the parsed actualUserID
|
||||
if p.UserId == userID || p.UserId == actualUserID {
|
||||
return &UserData{
|
||||
Email: p.Email,
|
||||
Name: p.Username,
|
||||
ID: userID, // Return the original ID for consistency
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("user with ID %s not found", userID)
|
||||
}
|
||||
|
||||
// GetAccount returns all the users for a given account.
|
||||
// Since Dex doesn't have account concepts, this returns all users.
|
||||
func (dm *DexManager) GetAccount(ctx context.Context, accountID string) ([]*UserData, error) {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountGetAccount()
|
||||
}
|
||||
|
||||
users, err := dm.getAllUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set the account ID for all users
|
||||
for _, user := range users {
|
||||
user.AppMetadata.WTAccountID = accountID
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||
// Since Dex doesn't have account concepts, all users are returned under UnsetAccountID.
|
||||
func (dm *DexManager) GetAllAccounts(ctx context.Context) (map[string][]*UserData, error) {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||
}
|
||||
|
||||
users, err := dm.getAllUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexedUsers := make(map[string][]*UserData)
|
||||
indexedUsers[UnsetAccountID] = users
|
||||
|
||||
return indexedUsers, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in Dex's password database.
|
||||
func (dm *DexManager) CreateUser(ctx context.Context, email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountCreateUser()
|
||||
}
|
||||
|
||||
client, err := dm.getDexClient(ctx)
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate a random password for the new user
|
||||
password := GeneratePassword(16, 2, 2, 2)
|
||||
|
||||
// Hash the password using bcrypt
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// Generate a user ID from email (Dex uses email as the key, but we need a stable ID)
|
||||
userID := strings.ReplaceAll(email, "@", "-at-")
|
||||
userID = strings.ReplaceAll(userID, ".", "-")
|
||||
|
||||
req := &api.CreatePasswordReq{
|
||||
Password: &api.Password{
|
||||
Email: email,
|
||||
Username: name,
|
||||
UserId: userID,
|
||||
Hash: hashedPassword,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.CreatePassword(ctx, req)
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to create user in Dex: %w", err)
|
||||
}
|
||||
|
||||
if resp.AlreadyExists {
|
||||
return nil, fmt.Errorf("user with email %s already exists", email)
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("created user %s in Dex", email)
|
||||
|
||||
return &UserData{
|
||||
Email: email,
|
||||
Name: name,
|
||||
ID: userID,
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: accountID,
|
||||
WTInvitedBy: invitedByEmail,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail searches users with a given email.
|
||||
// If no users have been found, this function returns an empty list.
|
||||
func (dm *DexManager) GetUserByEmail(ctx context.Context, email string) ([]*UserData, error) {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountGetUserByEmail()
|
||||
}
|
||||
|
||||
client, err := dm.getDexClient(ctx)
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.ListPasswords(ctx, &api.ListPasswordReq{})
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to list passwords from Dex: %w", err)
|
||||
}
|
||||
|
||||
users := make([]*UserData, 0)
|
||||
for _, p := range resp.Passwords {
|
||||
if strings.EqualFold(p.Email, email) {
|
||||
// Encode the user ID in Dex's composite format to match stored IDs
|
||||
encodedID := encodeDexUserID(p.UserId, "local")
|
||||
users = append(users, &UserData{
|
||||
Email: p.Email,
|
||||
Name: p.Username,
|
||||
ID: encodedID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// InviteUserByID resends an invitation to a user.
|
||||
// Dex doesn't support invitations, so this returns an error.
|
||||
func (dm *DexManager) InviteUserByID(_ context.Context, _ string) error {
|
||||
return fmt.Errorf("method InviteUserByID not implemented for Dex")
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user from Dex by user ID.
|
||||
func (dm *DexManager) DeleteUser(ctx context.Context, userID string) error {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountDeleteUser()
|
||||
}
|
||||
|
||||
client, err := dm.getDexClient(ctx)
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// First, find the user's email by ID
|
||||
resp, err := client.ListPasswords(ctx, &api.ListPasswordReq{})
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return fmt.Errorf("failed to list passwords from Dex: %w", err)
|
||||
}
|
||||
|
||||
// Try to parse the composite user ID from Dex JWT token
|
||||
actualUserID := parseDexUserID(userID)
|
||||
|
||||
var email string
|
||||
for _, p := range resp.Passwords {
|
||||
if p.UserId == userID || p.UserId == actualUserID {
|
||||
email = p.Email
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if email == "" {
|
||||
return fmt.Errorf("user with ID %s not found", userID)
|
||||
}
|
||||
|
||||
// Delete the user by email
|
||||
deleteResp, err := client.DeletePassword(ctx, &api.DeletePasswordReq{
|
||||
Email: email,
|
||||
})
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return fmt.Errorf("failed to delete user from Dex: %w", err)
|
||||
}
|
||||
|
||||
if deleteResp.NotFound {
|
||||
return fmt.Errorf("user with email %s not found", email)
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("deleted user %s from Dex", email)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAllUsers retrieves all users from Dex's password database.
|
||||
func (dm *DexManager) getAllUsers(ctx context.Context) ([]*UserData, error) {
|
||||
client, err := dm.getDexClient(ctx)
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.ListPasswords(ctx, &api.ListPasswordReq{})
|
||||
if err != nil {
|
||||
if dm.appMetrics != nil {
|
||||
dm.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to list passwords from Dex: %w", err)
|
||||
}
|
||||
|
||||
users := make([]*UserData, 0, len(resp.Passwords))
|
||||
for _, p := range resp.Passwords {
|
||||
// Encode the user ID in Dex's composite format (base64-encoded protobuf)
|
||||
// to match how NetBird stores user IDs from Dex JWT tokens.
|
||||
// The connector ID "local" is used for Dex's password database.
|
||||
encodedID := encodeDexUserID(p.UserId, "local")
|
||||
users = append(users, &UserData{
|
||||
Email: p.Email,
|
||||
Name: p.Username,
|
||||
ID: encodedID,
|
||||
})
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
137
management/server/idp/dex_test.go
Normal file
137
management/server/idp/dex_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
func TestNewDexManager(t *testing.T) {
|
||||
type test struct {
|
||||
name string
|
||||
inputConfig DexClientConfig
|
||||
assertErrFunc require.ErrorAssertionFunc
|
||||
assertErrFuncMessage string
|
||||
}
|
||||
|
||||
defaultTestConfig := DexClientConfig{
|
||||
GRPCAddr: "localhost:5557",
|
||||
Issuer: "https://dex.example.com/dex",
|
||||
}
|
||||
|
||||
testCase1 := test{
|
||||
name: "Good Configuration",
|
||||
inputConfig: defaultTestConfig,
|
||||
assertErrFunc: require.NoError,
|
||||
assertErrFuncMessage: "shouldn't return error",
|
||||
}
|
||||
|
||||
testCase2Config := defaultTestConfig
|
||||
testCase2Config.GRPCAddr = ""
|
||||
|
||||
testCase2 := test{
|
||||
name: "Missing GRPCAddr Configuration",
|
||||
inputConfig: testCase2Config,
|
||||
assertErrFunc: require.Error,
|
||||
assertErrFuncMessage: "should return error when GRPCAddr is empty",
|
||||
}
|
||||
|
||||
// Test with empty issuer - should still work since issuer is optional for the manager
|
||||
testCase3Config := defaultTestConfig
|
||||
testCase3Config.Issuer = ""
|
||||
|
||||
testCase3 := test{
|
||||
name: "Missing Issuer Configuration - OK",
|
||||
inputConfig: testCase3Config,
|
||||
assertErrFunc: require.NoError,
|
||||
assertErrFuncMessage: "shouldn't return error when issuer is empty",
|
||||
}
|
||||
|
||||
for _, testCase := range []test{testCase1, testCase2, testCase3} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
manager, err := NewDexManager(testCase.inputConfig, &telemetry.MockAppMetrics{})
|
||||
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
|
||||
|
||||
if err == nil {
|
||||
require.NotNil(t, manager, "manager should not be nil")
|
||||
require.Equal(t, testCase.inputConfig.GRPCAddr, manager.grpcAddr, "grpcAddr should match")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDexManagerUpdateUserAppMetadata(t *testing.T) {
|
||||
config := DexClientConfig{
|
||||
GRPCAddr: "localhost:5557",
|
||||
Issuer: "https://dex.example.com/dex",
|
||||
}
|
||||
|
||||
manager, err := NewDexManager(config, &telemetry.MockAppMetrics{})
|
||||
require.NoError(t, err, "should create manager without error")
|
||||
|
||||
// UpdateUserAppMetadata should be a no-op for Dex
|
||||
err = manager.UpdateUserAppMetadata(context.Background(), "test-user-id", AppMetadata{
|
||||
WTAccountID: "test-account",
|
||||
})
|
||||
require.NoError(t, err, "UpdateUserAppMetadata should not return error")
|
||||
}
|
||||
|
||||
func TestDexManagerInviteUserByID(t *testing.T) {
|
||||
config := DexClientConfig{
|
||||
GRPCAddr: "localhost:5557",
|
||||
Issuer: "https://dex.example.com/dex",
|
||||
}
|
||||
|
||||
manager, err := NewDexManager(config, &telemetry.MockAppMetrics{})
|
||||
require.NoError(t, err, "should create manager without error")
|
||||
|
||||
// InviteUserByID should return an error for Dex
|
||||
err = manager.InviteUserByID(context.Background(), "test-user-id")
|
||||
require.Error(t, err, "InviteUserByID should return error")
|
||||
require.Contains(t, err.Error(), "not implemented", "error should mention not implemented")
|
||||
}
|
||||
|
||||
func TestParseDexUserID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
compositeID string
|
||||
expectedID string
|
||||
}{
|
||||
{
|
||||
name: "Parse base64-encoded protobuf composite ID",
|
||||
// This is a real Dex composite ID: contains user ID "cf5db180-d360-484d-9b78-c5db92146420" and connector "local"
|
||||
compositeID: "CiRjZjVkYjE4MC1kMzYwLTQ4NGQtOWI3OC1jNWRiOTIxNDY0MjASBWxvY2Fs",
|
||||
expectedID: "cf5db180-d360-484d-9b78-c5db92146420",
|
||||
},
|
||||
{
|
||||
name: "Return plain ID unchanged",
|
||||
compositeID: "simple-user-id",
|
||||
expectedID: "simple-user-id",
|
||||
},
|
||||
{
|
||||
name: "Return UUID unchanged",
|
||||
compositeID: "cf5db180-d360-484d-9b78-c5db92146420",
|
||||
expectedID: "cf5db180-d360-484d-9b78-c5db92146420",
|
||||
},
|
||||
{
|
||||
name: "Handle empty string",
|
||||
compositeID: "",
|
||||
expectedID: "",
|
||||
},
|
||||
{
|
||||
name: "Handle invalid base64",
|
||||
compositeID: "not-valid-base64!!!",
|
||||
expectedID: "not-valid-base64!!!",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := parseDexUserID(tt.compositeID)
|
||||
require.Equal(t, tt.expectedID, result, "parsed user ID should match expected")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -173,40 +173,40 @@ func NewManager(ctx context.Context, config Config, appMetrics telemetry.AppMetr
|
||||
|
||||
return NewZitadelManager(*zitadelClientConfig, appMetrics)
|
||||
case "authentik":
|
||||
authentikConfig := AuthentikClientConfig{
|
||||
return NewAuthentikManager(AuthentikClientConfig{
|
||||
Issuer: config.ClientConfig.Issuer,
|
||||
ClientID: config.ClientConfig.ClientID,
|
||||
TokenEndpoint: config.ClientConfig.TokenEndpoint,
|
||||
GrantType: config.ClientConfig.GrantType,
|
||||
Username: config.ExtraConfig["Username"],
|
||||
Password: config.ExtraConfig["Password"],
|
||||
}
|
||||
return NewAuthentikManager(authentikConfig, appMetrics)
|
||||
}, appMetrics)
|
||||
case "okta":
|
||||
oktaClientConfig := OktaClientConfig{
|
||||
return NewOktaManager(OktaClientConfig{
|
||||
Issuer: config.ClientConfig.Issuer,
|
||||
TokenEndpoint: config.ClientConfig.TokenEndpoint,
|
||||
GrantType: config.ClientConfig.GrantType,
|
||||
APIToken: config.ExtraConfig["ApiToken"],
|
||||
}
|
||||
return NewOktaManager(oktaClientConfig, appMetrics)
|
||||
}, appMetrics)
|
||||
case "google":
|
||||
googleClientConfig := GoogleWorkspaceClientConfig{
|
||||
return NewGoogleWorkspaceManager(ctx, GoogleWorkspaceClientConfig{
|
||||
ServiceAccountKey: config.ExtraConfig["ServiceAccountKey"],
|
||||
CustomerID: config.ExtraConfig["CustomerId"],
|
||||
}
|
||||
return NewGoogleWorkspaceManager(ctx, googleClientConfig, appMetrics)
|
||||
}, appMetrics)
|
||||
case "jumpcloud":
|
||||
jumpcloudConfig := JumpCloudClientConfig{
|
||||
return NewJumpCloudManager(JumpCloudClientConfig{
|
||||
APIToken: config.ExtraConfig["ApiToken"],
|
||||
}
|
||||
return NewJumpCloudManager(jumpcloudConfig, appMetrics)
|
||||
}, appMetrics)
|
||||
case "pocketid":
|
||||
pocketidConfig := PocketIdClientConfig{
|
||||
return NewPocketIdManager(PocketIdClientConfig{
|
||||
APIToken: config.ExtraConfig["ApiToken"],
|
||||
ManagementEndpoint: config.ExtraConfig["ManagementEndpoint"],
|
||||
}
|
||||
return NewPocketIdManager(pocketidConfig, appMetrics)
|
||||
}, appMetrics)
|
||||
case "dex":
|
||||
return NewDexManager(DexClientConfig{
|
||||
GRPCAddr: config.ExtraConfig["GRPCAddr"],
|
||||
Issuer: config.ClientConfig.Issuer,
|
||||
}, appMetrics)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid manager type: %s", config.ManagerType)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user