mirror of
https://github.com/netbirdio/netbird
synced 2026-04-22 17:44:57 +02:00
Compare commits
58 Commits
v0.65.2
...
1f89a156b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f89a156b9 | ||
|
|
d266eda1ac | ||
|
|
3fd9b4b023 | ||
|
|
ee45c2a545 | ||
|
|
a93eb6f732 | ||
|
|
dbf02c859c | ||
|
|
6f280f696c | ||
|
|
2c32126582 | ||
|
|
000c50319c | ||
|
|
fd88c1b215 | ||
|
|
23d958d18a | ||
|
|
2896c89a39 | ||
|
|
74307febb2 | ||
|
|
086b4ee4d7 | ||
|
|
b6f0c656fb | ||
|
|
414c004650 | ||
|
|
d0ef552d09 | ||
|
|
9da9cef236 | ||
|
|
7de2d06ea3 | ||
|
|
eab95cb91f | ||
|
|
2de35591b0 | ||
|
|
1864e9c0bc | ||
|
|
9bb4535b89 | ||
|
|
fe16cb6ce2 | ||
|
|
9858379c2a | ||
|
|
bef25494de | ||
|
|
45d2fe1c50 | ||
|
|
a143cb384b | ||
|
|
6ae7b10bb3 | ||
|
|
ced2dedd84 | ||
|
|
9bdd2148ef | ||
|
|
1e06170203 | ||
|
|
bf21a47d7b | ||
|
|
cccf35eb40 | ||
|
|
2a7a374796 | ||
|
|
bd6b7bc842 | ||
|
|
95cef86892 | ||
|
|
2dea90967b | ||
|
|
e13a737550 | ||
|
|
1d56b26edf | ||
|
|
dc2f75b0fa | ||
|
|
ca8720bfcc | ||
|
|
db7833c938 | ||
|
|
c81fac3114 | ||
|
|
e328239697 | ||
|
|
cb7c58761e | ||
|
|
7f067798de | ||
|
|
9e045c95cc | ||
|
|
bc9aed3826 | ||
|
|
219dd848b8 | ||
|
|
182e4039f5 | ||
|
|
e35344b283 | ||
|
|
e71feb493d | ||
|
|
d86e1b1cce | ||
|
|
7eeef7cf70 | ||
|
|
4092675557 | ||
|
|
b76aa72586 | ||
|
|
f915522aa9 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,3 +31,4 @@ infrastructure_files/setup-*.env
|
||||
.DS_Store
|
||||
vendor/
|
||||
/netbird
|
||||
client/netbird-electron/
|
||||
|
||||
@@ -432,7 +432,7 @@ func (s *Server) extractAndValidateUser(token *gojwt.Token) (*auth.UserAuth, err
|
||||
return nil, fmt.Errorf("JWT extractor not initialized (user=%s)", userID)
|
||||
}
|
||||
|
||||
userAuth, err := jwtExtractor.ToUserAuth(token)
|
||||
userAuth, err := jwtExtractor.ToUserAuth(context.Background(), token)
|
||||
if err != nil {
|
||||
userID := extractUserID(token)
|
||||
return nil, fmt.Errorf("extract user from token (user=%s): %w", userID, err)
|
||||
|
||||
101
go.mod
101
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/netbirdio/netbird
|
||||
|
||||
go 1.24.10
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
cunicu.li/go-rosenpass v0.4.0
|
||||
@@ -8,22 +8,22 @@ require (
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/kardianos/service v1.2.3-0.20240613133416-becf2eb62b83
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
github.com/onsi/gomega v1.27.6
|
||||
github.com/rs/cors v1.8.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.9
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/sys v0.39.0
|
||||
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/protobuf v1.36.8
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
||||
@@ -41,6 +41,8 @@ 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 v0.0.0-00010101000000-000000000000
|
||||
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
|
||||
@@ -78,7 +80,7 @@ require (
|
||||
github.com/pion/transport/v3 v3.0.7
|
||||
github.com/pion/turn/v3 v3.0.1
|
||||
github.com/pkg/sftp v1.13.9
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/quic-go/quic-go v0.49.1
|
||||
github.com/redis/go-redis/v9 v9.7.3
|
||||
github.com/rs/xid v1.3.0
|
||||
@@ -96,11 +98,11 @@ require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0
|
||||
go.opentelemetry.io/otel v1.38.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.38.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
||||
go.uber.org/mock v0.5.0
|
||||
go.uber.org/zap v1.27.0
|
||||
goauthentik.io/api/v3 v3.2023051.3
|
||||
@@ -108,11 +110,11 @@ require (
|
||||
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab
|
||||
golang.org/x/mod v0.30.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/term v0.37.0
|
||||
golang.org/x/time v0.12.0
|
||||
google.golang.org/api v0.177.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/term v0.38.0
|
||||
golang.org/x/time v0.14.0
|
||||
google.golang.org/api v0.257.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/postgres v1.5.7
|
||||
@@ -122,13 +124,18 @@ 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
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AppsFlyer/go-sundheit v0.6.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.12.3 // indirect
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
@@ -149,12 +156,14 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||
github.com/aws/smithy-go v1.22.2 // indirect
|
||||
github.com/beevik/etree v1.6.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/containerd v1.7.29 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
@@ -168,26 +177,30 @@ require (
|
||||
github.com/fyne-io/glfw-js v0.3.0 // indirect
|
||||
github.com/fyne-io/image v0.1.1 // indirect
|
||||
github.com/fyne-io/oksvg v0.2.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // 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-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 // 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
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/go-text/render v0.2.0 // indirect
|
||||
github.com/go-text/typesetting v0.2.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.2 // indirect
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
@@ -196,18 +209,23 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/kelseyhightower/envconfig v1.4.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/libdns/libdns v0.2.2 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||
github.com/mholt/acmez/v2 v2.0.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
@@ -230,11 +248,14 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/russellhaering/goxmldsig v1.5.0 // indirect
|
||||
github.com/rymdport/portal v0.4.2 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
@@ -245,17 +266,17 @@ require (
|
||||
github.com/wlynxg/anet v0.0.3 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
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/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/image v0.33.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
)
|
||||
@@ -271,3 +292,5 @@ replace github.com/cloudflare/circl => github.com/cunicu/circl v0.0.0-2023080111
|
||||
replace github.com/pion/ice/v4 => github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51
|
||||
|
||||
replace github.com/libp2p/go-netroute => github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944
|
||||
|
||||
replace github.com/dexidp/dex => /Users/misha/Documents/GolandProjects/dexidp
|
||||
|
||||
284
go.sum
284
go.sum
@@ -1,15 +1,14 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs=
|
||||
cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w=
|
||||
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/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
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.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
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=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
|
||||
@@ -18,17 +17,28 @@ fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlF
|
||||
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk=
|
||||
github.com/AppsFlyer/go-sundheit v0.6.0/go.mod h1:LDdBHD6tQBtmHsdW+i1GwdTt6Wqc0qazf5ZEJVTbTME=
|
||||
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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Microsoft/hcsshim v0.12.3 h1:LS9NXqXhMoqNCplK1ApmVSfB4UnVLRDWRapB6EIlxE0=
|
||||
github.com/Microsoft/hcsshim v0.12.3/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ=
|
||||
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible h1:hqcTK6ZISdip65SR792lwYJTa/axESA0889D3UlZbLo=
|
||||
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible/go.mod h1:6B1nuc1MUs6c62ODZDl7hVE5Pv7O2XGSkgg2olnq34I=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g=
|
||||
@@ -73,6 +83,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/Xv
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
|
||||
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
|
||||
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
@@ -87,7 +99,6 @@ github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+Y
|
||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
@@ -95,8 +106,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
|
||||
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE=
|
||||
@@ -107,9 +116,11 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
|
||||
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 h1:/DS5cDX3FJdl+XaN2D7XAwFpuanTxnp52DBLZAaJKx0=
|
||||
@@ -117,6 +128,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=
|
||||
@@ -133,14 +146,14 @@ github.com/eko/gocache/store/go_cache/v4 v4.2.2 h1:tAI9nl6TLoJyKG1ujF0CS0n/IgTEM
|
||||
github.com/eko/gocache/store/go_cache/v4 v4.2.2/go.mod h1:T9zkHokzr8K9EiC7RfMbDg6HSwaV6rv3UdcNu13SGcA=
|
||||
github.com/eko/gocache/store/redis/v4 v4.2.2 h1:Thw31fzGuH3WzJywsdbMivOmP550D6JS7GDHhvCJPA0=
|
||||
github.com/eko/gocache/store/redis/v4 v4.2.2/go.mod h1:LaTxLKx9TG/YUEybQvPMij++D7PBTIJ4+pzvk0ykz0w=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
|
||||
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@@ -159,13 +172,19 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
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-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
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=
|
||||
@@ -178,8 +197,8 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
@@ -195,11 +214,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -210,9 +224,7 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
@@ -220,12 +232,9 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -240,23 +249,24 @@ github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg
|
||||
github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gopacket/gopacket v1.1.1 h1:zbx9F9d6A7sWNkFKrvMBZTfGgxFoY4NgUudFVVHMfcw=
|
||||
github.com/gopacket/gopacket v1.1.1/go.mod h1:HavMeONEl7W9036of9LbSWoonqhH7HA1+ZRO+rMIvFs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357 h1:Fkzd8ktnpOR9h47SXHe2AYPwelXLH2GjGsjlAloiWfo=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357/go.mod h1:w9Y7gY31krpLmrVU5ZPG9H7l9fZuRu5/3R3S3FMtVQ4=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
||||
@@ -274,6 +284,8 @@ github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
|
||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
@@ -285,6 +297,18 @@ 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 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -295,6 +319,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
@@ -309,8 +335,11 @@ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuV
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
@@ -329,9 +358,11 @@ github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tA
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||
@@ -344,8 +375,12 @@ github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
@@ -434,6 +469,7 @@ github.com/pion/turn/v3 v3.0.1 h1:wLi7BTQr6/Q20R0vt/lHbjv6y4GChFtC33nkYbasoT8=
|
||||
github.com/pion/turn/v3 v3.0.1/go.mod h1:MrJDKgqryDyWy1/4NT9TWfXWGMC7UHT6pJIv1+gMeNE=
|
||||
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
||||
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
@@ -445,25 +481,26 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/quic-go/quic-go v0.49.1 h1:e5JXpUyF0f2uFjckQzD8jTghZrOUK1xxDqqZhlwixo0=
|
||||
github.com/quic-go/quic-go v0.49.1/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s=
|
||||
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
|
||||
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so=
|
||||
github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
|
||||
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw=
|
||||
github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
|
||||
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
@@ -473,21 +510,26 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
@@ -499,7 +541,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
@@ -553,30 +594,28 @@ github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
|
||||
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
||||
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/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
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.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
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=
|
||||
@@ -587,6 +626,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
goauthentik.io/api/v3 v3.2023051.3 h1:NebAhD/TeTWNo/9X3/Uj+rM5fG1HaiLOlKTNLQv9Qq4=
|
||||
goauthentik.io/api/v3 v3.2023051.3/go.mod h1:nYECml4jGbp/541hj8GcylKQG1gVBsKppHy4+7G8u4U=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -600,16 +641,12 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab h1:Iqyc+2zr7aGyLuEadIm0KRJP0Wwt+fhlXLa51Fxf1+Q=
|
||||
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab/go.mod h1:Eq3Nh/5pFSWug2ohiudJ1iyU59SO78QFuh4qTTN++I0=
|
||||
@@ -624,18 +661,13 @@ 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/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -649,12 +681,10 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -665,9 +695,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -703,8 +732,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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=
|
||||
@@ -717,8 +746,8 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
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/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -730,15 +759,11 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -761,42 +786,33 @@ 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=
|
||||
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=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
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.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
|
||||
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
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/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=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
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/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
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=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
@@ -832,5 +848,3 @@ gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
|
||||
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h1:qDCwdCWECGnwQSQC01Dpnp09fRHxJs9PbktotUqG+hs=
|
||||
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1/go.mod h1:8hmigyCdYtw5xJGfQDJzSH5Ju8XEIDBnpyi8+O6GRt8=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
58
idp/cmd/idp/config.example.yaml
Normal file
58
idp/cmd/idp/config.example.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
# Dex configuration for NetBird embedded IDP
|
||||
# Compatible with standard Dex configuration format
|
||||
|
||||
issuer: https://netbird.example.com/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
|
||||
redirectURIs:
|
||||
- https://netbird.example.com/nb-auth
|
||||
- https://netbird.example.com/nb-silent-auth
|
||||
public: true
|
||||
|
||||
# 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
|
||||
# Generate password hash with: htpasswd -bnBC 10 "" YOUR_PASSWORD | tr -d ':\n'
|
||||
staticPasswords:
|
||||
- email: "admin@example.com"
|
||||
hash: "$2b$10$Z.VZX/2WLt/ICVumQ0UVTeE6DCTjF97c40PjAVJ6mHF7M4XrMkFVW"
|
||||
username: "admin"
|
||||
userID: "8ec9f38f-bb87-46ea-a5f1-fd041e7c728a"
|
||||
|
||||
# Optional: Add external identity provider connectors
|
||||
# connectors:
|
||||
# - type: github
|
||||
# id: github
|
||||
# name: GitHub
|
||||
# config:
|
||||
# clientID: $GITHUB_CLIENT_ID
|
||||
# clientSecret: $GITHUB_CLIENT_SECRET
|
||||
# redirectURI: https://netbird.example.com/dex/callback
|
||||
96
idp/cmd/idp/main.go
Normal file
96
idp/cmd/idp/main.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Standalone OIDC Identity Provider server
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/idp/dex"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
configFile = flag.String("config", "", "Path to YAML config file (dex-compatible format)")
|
||||
addUser = flag.String("add-user", "", "Add a user (format: email:password)")
|
||||
listUsers = flag.Bool("list-users", false, "List all users and exit")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *configFile == "" {
|
||||
log.Fatal("--config flag is required. Please provide a YAML configuration file.")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Load YAML config
|
||||
yamlConfig, err := dex.LoadConfig(*configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
provider, err := dex.NewProviderFromYAML(ctx, yamlConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create provider: %v", err)
|
||||
}
|
||||
|
||||
// Handle --list-users
|
||||
if *listUsers {
|
||||
users, err := provider.ListUsers(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list users: %v", err)
|
||||
}
|
||||
if len(users) == 0 {
|
||||
fmt.Println("No users found")
|
||||
} else {
|
||||
fmt.Printf("Users (%d):\n", len(users))
|
||||
for _, u := range users {
|
||||
fmt.Printf(" - %s (%s)\n", u.Email, u.Username)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle --add-user
|
||||
if *addUser != "" {
|
||||
parts := strings.SplitN(*addUser, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
log.Fatalf("Invalid --add-user format. Use: email:password")
|
||||
}
|
||||
email, password := parts[0], parts[1]
|
||||
username := strings.Split(email, "@")[0] // Use part before @ as username
|
||||
|
||||
userID, err := provider.CreateUser(ctx, email, username, password)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
log.Infof("Created user: %s (ID: %s)", email, userID)
|
||||
}
|
||||
|
||||
if err := provider.Start(ctx); err != nil {
|
||||
log.Fatalf("Failed to start: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("OIDC Provider: %s", yamlConfig.Issuer)
|
||||
log.Infof("Discovery: %s/.well-known/openid-configuration", yamlConfig.Issuer)
|
||||
if yamlConfig.GRPC.Addr != "" {
|
||||
log.Infof("gRPC API: %s", yamlConfig.GRPC.Addr)
|
||||
}
|
||||
|
||||
// Wait for shutdown
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
|
||||
log.Info("Shutting down...")
|
||||
if err := provider.Stop(ctx); err != nil {
|
||||
log.Errorf("Shutdown error: %v", err)
|
||||
}
|
||||
}
|
||||
302
idp/dex/config.go
Normal file
302
idp/dex/config.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package dex
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/dexidp/dex/server"
|
||||
"github.com/dexidp/dex/storage"
|
||||
"github.com/dexidp/dex/storage/sql"
|
||||
|
||||
"github.com/netbirdio/netbird/idp/dex/web"
|
||||
)
|
||||
|
||||
// parseDuration parses a duration string (e.g., "6h", "24h", "168h").
|
||||
func parseDuration(s string) (time.Duration, error) {
|
||||
return time.ParseDuration(s)
|
||||
}
|
||||
|
||||
// YAMLConfig represents the YAML configuration file format (mirrors dex's config format)
|
||||
type YAMLConfig struct {
|
||||
Issuer string `yaml:"issuer" json:"issuer"`
|
||||
Storage Storage `yaml:"storage" json:"storage"`
|
||||
Web Web `yaml:"web" json:"web"`
|
||||
GRPC GRPC `yaml:"grpc" json:"grpc"`
|
||||
OAuth2 OAuth2 `yaml:"oauth2" json:"oauth2"`
|
||||
Expiry Expiry `yaml:"expiry" json:"expiry"`
|
||||
Logger Logger `yaml:"logger" json:"logger"`
|
||||
Frontend Frontend `yaml:"frontend" json:"frontend"`
|
||||
|
||||
// StaticConnectors are user defined connectors specified in the config file
|
||||
StaticConnectors []Connector `yaml:"connectors" json:"connectors"`
|
||||
|
||||
// StaticClients cause the server to use this list of clients rather than
|
||||
// querying the storage. Write operations, like creating a client, will fail.
|
||||
StaticClients []storage.Client `yaml:"staticClients" json:"staticClients"`
|
||||
|
||||
// If enabled, the server will maintain a list of passwords which can be used
|
||||
// to identify a user.
|
||||
EnablePasswordDB bool `yaml:"enablePasswordDB" json:"enablePasswordDB"`
|
||||
|
||||
// StaticPasswords cause the server use this list of passwords rather than
|
||||
// querying the storage.
|
||||
StaticPasswords []Password `yaml:"staticPasswords" json:"staticPasswords"`
|
||||
}
|
||||
|
||||
// Web is the config format for the HTTP server.
|
||||
type Web struct {
|
||||
HTTP string `yaml:"http" json:"http"`
|
||||
HTTPS string `yaml:"https" json:"https"`
|
||||
AllowedOrigins []string `yaml:"allowedOrigins" json:"allowedOrigins"`
|
||||
AllowedHeaders []string `yaml:"allowedHeaders" json:"allowedHeaders"`
|
||||
}
|
||||
|
||||
// GRPC is the config for the gRPC API.
|
||||
type GRPC struct {
|
||||
Addr string `yaml:"addr" json:"addr"`
|
||||
TLSCert string `yaml:"tlsCert" json:"tlsCert"`
|
||||
TLSKey string `yaml:"tlsKey" json:"tlsKey"`
|
||||
TLSClientCA string `yaml:"tlsClientCA" json:"tlsClientCA"`
|
||||
}
|
||||
|
||||
// OAuth2 describes enabled OAuth2 extensions.
|
||||
type OAuth2 struct {
|
||||
SkipApprovalScreen bool `yaml:"skipApprovalScreen" json:"skipApprovalScreen"`
|
||||
AlwaysShowLoginScreen bool `yaml:"alwaysShowLoginScreen" json:"alwaysShowLoginScreen"`
|
||||
PasswordConnector string `yaml:"passwordConnector" json:"passwordConnector"`
|
||||
ResponseTypes []string `yaml:"responseTypes" json:"responseTypes"`
|
||||
GrantTypes []string `yaml:"grantTypes" json:"grantTypes"`
|
||||
}
|
||||
|
||||
// Expiry holds configuration for the validity period of components.
|
||||
type Expiry struct {
|
||||
SigningKeys string `yaml:"signingKeys" json:"signingKeys"`
|
||||
IDTokens string `yaml:"idTokens" json:"idTokens"`
|
||||
AuthRequests string `yaml:"authRequests" json:"authRequests"`
|
||||
DeviceRequests string `yaml:"deviceRequests" json:"deviceRequests"`
|
||||
RefreshTokens RefreshTokensExpiry `yaml:"refreshTokens" json:"refreshTokens"`
|
||||
}
|
||||
|
||||
// RefreshTokensExpiry holds configuration for refresh token expiry.
|
||||
type RefreshTokensExpiry struct {
|
||||
ReuseInterval string `yaml:"reuseInterval" json:"reuseInterval"`
|
||||
ValidIfNotUsedFor string `yaml:"validIfNotUsedFor" json:"validIfNotUsedFor"`
|
||||
AbsoluteLifetime string `yaml:"absoluteLifetime" json:"absoluteLifetime"`
|
||||
DisableRotation bool `yaml:"disableRotation" json:"disableRotation"`
|
||||
}
|
||||
|
||||
// Logger holds configuration required to customize logging.
|
||||
type Logger struct {
|
||||
Level string `yaml:"level" json:"level"`
|
||||
Format string `yaml:"format" json:"format"`
|
||||
}
|
||||
|
||||
// Frontend holds the server's frontend templates and assets config.
|
||||
type Frontend struct {
|
||||
Dir string `yaml:"dir" json:"dir"`
|
||||
Theme string `yaml:"theme" json:"theme"`
|
||||
Issuer string `yaml:"issuer" json:"issuer"`
|
||||
LogoURL string `yaml:"logoURL" json:"logoURL"`
|
||||
Extra map[string]string `yaml:"extra" json:"extra"`
|
||||
}
|
||||
|
||||
// Storage holds app's storage configuration.
|
||||
type Storage struct {
|
||||
Type string `yaml:"type" json:"type"`
|
||||
Config map[string]interface{} `yaml:"config" json:"config"`
|
||||
}
|
||||
|
||||
// Password represents a static user configuration
|
||||
type Password storage.Password
|
||||
|
||||
func (p *Password) UnmarshalYAML(node *yaml.Node) error {
|
||||
var data struct {
|
||||
Email string `yaml:"email"`
|
||||
Username string `yaml:"username"`
|
||||
UserID string `yaml:"userID"`
|
||||
Hash string `yaml:"hash"`
|
||||
HashFromEnv string `yaml:"hashFromEnv"`
|
||||
}
|
||||
if err := node.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
*p = Password(storage.Password{
|
||||
Email: data.Email,
|
||||
Username: data.Username,
|
||||
UserID: data.UserID,
|
||||
})
|
||||
if len(data.Hash) == 0 && len(data.HashFromEnv) > 0 {
|
||||
data.Hash = os.Getenv(data.HashFromEnv)
|
||||
}
|
||||
if len(data.Hash) == 0 {
|
||||
return fmt.Errorf("no password hash provided for user %s", data.Email)
|
||||
}
|
||||
|
||||
// If this value is a valid bcrypt, use it.
|
||||
_, bcryptErr := bcrypt.Cost([]byte(data.Hash))
|
||||
if bcryptErr == nil {
|
||||
p.Hash = []byte(data.Hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// For backwards compatibility try to base64 decode this value.
|
||||
hashBytes, err := base64.StdEncoding.DecodeString(data.Hash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("malformed bcrypt hash: %v", bcryptErr)
|
||||
}
|
||||
if _, err := bcrypt.Cost(hashBytes); err != nil {
|
||||
return fmt.Errorf("malformed bcrypt hash: %v", err)
|
||||
}
|
||||
p.Hash = hashBytes
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connector is a connector configuration that can unmarshal YAML dynamically.
|
||||
type Connector struct {
|
||||
Type string `yaml:"type" json:"type"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
ID string `yaml:"id" json:"id"`
|
||||
Config map[string]interface{} `yaml:"config" json:"config"`
|
||||
}
|
||||
|
||||
// ToStorageConnector converts a Connector to storage.Connector type.
|
||||
func (c *Connector) ToStorageConnector() (storage.Connector, error) {
|
||||
data, err := json.Marshal(c.Config)
|
||||
if err != nil {
|
||||
return storage.Connector{}, fmt.Errorf("failed to marshal connector config: %v", err)
|
||||
}
|
||||
|
||||
return storage.Connector{
|
||||
ID: c.ID,
|
||||
Type: c.Type,
|
||||
Name: c.Name,
|
||||
Config: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StorageConfig is a configuration that can create a storage.
|
||||
type StorageConfig interface {
|
||||
Open(logger *slog.Logger) (storage.Storage, error)
|
||||
}
|
||||
|
||||
// OpenStorage opens a storage based on the config
|
||||
func (s *Storage) OpenStorage(logger *slog.Logger) (storage.Storage, error) {
|
||||
switch s.Type {
|
||||
case "sqlite3":
|
||||
file, _ := s.Config["file"].(string)
|
||||
if file == "" {
|
||||
return nil, fmt.Errorf("sqlite3 storage requires 'file' config")
|
||||
}
|
||||
cfg := &sql.SQLite3{File: file}
|
||||
return cfg.Open(logger)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported storage type: %s", s.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates the configuration
|
||||
func (c *YAMLConfig) Validate() error {
|
||||
if c.Issuer == "" {
|
||||
return fmt.Errorf("no issuer specified in config file")
|
||||
}
|
||||
if c.Storage.Type == "" {
|
||||
return fmt.Errorf("no storage type specified in config file")
|
||||
}
|
||||
if c.Web.HTTP == "" && c.Web.HTTPS == "" {
|
||||
return fmt.Errorf("must supply a HTTP/HTTPS address to listen on")
|
||||
}
|
||||
if !c.EnablePasswordDB && len(c.StaticPasswords) != 0 {
|
||||
return fmt.Errorf("cannot specify static passwords without enabling password db")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToServerConfig converts YAMLConfig to dex server.Config
|
||||
func (c *YAMLConfig) ToServerConfig(stor storage.Storage, logger *slog.Logger) server.Config {
|
||||
cfg := server.Config{
|
||||
Issuer: c.Issuer,
|
||||
Storage: stor,
|
||||
Logger: logger,
|
||||
SkipApprovalScreen: c.OAuth2.SkipApprovalScreen,
|
||||
AllowedOrigins: c.Web.AllowedOrigins,
|
||||
AllowedHeaders: c.Web.AllowedHeaders,
|
||||
Web: server.WebConfig{
|
||||
Issuer: c.Frontend.Issuer,
|
||||
LogoURL: c.Frontend.LogoURL,
|
||||
Theme: c.Frontend.Theme,
|
||||
Dir: c.Frontend.Dir,
|
||||
Extra: c.Frontend.Extra,
|
||||
},
|
||||
}
|
||||
|
||||
// Use embedded NetBird-styled templates if no custom dir specified
|
||||
if c.Frontend.Dir == "" {
|
||||
cfg.Web.WebFS = web.FS()
|
||||
}
|
||||
|
||||
if len(c.OAuth2.ResponseTypes) > 0 {
|
||||
cfg.SupportedResponseTypes = c.OAuth2.ResponseTypes
|
||||
}
|
||||
|
||||
// Apply expiry settings
|
||||
if c.Expiry.SigningKeys != "" {
|
||||
if d, err := parseDuration(c.Expiry.SigningKeys); err == nil {
|
||||
cfg.RotateKeysAfter = d
|
||||
}
|
||||
}
|
||||
if c.Expiry.IDTokens != "" {
|
||||
if d, err := parseDuration(c.Expiry.IDTokens); err == nil {
|
||||
cfg.IDTokensValidFor = d
|
||||
}
|
||||
}
|
||||
if c.Expiry.AuthRequests != "" {
|
||||
if d, err := parseDuration(c.Expiry.AuthRequests); err == nil {
|
||||
cfg.AuthRequestsValidFor = d
|
||||
}
|
||||
}
|
||||
if c.Expiry.DeviceRequests != "" {
|
||||
if d, err := parseDuration(c.Expiry.DeviceRequests); err == nil {
|
||||
cfg.DeviceRequestsValidFor = d
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// GetRefreshTokenPolicy creates a RefreshTokenPolicy from the expiry config.
|
||||
// This should be called after ToServerConfig and the policy set on the config.
|
||||
func (c *YAMLConfig) GetRefreshTokenPolicy(logger *slog.Logger) (*server.RefreshTokenPolicy, error) {
|
||||
return server.NewRefreshTokenPolicy(
|
||||
logger,
|
||||
c.Expiry.RefreshTokens.DisableRotation,
|
||||
c.Expiry.RefreshTokens.ValidIfNotUsedFor,
|
||||
c.Expiry.RefreshTokens.AbsoluteLifetime,
|
||||
c.Expiry.RefreshTokens.ReuseInterval,
|
||||
)
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from a YAML file
|
||||
func LoadConfig(path string) (*YAMLConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var cfg YAMLConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
942
idp/dex/provider.go
Normal file
942
idp/dex/provider.go
Normal file
@@ -0,0 +1,942 @@
|
||||
// Package dex provides an embedded Dex OIDC identity provider.
|
||||
package dex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dexapi "github.com/dexidp/dex/api/v2"
|
||||
"github.com/dexidp/dex/server"
|
||||
"github.com/dexidp/dex/storage"
|
||||
"github.com/dexidp/dex/storage/sql"
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Config matches what management/internals/server/server.go expects
|
||||
type Config struct {
|
||||
Issuer string
|
||||
Port int
|
||||
DataDir string
|
||||
DevMode bool
|
||||
|
||||
// GRPCAddr is the address for the gRPC API (e.g., ":5557"). Empty disables gRPC.
|
||||
GRPCAddr string
|
||||
}
|
||||
|
||||
// Provider wraps a Dex server
|
||||
type Provider struct {
|
||||
config *Config
|
||||
yamlConfig *YAMLConfig
|
||||
dexServer *server.Server
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
grpcServer *grpc.Server
|
||||
grpcListener net.Listener
|
||||
storage storage.Storage
|
||||
logger *slog.Logger
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
}
|
||||
|
||||
// NewProvider creates and initializes the Dex server
|
||||
func NewProvider(ctx context.Context, config *Config) (*Provider, error) {
|
||||
if config.Issuer == "" {
|
||||
return nil, fmt.Errorf("issuer is required")
|
||||
}
|
||||
if config.Port <= 0 {
|
||||
return nil, fmt.Errorf("invalid port")
|
||||
}
|
||||
if config.DataDir == "" {
|
||||
return nil, fmt.Errorf("data directory is required")
|
||||
}
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
|
||||
// Ensure data directory exists
|
||||
if err := os.MkdirAll(config.DataDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
// Initialize SQLite storage
|
||||
dbPath := filepath.Join(config.DataDir, "oidc.db")
|
||||
sqliteConfig := &sql.SQLite3{File: dbPath}
|
||||
stor, err := sqliteConfig.Open(logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open storage: %w", err)
|
||||
}
|
||||
|
||||
// Ensure a local connector exists (for password authentication)
|
||||
if err := ensureLocalConnector(ctx, stor); err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to ensure local connector: %w", err)
|
||||
}
|
||||
|
||||
// Ensure issuer ends with /oauth2 for proper path mounting
|
||||
issuer := strings.TrimSuffix(config.Issuer, "/")
|
||||
if !strings.HasSuffix(issuer, "/oauth2") {
|
||||
issuer = issuer + "/oauth2"
|
||||
}
|
||||
|
||||
// Build refresh token policy (required to avoid nil pointer panics)
|
||||
refreshPolicy, err := server.NewRefreshTokenPolicy(logger, false, "", "", "")
|
||||
if err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to create refresh token policy: %w", err)
|
||||
}
|
||||
|
||||
// Build Dex server config - use Dex's types directly
|
||||
dexConfig := server.Config{
|
||||
Issuer: issuer,
|
||||
Storage: stor,
|
||||
SkipApprovalScreen: true,
|
||||
SupportedResponseTypes: []string{"code"},
|
||||
Logger: logger,
|
||||
PrometheusRegistry: prometheus.NewRegistry(),
|
||||
RotateKeysAfter: 6 * time.Hour,
|
||||
IDTokensValidFor: 24 * time.Hour,
|
||||
RefreshTokenPolicy: refreshPolicy,
|
||||
Web: server.WebConfig{
|
||||
Issuer: "NetBird",
|
||||
},
|
||||
}
|
||||
|
||||
dexSrv, err := server.NewServer(ctx, dexConfig)
|
||||
if err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to create dex server: %w", err)
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
config: config,
|
||||
dexServer: dexSrv,
|
||||
storage: stor,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewProviderFromYAML creates and initializes the Dex server from a YAMLConfig
|
||||
func NewProviderFromYAML(ctx context.Context, yamlConfig *YAMLConfig) (*Provider, error) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
|
||||
// Open storage based on config
|
||||
stor, err := yamlConfig.Storage.OpenStorage(logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open storage: %w", err)
|
||||
}
|
||||
|
||||
// Ensure a local connector exists if password DB is enabled
|
||||
if yamlConfig.EnablePasswordDB {
|
||||
if err := ensureLocalConnector(ctx, stor); err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to ensure local connector: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create static passwords if provided
|
||||
for _, pw := range yamlConfig.StaticPasswords {
|
||||
existing, err := stor.GetPassword(ctx, pw.Email)
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
if err := stor.CreatePassword(ctx, storage.Password(pw)); err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to create password for %s: %w", pw.Email, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to get password for %s: %w", pw.Email, err)
|
||||
}
|
||||
// Update existing user if hash changed
|
||||
if string(existing.Hash) != string(pw.Hash) {
|
||||
if err := stor.UpdatePassword(ctx, pw.Email, func(old storage.Password) (storage.Password, error) {
|
||||
old.Hash = pw.Hash
|
||||
old.Username = pw.Username
|
||||
return old, nil
|
||||
}); err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to update password for %s: %w", pw.Email, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create static clients if provided
|
||||
for _, client := range yamlConfig.StaticClients {
|
||||
_, err := stor.GetClient(ctx, client.ID)
|
||||
if err == storage.ErrNotFound {
|
||||
if err := stor.CreateClient(ctx, client); err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to create client %s: %w", client.ID, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to get client %s: %w", client.ID, err)
|
||||
}
|
||||
// Update if exists
|
||||
if err := stor.UpdateClient(ctx, client.ID, func(old storage.Client) (storage.Client, error) {
|
||||
old.RedirectURIs = client.RedirectURIs
|
||||
old.Name = client.Name
|
||||
old.Public = client.Public
|
||||
return old, nil
|
||||
}); err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to update client %s: %w", client.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create connectors if provided
|
||||
for _, conn := range yamlConfig.StaticConnectors {
|
||||
storConn, err := conn.ToStorageConnector()
|
||||
if err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to convert connector %s: %w", conn.ID, err)
|
||||
}
|
||||
_, err = stor.GetConnector(ctx, conn.ID)
|
||||
if err == storage.ErrNotFound {
|
||||
if err := stor.CreateConnector(ctx, storConn); err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to create connector %s: %w", conn.ID, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to get connector %s: %w", conn.ID, err)
|
||||
}
|
||||
// Update if exists
|
||||
if err := stor.UpdateConnector(ctx, conn.ID, func(old storage.Connector) (storage.Connector, error) {
|
||||
old.Name = storConn.Name
|
||||
old.Config = storConn.Config
|
||||
return old, nil
|
||||
}); err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to update connector %s: %w", conn.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Build Dex server config
|
||||
dexConfig := yamlConfig.ToServerConfig(stor, logger)
|
||||
dexConfig.PrometheusRegistry = prometheus.NewRegistry()
|
||||
// Set defaults only if not configured in YAML
|
||||
if dexConfig.RotateKeysAfter == 0 {
|
||||
dexConfig.RotateKeysAfter = 6 * time.Hour
|
||||
}
|
||||
if dexConfig.IDTokensValidFor == 0 {
|
||||
dexConfig.IDTokensValidFor = 24 * time.Hour
|
||||
}
|
||||
if dexConfig.Web.Issuer == "" {
|
||||
dexConfig.Web.Issuer = "NetBird"
|
||||
}
|
||||
if len(dexConfig.SupportedResponseTypes) == 0 {
|
||||
dexConfig.SupportedResponseTypes = []string{"code"}
|
||||
}
|
||||
|
||||
// Create RefreshTokenPolicy from YAML config or use defaults
|
||||
refreshPolicy, err := yamlConfig.GetRefreshTokenPolicy(logger)
|
||||
if err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to create refresh token policy: %w", err)
|
||||
}
|
||||
dexConfig.RefreshTokenPolicy = refreshPolicy
|
||||
|
||||
dexSrv, err := server.NewServer(ctx, dexConfig)
|
||||
if err != nil {
|
||||
stor.Close()
|
||||
return nil, fmt.Errorf("failed to create dex server: %w", err)
|
||||
}
|
||||
|
||||
// Convert YAMLConfig to Config for internal use
|
||||
config := &Config{
|
||||
Issuer: yamlConfig.Issuer,
|
||||
GRPCAddr: yamlConfig.GRPC.Addr,
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
config: config,
|
||||
yamlConfig: yamlConfig,
|
||||
dexServer: dexSrv,
|
||||
storage: stor,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start starts the HTTP server and optionally the gRPC API server
|
||||
func (p *Provider) Start(_ context.Context) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.running {
|
||||
return fmt.Errorf("already running")
|
||||
}
|
||||
|
||||
// Determine listen address from config
|
||||
var addr string
|
||||
if p.yamlConfig != nil {
|
||||
addr = p.yamlConfig.Web.HTTP
|
||||
if addr == "" {
|
||||
addr = p.yamlConfig.Web.HTTPS
|
||||
}
|
||||
} else if p.config != nil && p.config.Port > 0 {
|
||||
addr = fmt.Sprintf(":%d", p.config.Port)
|
||||
}
|
||||
if addr == "" {
|
||||
return fmt.Errorf("no listen address configured")
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on %s: %w", addr, err)
|
||||
}
|
||||
p.listener = listener
|
||||
|
||||
// Mount Dex at /oauth2/ path for reverse proxy compatibility
|
||||
// Don't strip the prefix - Dex's issuer includes /oauth2 so it expects the full path
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/oauth2/", p.dexServer)
|
||||
|
||||
p.httpServer = &http.Server{Handler: mux}
|
||||
p.running = true
|
||||
|
||||
go func() {
|
||||
if err := p.httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
p.logger.Error("http server error", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start gRPC API server if configured
|
||||
if p.config.GRPCAddr != "" {
|
||||
if err := p.startGRPCServer(); err != nil {
|
||||
// Clean up HTTP server on failure
|
||||
_ = p.httpServer.Close()
|
||||
_ = p.listener.Close()
|
||||
return fmt.Errorf("failed to start gRPC server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
p.logger.Info("HTTP server started", "addr", addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// startGRPCServer starts the gRPC API server using Dex's built-in API
|
||||
func (p *Provider) startGRPCServer() error {
|
||||
grpcListener, err := net.Listen("tcp", p.config.GRPCAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on %s: %w", p.config.GRPCAddr, err)
|
||||
}
|
||||
p.grpcListener = grpcListener
|
||||
|
||||
p.grpcServer = grpc.NewServer()
|
||||
// Use Dex's built-in API server implementation
|
||||
// server.NewAPI(storage, logger, version, dexServer)
|
||||
dexapi.RegisterDexServer(p.grpcServer, server.NewAPI(p.storage, p.logger, "netbird-dex", p.dexServer))
|
||||
|
||||
go func() {
|
||||
if err := p.grpcServer.Serve(grpcListener); err != nil {
|
||||
p.logger.Error("grpc server error", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
p.logger.Info("gRPC API server started", "addr", p.config.GRPCAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down
|
||||
func (p *Provider) Stop(ctx context.Context) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if !p.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
// Stop gRPC server first
|
||||
if p.grpcServer != nil {
|
||||
p.grpcServer.GracefulStop()
|
||||
p.grpcServer = nil
|
||||
}
|
||||
if p.grpcListener != nil {
|
||||
p.grpcListener.Close()
|
||||
p.grpcListener = nil
|
||||
}
|
||||
|
||||
if p.httpServer != nil {
|
||||
if err := p.httpServer.Shutdown(ctx); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly close listener as fallback (Shutdown should do this, but be safe)
|
||||
if p.listener != nil {
|
||||
if err := p.listener.Close(); err != nil {
|
||||
// Ignore "use of closed network connection" - expected after Shutdown
|
||||
if !strings.Contains(err.Error(), "use of closed") {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
p.listener = nil
|
||||
}
|
||||
|
||||
if p.storage != nil {
|
||||
if err := p.storage.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
p.httpServer = nil
|
||||
p.running = false
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("shutdown errors: %v", errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureDefaultClients creates dashboard and CLI OAuth clients
|
||||
// Uses Dex's storage.Client directly - no custom wrappers
|
||||
func (p *Provider) EnsureDefaultClients(ctx context.Context, dashboardURIs, cliURIs []string) error {
|
||||
clients := []storage.Client{
|
||||
{
|
||||
ID: "netbird-dashboard",
|
||||
Name: "NetBird Dashboard",
|
||||
RedirectURIs: dashboardURIs,
|
||||
Public: true,
|
||||
},
|
||||
{
|
||||
ID: "netbird-cli",
|
||||
Name: "NetBird CLI",
|
||||
RedirectURIs: cliURIs,
|
||||
Public: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
_, err := p.storage.GetClient(ctx, client.ID)
|
||||
if err == storage.ErrNotFound {
|
||||
if err := p.storage.CreateClient(ctx, client); err != nil {
|
||||
return fmt.Errorf("failed to create client %s: %w", client.ID, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get client %s: %w", client.ID, err)
|
||||
}
|
||||
// Update if exists
|
||||
if err := p.storage.UpdateClient(ctx, client.ID, func(old storage.Client) (storage.Client, error) {
|
||||
old.RedirectURIs = client.RedirectURIs
|
||||
return old, nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to update client %s: %w", client.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
p.logger.Info("default OIDC clients ensured")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Storage returns the underlying Dex storage for direct access
|
||||
// Users can use storage.Client, storage.Password, storage.Connector directly
|
||||
func (p *Provider) Storage() storage.Storage {
|
||||
return p.storage
|
||||
}
|
||||
|
||||
// Handler returns the Dex server as an http.Handler for embedding in another server.
|
||||
// The handler expects requests with path prefix "/oauth2/".
|
||||
func (p *Provider) Handler() http.Handler {
|
||||
return p.dexServer
|
||||
}
|
||||
|
||||
// CreateUser creates a new user with the given email, username, and password.
|
||||
// Returns the encoded user ID in Dex's format (base64-encoded protobuf with connector ID).
|
||||
func (p *Provider) CreateUser(ctx context.Context, email, username, password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
userID := uuid.New().String()
|
||||
err = p.storage.CreatePassword(ctx, storage.Password{
|
||||
Email: email,
|
||||
Username: username,
|
||||
UserID: userID,
|
||||
Hash: hash,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encode the user ID in Dex's format: base64(protobuf{user_id, connector_id})
|
||||
// This matches the format Dex uses in JWT tokens
|
||||
encodedID := EncodeDexUserID(userID, "local")
|
||||
return encodedID, nil
|
||||
}
|
||||
|
||||
// EncodeDexUserID encodes user ID and connector ID into Dex's base64-encoded protobuf format.
|
||||
// Dex uses this format for the 'sub' claim in JWT tokens.
|
||||
// Format: base64(protobuf message with field 1 = user_id, field 2 = connector_id)
|
||||
func EncodeDexUserID(userID, connectorID string) string {
|
||||
// Manually encode protobuf: field 1 (user_id) and field 2 (connector_id)
|
||||
// Wire type 2 (length-delimited) for strings
|
||||
var buf []byte
|
||||
|
||||
// Field 1: user_id (tag = 0x0a = field 1, wire type 2)
|
||||
buf = append(buf, 0x0a)
|
||||
buf = append(buf, byte(len(userID)))
|
||||
buf = append(buf, []byte(userID)...)
|
||||
|
||||
// Field 2: connector_id (tag = 0x12 = field 2, wire type 2)
|
||||
buf = append(buf, 0x12)
|
||||
buf = append(buf, byte(len(connectorID)))
|
||||
buf = append(buf, []byte(connectorID)...)
|
||||
|
||||
return base64.RawStdEncoding.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// DecodeDexUserID decodes Dex's base64-encoded user ID back to the raw user ID and connector ID.
|
||||
func DecodeDexUserID(encodedID string) (userID, connectorID string, err error) {
|
||||
// Try RawStdEncoding first, then StdEncoding (with padding)
|
||||
buf, err := base64.RawStdEncoding.DecodeString(encodedID)
|
||||
if err != nil {
|
||||
buf, err = base64.StdEncoding.DecodeString(encodedID)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to decode base64: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse protobuf manually
|
||||
i := 0
|
||||
for i < len(buf) {
|
||||
if i >= len(buf) {
|
||||
break
|
||||
}
|
||||
tag := buf[i]
|
||||
i++
|
||||
|
||||
fieldNum := tag >> 3
|
||||
wireType := tag & 0x07
|
||||
|
||||
if wireType != 2 { // We only expect length-delimited strings
|
||||
return "", "", fmt.Errorf("unexpected wire type %d", wireType)
|
||||
}
|
||||
|
||||
if i >= len(buf) {
|
||||
return "", "", fmt.Errorf("truncated message")
|
||||
}
|
||||
length := int(buf[i])
|
||||
i++
|
||||
|
||||
if i+length > len(buf) {
|
||||
return "", "", fmt.Errorf("truncated string field")
|
||||
}
|
||||
value := string(buf[i : i+length])
|
||||
i += length
|
||||
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
userID = value
|
||||
case 2:
|
||||
connectorID = value
|
||||
}
|
||||
}
|
||||
|
||||
return userID, connectorID, nil
|
||||
}
|
||||
|
||||
// GetUser returns a user by email
|
||||
func (p *Provider) GetUser(ctx context.Context, email string) (storage.Password, error) {
|
||||
return p.storage.GetPassword(ctx, email)
|
||||
}
|
||||
|
||||
// GetUserByID returns a user by user ID.
|
||||
// The userID can be either an encoded Dex ID (base64 protobuf) or a raw UUID.
|
||||
// Note: This requires iterating through all users since dex storage doesn't index by userID.
|
||||
func (p *Provider) GetUserByID(ctx context.Context, userID string) (storage.Password, error) {
|
||||
// Try to decode the user ID in case it's encoded
|
||||
rawUserID, _, err := DecodeDexUserID(userID)
|
||||
if err != nil {
|
||||
// If decoding fails, assume it's already a raw UUID
|
||||
rawUserID = userID
|
||||
}
|
||||
|
||||
users, err := p.storage.ListPasswords(ctx)
|
||||
if err != nil {
|
||||
return storage.Password{}, fmt.Errorf("failed to list users: %w", err)
|
||||
}
|
||||
for _, user := range users {
|
||||
if user.UserID == rawUserID {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
return storage.Password{}, storage.ErrNotFound
|
||||
}
|
||||
|
||||
// DeleteUser removes a user by email
|
||||
func (p *Provider) DeleteUser(ctx context.Context, email string) error {
|
||||
return p.storage.DeletePassword(ctx, email)
|
||||
}
|
||||
|
||||
// ListUsers returns all users
|
||||
func (p *Provider) ListUsers(ctx context.Context) ([]storage.Password, error) {
|
||||
return p.storage.ListPasswords(ctx)
|
||||
}
|
||||
|
||||
// ensureLocalConnector creates a local (password) connector if none exists
|
||||
func ensureLocalConnector(ctx context.Context, stor storage.Storage) error {
|
||||
connectors, err := stor.ListConnectors(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list connectors: %w", err)
|
||||
}
|
||||
|
||||
// If any connector exists, we're good
|
||||
if len(connectors) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a local connector for password authentication
|
||||
localConnector := storage.Connector{
|
||||
ID: "local",
|
||||
Type: "local",
|
||||
Name: "Email",
|
||||
}
|
||||
|
||||
if err := stor.CreateConnector(ctx, localConnector); err != nil {
|
||||
return fmt.Errorf("failed to create local connector: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectorConfig represents the configuration for an identity provider connector
|
||||
type ConnectorConfig struct {
|
||||
// ID is the unique identifier for the connector
|
||||
ID string
|
||||
// Name is a human-readable name for the connector
|
||||
Name string
|
||||
// Type is the connector type (oidc, google, microsoft)
|
||||
Type string
|
||||
// Issuer is the OIDC issuer URL (for OIDC-based connectors)
|
||||
Issuer string
|
||||
// ClientID is the OAuth2 client ID
|
||||
ClientID string
|
||||
// ClientSecret is the OAuth2 client secret
|
||||
ClientSecret string
|
||||
// RedirectURI is the OAuth2 redirect URI
|
||||
RedirectURI string
|
||||
}
|
||||
|
||||
// CreateConnector creates a new connector in Dex storage.
|
||||
// It maps the connector config to the appropriate Dex connector type and configuration.
|
||||
func (p *Provider) CreateConnector(ctx context.Context, cfg *ConnectorConfig) (*ConnectorConfig, error) {
|
||||
// Fill in the redirect URI if not provided
|
||||
if cfg.RedirectURI == "" {
|
||||
cfg.RedirectURI = p.GetRedirectURI()
|
||||
}
|
||||
|
||||
storageConn, err := p.buildStorageConnector(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build connector: %w", err)
|
||||
}
|
||||
|
||||
if err := p.storage.CreateConnector(ctx, storageConn); err != nil {
|
||||
return nil, fmt.Errorf("failed to create connector: %w", err)
|
||||
}
|
||||
|
||||
p.logger.Info("connector created", "id", cfg.ID, "type", cfg.Type)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// GetConnector retrieves a connector by ID from Dex storage.
|
||||
func (p *Provider) GetConnector(ctx context.Context, id string) (*ConnectorConfig, error) {
|
||||
conn, err := p.storage.GetConnector(ctx, id)
|
||||
if err != nil {
|
||||
if err == storage.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get connector: %w", err)
|
||||
}
|
||||
|
||||
return p.parseStorageConnector(conn)
|
||||
}
|
||||
|
||||
// ListConnectors returns all connectors from Dex storage (excluding the local connector).
|
||||
func (p *Provider) ListConnectors(ctx context.Context) ([]*ConnectorConfig, error) {
|
||||
connectors, err := p.storage.ListConnectors(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list connectors: %w", err)
|
||||
}
|
||||
|
||||
result := make([]*ConnectorConfig, 0, len(connectors))
|
||||
for _, conn := range connectors {
|
||||
// Skip the local password connector
|
||||
if conn.ID == "local" && conn.Type == "local" {
|
||||
continue
|
||||
}
|
||||
|
||||
cfg, err := p.parseStorageConnector(conn)
|
||||
if err != nil {
|
||||
p.logger.Warn("failed to parse connector", "id", conn.ID, "error", err)
|
||||
continue
|
||||
}
|
||||
result = append(result, cfg)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UpdateConnector updates an existing connector in Dex storage.
|
||||
func (p *Provider) UpdateConnector(ctx context.Context, cfg *ConnectorConfig) error {
|
||||
storageConn, err := p.buildStorageConnector(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build connector: %w", err)
|
||||
}
|
||||
|
||||
if err := p.storage.UpdateConnector(ctx, cfg.ID, func(old storage.Connector) (storage.Connector, error) {
|
||||
return storageConn, nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to update connector: %w", err)
|
||||
}
|
||||
|
||||
p.logger.Info("connector updated", "id", cfg.ID, "type", cfg.Type)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteConnector removes a connector from Dex storage.
|
||||
func (p *Provider) DeleteConnector(ctx context.Context, id string) error {
|
||||
// Prevent deletion of the local connector
|
||||
if id == "local" {
|
||||
return fmt.Errorf("cannot delete the local password connector")
|
||||
}
|
||||
|
||||
if err := p.storage.DeleteConnector(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete connector: %w", err)
|
||||
}
|
||||
|
||||
p.logger.Info("connector deleted", "id", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildStorageConnector creates a storage.Connector from ConnectorConfig.
|
||||
// It handles the type-specific configuration for each connector type.
|
||||
func (p *Provider) buildStorageConnector(cfg *ConnectorConfig) (storage.Connector, error) {
|
||||
var configData []byte
|
||||
var dexType string
|
||||
|
||||
// Determine the redirect URI - default to oauth2 callback
|
||||
redirectURI := cfg.RedirectURI
|
||||
if redirectURI == "" && p.config != nil {
|
||||
issuer := strings.TrimSuffix(p.config.Issuer, "/")
|
||||
if !strings.HasSuffix(issuer, "/oauth2") {
|
||||
issuer = issuer + "/oauth2"
|
||||
}
|
||||
redirectURI = issuer + "/callback"
|
||||
}
|
||||
|
||||
switch cfg.Type {
|
||||
case "oidc", "zitadel", "entra", "okta", "pocketid":
|
||||
// All these types use the OIDC connector in Dex
|
||||
dexType = "oidc"
|
||||
oidcConfig := map[string]interface{}{
|
||||
"issuer": cfg.Issuer,
|
||||
"clientID": cfg.ClientID,
|
||||
"clientSecret": cfg.ClientSecret,
|
||||
"redirectURI": redirectURI,
|
||||
"scopes": []string{"openid", "profile", "email"},
|
||||
}
|
||||
// Type-specific configurations
|
||||
switch cfg.Type {
|
||||
case "zitadel":
|
||||
oidcConfig["getUserInfo"] = true
|
||||
case "entra":
|
||||
oidcConfig["insecureSkipEmailVerified"] = true
|
||||
oidcConfig["claimMapping"] = map[string]string{
|
||||
"email": "preferred_username",
|
||||
}
|
||||
case "okta":
|
||||
oidcConfig["insecureSkipEmailVerified"] = true
|
||||
}
|
||||
var err error
|
||||
configData, err = encodeConnectorConfig(oidcConfig)
|
||||
if err != nil {
|
||||
return storage.Connector{}, err
|
||||
}
|
||||
|
||||
case "google":
|
||||
dexType = "google"
|
||||
googleConfig := map[string]interface{}{
|
||||
"clientID": cfg.ClientID,
|
||||
"clientSecret": cfg.ClientSecret,
|
||||
"redirectURI": redirectURI,
|
||||
}
|
||||
var err error
|
||||
configData, err = encodeConnectorConfig(googleConfig)
|
||||
if err != nil {
|
||||
return storage.Connector{}, err
|
||||
}
|
||||
|
||||
case "microsoft":
|
||||
dexType = "microsoft"
|
||||
msConfig := map[string]interface{}{
|
||||
"clientID": cfg.ClientID,
|
||||
"clientSecret": cfg.ClientSecret,
|
||||
"redirectURI": redirectURI,
|
||||
}
|
||||
var err error
|
||||
configData, err = encodeConnectorConfig(msConfig)
|
||||
if err != nil {
|
||||
return storage.Connector{}, err
|
||||
}
|
||||
|
||||
default:
|
||||
return storage.Connector{}, fmt.Errorf("unsupported connector type: %s", cfg.Type)
|
||||
}
|
||||
|
||||
return storage.Connector{
|
||||
ID: cfg.ID,
|
||||
Type: dexType,
|
||||
Name: cfg.Name,
|
||||
Config: configData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseStorageConnector converts a storage.Connector back to ConnectorConfig.
|
||||
// It infers the original identity provider type from the Dex connector type and ID.
|
||||
func (p *Provider) parseStorageConnector(conn storage.Connector) (*ConnectorConfig, error) {
|
||||
cfg := &ConnectorConfig{
|
||||
ID: conn.ID,
|
||||
Name: conn.Name,
|
||||
}
|
||||
|
||||
if len(conn.Config) == 0 {
|
||||
cfg.Type = conn.Type
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
var configMap map[string]interface{}
|
||||
if err := decodeConnectorConfig(conn.Config, &configMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse connector config: %w", err)
|
||||
}
|
||||
|
||||
// Extract common fields
|
||||
if v, ok := configMap["clientID"].(string); ok {
|
||||
cfg.ClientID = v
|
||||
}
|
||||
if v, ok := configMap["clientSecret"].(string); ok {
|
||||
cfg.ClientSecret = v
|
||||
}
|
||||
if v, ok := configMap["redirectURI"].(string); ok {
|
||||
cfg.RedirectURI = v
|
||||
}
|
||||
if v, ok := configMap["issuer"].(string); ok {
|
||||
cfg.Issuer = v
|
||||
}
|
||||
|
||||
// Infer the original identity provider type from Dex connector type and ID
|
||||
cfg.Type = inferIdentityProviderType(conn.Type, conn.ID, configMap)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// inferIdentityProviderType determines the original identity provider type
|
||||
// based on the Dex connector type, connector ID, and configuration.
|
||||
func inferIdentityProviderType(dexType, connectorID string, config map[string]interface{}) string {
|
||||
connectorIDLower := strings.ToLower(connectorID)
|
||||
|
||||
switch dexType {
|
||||
case "oidc":
|
||||
// Check connector ID for specific provider hints
|
||||
switch {
|
||||
case strings.Contains(connectorIDLower, "pocketid"):
|
||||
return "pocketid"
|
||||
case strings.Contains(connectorIDLower, "zitadel"):
|
||||
return "zitadel"
|
||||
case strings.Contains(connectorIDLower, "entra"):
|
||||
return "entra"
|
||||
case strings.Contains(connectorIDLower, "okta"):
|
||||
return "okta"
|
||||
default:
|
||||
return "oidc"
|
||||
}
|
||||
case "google":
|
||||
return "google"
|
||||
case "microsoft":
|
||||
return "microsoft"
|
||||
default:
|
||||
return dexType
|
||||
}
|
||||
}
|
||||
|
||||
// encodeConnectorConfig serializes connector config to JSON bytes.
|
||||
func encodeConnectorConfig(config map[string]interface{}) ([]byte, error) {
|
||||
return json.Marshal(config)
|
||||
}
|
||||
|
||||
// decodeConnectorConfig deserializes connector config from JSON bytes.
|
||||
func decodeConnectorConfig(data []byte, v interface{}) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// GetRedirectURI returns the default redirect URI for connectors.
|
||||
func (p *Provider) GetRedirectURI() string {
|
||||
if p.config == nil {
|
||||
return ""
|
||||
}
|
||||
issuer := strings.TrimSuffix(p.config.Issuer, "/")
|
||||
if !strings.HasSuffix(issuer, "/oauth2") {
|
||||
issuer = issuer + "/oauth2"
|
||||
}
|
||||
return issuer + "/callback"
|
||||
}
|
||||
|
||||
// GetIssuer returns the OIDC issuer URL.
|
||||
func (p *Provider) GetIssuer() string {
|
||||
if p.config == nil {
|
||||
return ""
|
||||
}
|
||||
issuer := strings.TrimSuffix(p.config.Issuer, "/")
|
||||
if !strings.HasSuffix(issuer, "/oauth2") {
|
||||
issuer = issuer + "/oauth2"
|
||||
}
|
||||
return issuer
|
||||
}
|
||||
|
||||
// GetKeysLocation returns the JWKS endpoint URL for token validation.
|
||||
func (p *Provider) GetKeysLocation() string {
|
||||
issuer := p.GetIssuer()
|
||||
if issuer == "" {
|
||||
return ""
|
||||
}
|
||||
return issuer + "/keys"
|
||||
}
|
||||
|
||||
// GetClientIDs returns the OAuth2 client IDs configured for this provider.
|
||||
func (p *Provider) GetClientIDs() []string {
|
||||
if p.yamlConfig != nil && len(p.yamlConfig.StaticClients) > 0 {
|
||||
clientIDs := make([]string, 0, len(p.yamlConfig.StaticClients))
|
||||
for _, client := range p.yamlConfig.StaticClients {
|
||||
clientIDs = append(clientIDs, client.ID)
|
||||
}
|
||||
return clientIDs
|
||||
}
|
||||
// Default client IDs if not configured via YAML
|
||||
return []string{"netbird-dashboard", "netbird-cli"}
|
||||
}
|
||||
|
||||
func (p *Provider) GetUserIDClaim() string {
|
||||
return "sub"
|
||||
}
|
||||
197
idp/dex/provider_test.go
Normal file
197
idp/dex/provider_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package dex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserCreationFlow(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a temporary directory for the test
|
||||
tmpDir, err := os.MkdirTemp("", "dex-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create provider with minimal config
|
||||
config := &Config{
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Port: 5556,
|
||||
DataDir: tmpDir,
|
||||
}
|
||||
|
||||
provider, err := NewProvider(ctx, config)
|
||||
require.NoError(t, err)
|
||||
defer provider.Stop(ctx)
|
||||
|
||||
// Test user data
|
||||
email := "test@example.com"
|
||||
username := "testuser"
|
||||
password := "testpassword123"
|
||||
|
||||
// Create the user
|
||||
encodedID, err := provider.CreateUser(ctx, email, username, password)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, encodedID)
|
||||
|
||||
t.Logf("Created user with encoded ID: %s", encodedID)
|
||||
|
||||
// Verify the encoded ID can be decoded
|
||||
rawUserID, connectorID, err := DecodeDexUserID(encodedID)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rawUserID)
|
||||
assert.Equal(t, "local", connectorID)
|
||||
|
||||
t.Logf("Decoded: rawUserID=%s, connectorID=%s", rawUserID, connectorID)
|
||||
|
||||
// Verify we can look up the user by encoded ID
|
||||
user, err := provider.GetUserByID(ctx, encodedID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, email, user.Email)
|
||||
assert.Equal(t, username, user.Username)
|
||||
assert.Equal(t, rawUserID, user.UserID)
|
||||
|
||||
// Verify we can also look up by raw UUID (backwards compatibility)
|
||||
user2, err := provider.GetUserByID(ctx, rawUserID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, email, user2.Email)
|
||||
|
||||
// Verify we can look up by email
|
||||
user3, err := provider.GetUser(ctx, email)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rawUserID, user3.UserID)
|
||||
|
||||
// Verify encoding produces consistent format
|
||||
reEncodedID := EncodeDexUserID(rawUserID, "local")
|
||||
assert.Equal(t, encodedID, reEncodedID)
|
||||
}
|
||||
|
||||
func TestDecodeDexUserID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
encodedID string
|
||||
wantUserID string
|
||||
wantConnID string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid encoded ID",
|
||||
encodedID: "CiQ3YWFkOGMwNS0zMjg3LTQ3M2YtYjQyYS0zNjU1MDRiZjI1ZTcSBWxvY2Fs",
|
||||
wantUserID: "7aad8c05-3287-473f-b42a-365504bf25e7",
|
||||
wantConnID: "local",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid base64",
|
||||
encodedID: "not-valid-base64!!!",
|
||||
wantUserID: "",
|
||||
wantConnID: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
userID, connID, err := DecodeDexUserID(tt.encodedID)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantUserID, userID)
|
||||
assert.Equal(t, tt.wantConnID, connID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeDexUserID(t *testing.T) {
|
||||
userID := "7aad8c05-3287-473f-b42a-365504bf25e7"
|
||||
connectorID := "local"
|
||||
|
||||
encoded := EncodeDexUserID(userID, connectorID)
|
||||
assert.NotEmpty(t, encoded)
|
||||
|
||||
// Verify round-trip
|
||||
decodedUserID, decodedConnID, err := DecodeDexUserID(encoded)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, userID, decodedUserID)
|
||||
assert.Equal(t, connectorID, decodedConnID)
|
||||
}
|
||||
|
||||
func TestEncodeDexUserID_MatchesDexFormat(t *testing.T) {
|
||||
// This is an actual ID from Dex - verify our encoding matches
|
||||
knownEncodedID := "CiQ3YWFkOGMwNS0zMjg3LTQ3M2YtYjQyYS0zNjU1MDRiZjI1ZTcSBWxvY2Fs"
|
||||
knownUserID := "7aad8c05-3287-473f-b42a-365504bf25e7"
|
||||
knownConnectorID := "local"
|
||||
|
||||
// Decode the known ID
|
||||
userID, connID, err := DecodeDexUserID(knownEncodedID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, knownUserID, userID)
|
||||
assert.Equal(t, knownConnectorID, connID)
|
||||
|
||||
// Re-encode and verify it matches
|
||||
reEncoded := EncodeDexUserID(knownUserID, knownConnectorID)
|
||||
assert.Equal(t, knownEncodedID, reEncoded)
|
||||
}
|
||||
|
||||
func TestCreateUserInTempDB(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create temp directory
|
||||
tmpDir, err := os.MkdirTemp("", "dex-create-user-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create YAML config for the test
|
||||
yamlContent := `
|
||||
issuer: http://localhost:5556/dex
|
||||
storage:
|
||||
type: sqlite3
|
||||
config:
|
||||
file: ` + filepath.Join(tmpDir, "dex.db") + `
|
||||
web:
|
||||
http: 127.0.0.1:5556
|
||||
enablePasswordDB: true
|
||||
`
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
err = os.WriteFile(configPath, []byte(yamlContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Load config and create provider
|
||||
yamlConfig, err := LoadConfig(configPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider, err := NewProviderFromYAML(ctx, yamlConfig)
|
||||
require.NoError(t, err)
|
||||
defer provider.Stop(ctx)
|
||||
|
||||
// Create user
|
||||
email := "newuser@example.com"
|
||||
username := "newuser"
|
||||
password := "securepassword123"
|
||||
|
||||
encodedID, err := provider.CreateUser(ctx, email, username, password)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("Created user: email=%s, encodedID=%s", email, encodedID)
|
||||
|
||||
// Verify lookup works with encoded ID
|
||||
user, err := provider.GetUserByID(ctx, encodedID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, email, user.Email)
|
||||
assert.Equal(t, username, user.Username)
|
||||
|
||||
// Decode and verify format
|
||||
rawID, connID, err := DecodeDexUserID(encodedID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "local", connID)
|
||||
assert.Equal(t, rawID, user.UserID)
|
||||
|
||||
t.Logf("User lookup successful: rawID=%s, connectorID=%s", rawID, connID)
|
||||
}
|
||||
2
idp/dex/web/robots.txt
Executable file
2
idp/dex/web/robots.txt
Executable file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
1
idp/dex/web/static/main.css
Executable file
1
idp/dex/web/static/main.css
Executable file
@@ -0,0 +1 @@
|
||||
/* NetBird DEX Static CSS - main styles are inline in header.html */
|
||||
26
idp/dex/web/templates/approval.html
Executable file
26
idp/dex/web/templates/approval.html
Executable file
@@ -0,0 +1,26 @@
|
||||
{{ template "header.html" . }}
|
||||
|
||||
<div class="nb-card">
|
||||
<h1 class="nb-heading">Grant Access</h1>
|
||||
<p class="nb-subheading">{{ .Client }} wants to access your account</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="req" value="{{ .ReqID }}"/>
|
||||
<input type="hidden" name="approval" value="approve"/>
|
||||
<button type="submit" class="nb-btn">
|
||||
Allow Access
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="nb-divider"></div>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="req" value="{{ .ReqID }}"/>
|
||||
<input type="hidden" name="approval" value="rejected"/>
|
||||
<button type="submit" class="nb-btn-connector" style="margin-bottom:0">
|
||||
Deny Access
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{ template "footer.html" . }}
|
||||
34
idp/dex/web/templates/device.html
Executable file
34
idp/dex/web/templates/device.html
Executable file
@@ -0,0 +1,34 @@
|
||||
{{ template "header.html" . }}
|
||||
|
||||
<div class="nb-card">
|
||||
<h1 class="nb-heading">Device Login</h1>
|
||||
<p class="nb-subheading">Enter the code shown on your device</p>
|
||||
|
||||
<form method="post" action="{{ .PostURL }}">
|
||||
{{ if .Invalid }}
|
||||
<div class="nb-error">
|
||||
Invalid user code.
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="nb-form-group">
|
||||
<label class="nb-label" for="user_code">Device Code</label>
|
||||
<input
|
||||
type="text"
|
||||
id="user_code"
|
||||
name="user_code"
|
||||
class="nb-input"
|
||||
placeholder="XXXX-XXXX"
|
||||
{{ if .UserCode }}value="{{ .UserCode }}"{{ end }}
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="nb-btn">
|
||||
Continue
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{ template "footer.html" . }}
|
||||
16
idp/dex/web/templates/device_success.html
Executable file
16
idp/dex/web/templates/device_success.html
Executable file
@@ -0,0 +1,16 @@
|
||||
{{ template "header.html" . }}
|
||||
|
||||
<div class="nb-card">
|
||||
<div style="text-align:center;margin-bottom:24px">
|
||||
<svg height="48" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" fill="none" r="45" stroke="#5cb85c" stroke-width="3"/>
|
||||
<path d="M30 50 L45 65 L70 35" fill="none" stroke="#5cb85c" stroke-width="5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="nb-heading">Device Authorized</h1>
|
||||
<p class="nb-subheading">
|
||||
Your device has been successfully authorized. You can close this window.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{ template "footer.html" . }}
|
||||
16
idp/dex/web/templates/error.html
Executable file
16
idp/dex/web/templates/error.html
Executable file
@@ -0,0 +1,16 @@
|
||||
{{ template "header.html" . }}
|
||||
|
||||
<div class="nb-card">
|
||||
<div style="text-align:center;margin-bottom:24px">
|
||||
<svg height="48" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" fill="none" r="45" stroke="#f87171" stroke-width="3"/>
|
||||
<path d="M30 30 L70 70 M30 70 L70 30" fill="none" stroke="#f87171" stroke-width="3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="nb-heading">{{ .ErrType }}</h1>
|
||||
<div class="nb-error">
|
||||
{{ .ErrMsg }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ template "footer.html" . }}
|
||||
3
idp/dex/web/templates/footer.html
Executable file
3
idp/dex/web/templates/footer.html
Executable file
@@ -0,0 +1,3 @@
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
67
idp/dex/web/templates/header.html
Executable file
67
idp/dex/web/templates/header.html
Executable file
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>{{ issuer }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/x-icon" href="{{ url .ReqPath "theme/favicon.ico" }}">
|
||||
<style>
|
||||
*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}
|
||||
html,body{margin:0;padding:0;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji;font-size:14px;line-height:1.5;background-color:#18191d;color:#e4e7e9;min-height:100vh}
|
||||
.nb-container{max-width:820px;margin:0 auto;padding:40px 20px;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh}
|
||||
.nb-logo{width:180px;margin-bottom:40px}
|
||||
.nb-card{background-color:#1b1f22;border:1px solid rgba(50,54,61,.5);border-radius:12px;padding:40px;width:100%;max-width:400px;box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1)}
|
||||
.nb-heading{font-size:24px;font-weight:500;text-align:center;margin:0 0 24px 0;color:#fff}
|
||||
.nb-subheading{font-size:14px;color:rgba(167,177,185,.8);text-align:center;margin-bottom:24px}
|
||||
.nb-form-group{margin-bottom:16px}
|
||||
.nb-label{display:block;font-size:13px;font-weight:500;color:#a7b1b9;margin-bottom:6px}
|
||||
.nb-input{width:100%;padding:10px 14px;background-color:rgba(63,68,75,.5);border:1px solid rgba(63,68,75,.8);border-radius:8px;color:#e4e7e9;font-size:14px;outline:none;transition:border-color .2s}
|
||||
.nb-input:focus{border-color:#f68330}
|
||||
.nb-input::placeholder{color:rgba(167,177,185,.5)}
|
||||
.nb-btn{width:100%;padding:12px 20px;background-color:#f68330;border:none;border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}
|
||||
.nb-btn:hover{background-color:#e5722a}
|
||||
.nb-btn:disabled{opacity:.6;cursor:not-allowed}
|
||||
.nb-btn-connector{width:100%;padding:12px 20px;background-color:rgba(63,68,75,.5);border:1px solid rgba(63,68,75,.8);border-radius:8px;color:#e4e7e9;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:flex-start;text-decoration:none;margin-bottom:12px;gap:12px}
|
||||
.nb-btn-connector:hover{background-color:rgba(63,68,75,.8);border-color:rgba(63,68,75,1)}
|
||||
.nb-btn-connector .nb-icon{width:20px;height:20px;flex-shrink:0;background-size:contain;background-position:center;background-repeat:no-repeat}
|
||||
.nb-icon-google{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%23FFC107' d='M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z'/%3E%3Cpath fill='%23FF3D00' d='m6.306 14.691 6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z'/%3E%3Cpath fill='%234CAF50' d='M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.91 11.91 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z'/%3E%3Cpath fill='%231976D2' d='M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002 6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z'/%3E%3C/svg%3E")}
|
||||
.nb-icon-github{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1024' height='1024' fill='none'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28s87.04 5.76 128 17.28c97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0' clip-rule='evenodd'/%3E%3C/svg%3E")}
|
||||
.nb-icon-microsoft{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='221' height='221'%3E%3Cg fill='none'%3E%3Cpath fill='%23F1511B' d='M104.868 104.868H0V0h104.868z'/%3E%3Cpath fill='%2380CC28' d='M220.654 104.868H115.788V0h104.866z'/%3E%3Cpath fill='%2300ADEF' d='M104.865 220.695H0V115.828h104.865z'/%3E%3Cpath fill='%23FBBC09' d='M220.654 220.695H115.788V115.828h104.866z'/%3E%3C/g%3E%3C/svg%3E")}
|
||||
.nb-icon-azure{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150' viewBox='0 0 96 96'%3E%3Cdefs%3E%3ClinearGradient id='a' x1='-1032.172' x2='-1059.213' y1='145.312' y2='65.426' gradientTransform='matrix(1 0 0 -1 1075 158)' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23114a8b'/%3E%3Cstop offset='1' stop-color='%230669bc'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' x1='-1023.725' x2='-1029.98' y1='108.083' y2='105.968' gradientTransform='matrix(1 0 0 -1 1075 158)' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-opacity='.3'/%3E%3Cstop offset='.071' stop-opacity='.2'/%3E%3Cstop offset='.321' stop-opacity='.1'/%3E%3Cstop offset='.623' stop-opacity='.05'/%3E%3Cstop offset='1' stop-opacity='0'/%3E%3C/linearGradient%3E%3ClinearGradient id='c' x1='-1027.165' x2='-997.482' y1='147.642' y2='68.561' gradientTransform='matrix(1 0 0 -1 1075 158)' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%233ccbf4'/%3E%3Cstop offset='1' stop-color='%232892df'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath fill='url(%23a)' d='M33.338 6.544h26.038l-27.03 80.087a4.15 4.15 0 0 1-3.933 2.824H8.149a4.145 4.145 0 0 1-3.928-5.47L29.404 9.368a4.15 4.15 0 0 1 3.934-2.825z'/%3E%3Cpath fill='%230078d4' d='M71.175 60.261h-41.29a1.911 1.911 0 0 0-1.305 3.309l26.532 24.764a4.17 4.17 0 0 0 2.846 1.121h23.38z'/%3E%3Cpath fill='url(%23b)' d='M33.338 6.544a4.12 4.12 0 0 0-3.943 2.879L4.252 83.917a4.14 4.14 0 0 0 3.908 5.538h20.787a4.44 4.44 0 0 0 3.41-2.9l5.014-14.777 17.91 16.705a4.24 4.24 0 0 0 2.666.972H81.24L71.024 60.261l-29.781.007L59.47 6.544z'/%3E%3Cpath fill='url(%23c)' d='M66.595 9.364a4.145 4.145 0 0 0-3.928-2.82H33.648a4.15 4.15 0 0 1 3.928 2.82l25.184 74.62a4.146 4.146 0 0 1-3.928 5.472h29.02a4.146 4.146 0 0 0 3.927-5.472z'/%3E%3C/svg%3E")}
|
||||
.nb-icon-entra{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' data-name='Layer 1'%3E%3Cpath fill='%23225086' d='M3.802 14.032c.388.242 1.033.511 1.715.511.621 0 1.198-.18 1.676-.487l.002-.001L9 12.927V17a1.56 1.56 0 0 1-.824-.234z'/%3E%3Cpath fill='%236df' d='m7.853 1.507-7.5 8.46c-.579.654-.428 1.642.323 2.111l3.126 1.954c.388.242 1.033.511 1.715.511.621 0 1.198-.18 1.676-.487l.002-.001L9 12.927l-4.364-2.728 4.365-4.924V1c-.424 0-.847.169-1.147.507Z'/%3E%3Cpath fill='%23cbf8ff' d='m4.636 10.199.052.032L9 12.927h.001V5.276L9 5.275z'/%3E%3Cpath fill='%23074793' d='M17.324 12.078c.751-.469.902-1.457.323-2.111l-4.921-5.551a3.1 3.1 0 0 0-1.313-.291c-.925 0-1.752.399-2.302 1.026l-.109.123 4.364 4.924-4.365 2.728v4.073c.287 0 .573-.078.823-.234l7.5-4.688Z'/%3E%3Cpath fill='%230294e4' d='M9.001 1v4.275l.109-.123a3.05 3.05 0 0 1 2.302-1.026c.472 0 .916.107 1.313.291l-2.579-2.909A1.52 1.52 0 0 0 9 1.001Z'/%3E%3Cpath fill='%2396bcc2' d='M13.365 10.199 9.001 5.276v7.65z'/%3E%3C/svg%3E")}
|
||||
.nb-icon-okta{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36' fill='none'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='m19.8.26-.74 9.12c-.35-.04-.7-.06-1.06-.06-.45 0-.89.03-1.32.1L16.26 5c-.01-.14.1-.26.24-.26h.75L16.89.27c-.01-.14.1-.26.23-.26h2.45c.14 0 .25.12.23.26zm-6.18.45c-.04-.13-.18-.21-.31-.16l-2.3.84c-.13.05-.19.2-.13.32l1.87 4.08-.71.26c-.13.05-.19.2-.13.32l1.91 4.01c.69-.38 1.44-.67 2.23-.85L13.63.71zM7.98 3.25l5.29 7.46c-.67.44-1.28.96-1.8 1.56L8.3 9.15c-.1-.1-.09-.26.01-.35l.58-.48-3.15-3.19c-.1-.1-.09-.26.02-.35l1.87-1.57c.11-.09.26-.07.34.04zM3.54 7.57c-.11-.08-.27-.04-.34.08L1.98 9.77c-.07.12-.02.27.1.33l4.06 1.92-.38.65a.23.23 0 0 0 .11.33l4.04 1.85c.29-.75.68-1.45 1.16-2.08zM.55 13.33c.02-.14.16-.22.29-.19l8.85 2.31c-.23.75-.36 1.54-.38 2.36l-4.43-.36a.23.23 0 0 1-.21-.28l.13-.74-4.47-.42c-.14-.01-.23-.14-.21-.28l.42-2.41zm-.33 5.98c-.14.01-.23.14-.21.28L.44 22c.02.14.16.22.29.19l4.34-1.13.13.74c.02.14.16.22.29.19l4.28-1.18c-.25-.74-.41-1.53-.45-2.34l-9.11.84zm1.42 6.34a.236.236 0 0 1 .1-.33L10 21.4c.31.74.73 1.43 1.23 2.05l-3.62 2.58c-.11.08-.27.05-.34-.07l-.38-.66-3.69 2.55c-.11.08-.27.04-.34-.08l-1.23-2.12zm10.01-1.72-6.43 6.51c-.1.1-.09.26.02.35l1.88 1.57c.11.09.26.07.34-.04l2.6-3.66.58.49c.11.09.27.07.35-.05l2.52-3.66c-.68-.42-1.31-.93-1.85-1.51zm-1.27 10.45a.234.234 0 0 1-.13-.32l3.81-8.32c.7.36 1.46.63 2.25.78l-1.12 4.3c-.03.13-.18.21-.31.16l-.71-.26-1.19 4.33c-.04.13-.18.21-.31.16l-2.3-.84zm6.56-7.75-.74 9.12c-.01.14.1.26.23.26h2.45c.14 0 .25-.12.23-.26l-.36-4.47h.75c.14 0 .25-.12.24-.26l-.42-4.42c-.43.07-.87.1-1.32.1-.36 0-.71-.02-1.06-.07m8.82-24.69c.06-.13 0-.27-.13-.32l-2.3-.84c-.13-.05-.27.03-.31.16l-1.19 4.33-.71-.26c-.13-.05-.27.03-.31.16l-1.12 4.3c.8.16 1.55.43 2.25.78zm5.02 3.63-6.43 6.51a8.7 8.7 0 0 0-1.85-1.51l2.52-3.66c.08-.11.24-.14.35-.05l.58.49 2.6-3.66c.08-.11.24-.13.34-.04l1.88 1.57c.11.09.11.25.02.35zm3.48 5.12c.13-.06.17-.21.1-.33l-1.23-2.12a.246.246 0 0 0-.34-.08l-3.69 2.55-.38-.65c-.07-.12-.23-.16-.34-.07l-3.62 2.58c.5.62.91 1.31 1.23 2.05l8.26-3.92zm1.3 3.32.42 2.41c.02.14-.07.26-.21.28l-9.11.85c-.04-.82-.2-1.6-.45-2.34l4.28-1.18c.13-.04.27.05.29.19l.13.74 4.34-1.13c.13-.03.27.05.29.19zm-.41 8.85c.13.03.27-.05.29-.19l.42-2.41a.24.24 0 0 0-.21-.28l-4.47-.42.13-.74a.24.24 0 0 0-.21-.28l-4.43-.36c-.02.82-.15 1.61-.38 2.36l8.85 2.31zm-2.36 5.5c-.07.12-.23.15-.34.08l-7.53-5.2c.48-.63.87-1.33 1.16-2.08l4.04 1.85c.13.06.18.21.11.33l-.38.65 4.06 1.92c.12.06.17.21.1.33zm-10.07-3.07 5.29 7.46c.08.11.24.13.34.04l1.87-1.57c.11-.09.11-.25.02-.35l-3.15-3.19.58-.48c.11-.09.11-.25.01-.35l-3.17-3.12c-.53.6-1.13 1.13-1.8 1.56zm-.05 10.16c-.13.05-.27-.03-.31-.16l-2.42-8.82c.79-.18 1.54-.47 2.23-.85l1.91 4.01c.06.13 0 .28-.13.32l-.71.26 1.87 4.08c.06.13 0 .27-.13.32l-2.3.84z' clip-rule='evenodd'/%3E%3C/svg%3E")}
|
||||
.nb-icon-jumpcloud{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='168' height='82' fill='none'%3E%3Cpath fill='%23fff' d='M167.627 58.455a22.705 22.705 0 0 1-22.707 22.707h-6.243c-.651-7.57-8.461-14.005-19.501-16.994a19.72 19.72 0 0 0 4.394-21.52 19.72 19.72 0 0 0-18.243-12.235 19.718 19.718 0 0 0-13.848 33.755 34.3 34.3 0 0 0-14.246 7.231 34.3 34.3 0 0 0-8.268-3.398 16.874 16.874 0 1 0-23.623 0C36.64 70.41 30.3 75.232 28.95 81.065h-6.243a22.73 22.73 0 0 1 0-45.438c2.89.01 5.753.567 8.437 1.64A22.66 22.66 0 0 1 51.85 24.08h1.64a29.601 29.601 0 0 1 54.429-9.642 24.1 24.1 0 0 1 21.003 3.439 24.11 24.11 0 0 1 10.092 18.738 22.66 22.66 0 0 1 28.613 21.935z'/%3E%3C/svg%3E")}
|
||||
.nb-icon-pocketid{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Ccircle cx='256' cy='256' r='256' fill='%23fff'/%3E%3Cpath d='M268.6 102.4c64.4 0 116.8 52.4 116.8 116.7 0 25.3-8 49.4-23 69.6-14.8 19.9-35 34.3-58.4 41.7l-6.5 2-15.5-76.2 4.3-2c14-6.7 23-21.1 23-36.6 0-22.4-18.2-40.6-40.6-40.6S228 195.2 228 217.6c0 15.5 9 29.8 23 36.6l4.2 2-25 153.4h-69.5V102.4z' fill='%23191919'/%3E%3C/svg%3E")}
|
||||
.nb-icon-zitadel{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='79' viewBox='0 0 80 79' fill='none'%3E%3Cdefs%3E%3ClinearGradient id='a' x1='3.86' x2='76.88' y1='47.89' y2='47.89' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-color='%23FF8F00'/%3E%3Cstop offset='1' stop-color='%23FE00FF'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath fill='url(%23a)' fill-rule='evenodd' d='M17.12 39.17l1.42 5.32-6.68 6.68 9.12 2.44 1.43 5.32-19.77-5.3L17.12 39.17zM58.82 22.41l-5.32-1.43-2.44-9.12-6.68 6.68-5.32-1.43 14.47-14.47 5.3 19.77zM52.65 67.11l3.89-3.89 9.12 2.44-2.44-9.12 3.9-3.9 5.29 19.77-19.76-5.3zM36.43 69.54l-1.18-12.07 8.23 2.21-7.05 9.86zM23 23.84l5.02 11.04 6.02-6.02L23 23.84zM69.32 36.2l-12.07-1.18 2.2 8.23 9.87-7.05z' clip-rule='evenodd'/%3E%3C/svg%3E")}
|
||||
.nb-icon-default{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23a7b1b9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4'/%3E%3Cpolyline points='10 17 15 12 10 7'/%3E%3Cline x1='15' y1='12' x2='3' y2='12'/%3E%3C/svg%3E")}
|
||||
.nb-error{background-color:rgba(153,27,27,.2);border:1px solid rgba(153,27,27,.5);border-radius:8px;padding:12px 16px;color:#f87171;font-size:13px;text-align:center;margin-bottom:16px}
|
||||
.nb-link{color:#f68330;text-decoration:none;font-size:13px}
|
||||
.nb-link:hover{text-decoration:underline}
|
||||
.nb-back-link{text-align:center;margin-top:20px}
|
||||
.nb-divider{height:1px;background-color:rgba(63,68,75,.5);margin:24px 0}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="nb-container">
|
||||
<div class="nb-logo">
|
||||
<svg width="180" height="31" viewBox="0 0 133 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M46.9438 7.5013C48.1229 8.64688 48.7082 10.3025 48.7082 12.4683V21.6663H46.1411V12.8362C46.1411 11.2809 45.7481 10.0851 44.9704 9.26566C44.1928 8.43783 43.1308 8.0281 41.7846 8.0281C40.4383 8.0281 39.3345 8.45455 38.5234 9.30747C37.7123 10.1604 37.3109 11.4063 37.3109 13.0369V21.6663H34.7188V6.06305H37.3109V8.28732C37.821 7.49294 38.5234 6.87416 39.4014 6.43934C40.2878 6.00452 41.2578 5.78711 42.3197 5.78711C44.2179 5.78711 45.7565 6.36408 46.9355 7.50966L46.9438 7.5013Z" fill="#F2F2F2"/>
|
||||
<path d="M67.1048 14.8344H54.6288C54.7208 16.373 55.2476 17.5771 56.2092 18.4384C57.1708 19.2997 58.3331 19.7345 59.6961 19.7345C60.8166 19.7345 61.7531 19.4753 62.4973 18.9485C63.2499 18.4301 63.7767 17.7277 64.0777 16.858H66.8706C66.4525 18.3548 65.6163 19.5756 64.3621 20.5205C63.1078 21.4571 61.5525 21.9337 59.6878 21.9337C58.2077 21.9337 56.8865 21.5992 55.7159 20.9386C54.5452 20.278 53.6337 19.3331 52.9648 18.1039C52.2958 16.8831 51.9697 15.4616 51.9697 13.8477C51.9697 12.2339 52.2958 10.8207 52.9397 9.60825C53.5836 8.39578 54.495 7.45924 55.6573 6.80702C56.828 6.15479 58.1659 5.82031 59.6878 5.82031C61.2096 5.82031 62.4806 6.14643 63.6178 6.79029C64.7551 7.43416 65.6331 8.32052 66.2518 9.44938C66.8706 10.5782 67.18 11.8576 67.18 13.2791C67.18 13.7725 67.1549 14.2909 67.0964 14.8428L67.1048 14.8344ZM63.8603 10.1769C63.4255 9.4661 62.8318 8.92258 62.0793 8.55465C61.3267 8.18673 60.4989 8.00277 59.5874 8.00277C58.2746 8.00277 57.1625 8.42086 56.2427 9.25705C55.3228 10.0932 54.796 11.2472 54.6623 12.7356H64.5126C64.5126 11.7489 64.2952 10.896 63.8603 10.1852V10.1769Z" fill="#F2F2F2"/>
|
||||
<path d="M73.7695 8.20355V17.4016C73.7695 18.1626 73.9284 18.6977 74.2545 19.0071C74.5806 19.3165 75.1409 19.4754 75.9352 19.4754H77.8418V21.6662H75.5088C74.0622 21.6662 72.9835 21.3317 72.2644 20.6711C71.5452 20.0105 71.1857 18.9151 71.1857 17.3933V8.19519H69.1621V6.0629H71.1857V2.13281H73.7779V6.0629H77.8501V8.19519H73.7779L73.7695 8.20355Z" fill="#F2F2F2"/>
|
||||
<path d="M85.9022 6.68902C86.9307 6.10369 88.093 5.80266 89.4058 5.80266C90.8106 5.80266 92.0732 6.13714 93.1937 6.79773C94.3142 7.46668 95.2006 8.39485 95.8444 9.59896C96.4883 10.8031 96.8144 12.2079 96.8144 13.7966C96.8144 15.3854 96.4883 16.7818 95.8444 18.011C95.2006 19.2486 94.3142 20.2018 93.1854 20.8875C92.0565 21.5732 90.7939 21.916 89.4141 21.916C88.0344 21.916 86.8805 21.6234 85.8687 21.0297C84.8569 20.4443 84.0876 19.6918 83.5775 18.7803V21.6568H80.9854V0.601562H83.5775V8.97182C84.1127 8.04365 84.8904 7.28272 85.9105 6.69738L85.9022 6.68902ZM93.4529 10.7362C92.9763 9.86654 92.3408 9.19759 91.5297 8.74605C90.7186 8.29451 89.8322 8.06037 88.8706 8.06037C87.909 8.06037 87.0394 8.29451 86.2366 8.75441C85.4255 9.22268 84.7817 9.89163 84.2967 10.778C83.8117 11.6643 83.5692 12.6845 83.5692 13.8384C83.5692 14.9924 83.8117 16.046 84.2967 16.9323C84.7817 17.8187 85.4255 18.4877 86.2366 18.9559C87.0394 19.4242 87.9174 19.65 88.8706 19.65C89.8239 19.65 90.727 19.4158 91.5297 18.9559C92.3324 18.4877 92.9763 17.8187 93.4529 16.9323C93.9296 16.046 94.1637 15.0091 94.1637 13.8134C94.1637 12.6176 93.9296 11.6142 93.4529 10.7362Z" fill="#F2F2F2"/>
|
||||
<path d="M100.318 3.01864C99.9749 2.67581 99.8076 2.25771 99.8076 1.76436C99.8076 1.27101 99.9749 0.852913 100.318 0.510076C100.661 0.167238 101.079 0 101.572 0C102.065 0 102.45 0.167238 102.784 0.510076C103.119 0.852913 103.286 1.27101 103.286 1.76436C103.286 2.25771 103.119 2.67581 102.784 3.01864C102.45 3.36148 102.049 3.52872 101.572 3.52872C101.095 3.52872 100.661 3.36148 100.318 3.01864ZM102.826 6.06237V21.6657H100.234V6.06237H102.826Z" fill="#F2F2F2"/>
|
||||
<path d="M111.773 6.52155C112.617 6.0282 113.646 5.77734 114.867 5.77734V8.45315H114.181C111.28 8.45315 109.825 10.0252 109.825 13.1776V21.6649H107.232V6.06165H109.825V8.5953C110.276 7.70058 110.928 7.00654 111.773 6.51319V6.52155Z" fill="#F2F2F2"/>
|
||||
<path d="M117.861 9.60732C118.505 8.40321 119.391 7.46668 120.52 6.80609C121.649 6.1455 122.92 5.81102 124.325 5.81102C125.537 5.81102 126.666 6.09533 127.711 6.64721C128.757 7.20746 129.551 7.94331 130.103 8.85475V0.601562H132.72V21.6735H130.103V18.7385C129.593 19.6667 128.832 20.436 127.828 21.0297C126.825 21.6317 125.646 21.9244 124.3 21.9244C122.953 21.9244 121.657 21.5816 120.528 20.8959C119.4 20.2102 118.513 19.257 117.869 18.0194C117.226 16.7818 116.899 15.377 116.899 13.805C116.899 12.233 117.226 10.8114 117.869 9.60732H117.861ZM129.392 10.7613C128.915 9.89163 128.28 9.22268 127.469 8.75441C126.658 8.28614 125.771 8.06037 124.81 8.06037C123.848 8.06037 122.962 8.28614 122.159 8.74605C121.356 9.20595 120.729 9.86654 120.253 10.7362C119.776 11.6058 119.542 12.6343 119.542 13.8134C119.542 14.9924 119.776 16.046 120.253 16.9323C120.729 17.8187 121.365 18.4877 122.159 18.9559C122.953 19.4242 123.84 19.65 124.81 19.65C125.78 19.65 126.666 19.4158 127.469 18.9559C128.272 18.4877 128.915 17.8187 129.392 16.9323C129.869 16.046 130.103 15.0175 130.103 13.8384C130.103 12.6594 129.869 11.6393 129.392 10.7613Z" fill="#F2F2F2"/>
|
||||
<path d="M21.4651 0.568359C17.8193 0.902835 16.0047 3.00167 15.3191 4.06363L4.66602 22.5183H17.5182L30.1949 0.568359H21.4651Z" fill="#F68330"/>
|
||||
<path d="M17.5265 22.5187L0 3.9302C0 3.9302 19.8177 -1.39633 21.7493 15.2188L17.5265 22.5187Z" fill="#F68330"/>
|
||||
<path d="M14.9255 4.75055L9.54883 14.0657L17.5177 22.5196L21.7405 15.2029C21.0715 9.49174 18.287 6.37276 14.9255 4.74219" fill="#F35E32"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="132.72" height="22.5186" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
37
idp/dex/web/templates/login.html
Executable file
37
idp/dex/web/templates/login.html
Executable file
@@ -0,0 +1,37 @@
|
||||
{{ template "header.html" . }}
|
||||
|
||||
<div class="nb-card">
|
||||
<h1 class="nb-heading">Sign in</h1>
|
||||
<p class="nb-subheading">Choose your login method</p>
|
||||
|
||||
{{ range $c := .Connectors }}
|
||||
<a href="{{ $c.URL }}" class="nb-btn-connector">
|
||||
{{- $iconClass := "nb-icon-default" -}}
|
||||
{{- $nameLower := lower $c.Name -}}
|
||||
{{- $idLower := lower $c.ID -}}
|
||||
{{- if or (contains "google" $nameLower) (contains "google" $idLower) -}}
|
||||
{{- $iconClass = "nb-icon-google" -}}
|
||||
{{- else if or (contains "github" $nameLower) (contains "github" $idLower) -}}
|
||||
{{- $iconClass = "nb-icon-github" -}}
|
||||
{{- else if or (contains "entra" $nameLower) (contains "entra" $idLower) -}}
|
||||
{{- $iconClass = "nb-icon-entra" -}}
|
||||
{{- else if or (contains "azure" $nameLower) (contains "azure" $idLower) -}}
|
||||
{{- $iconClass = "nb-icon-azure" -}}
|
||||
{{- else if or (contains "microsoft" $nameLower) (contains "microsoft" $idLower) -}}
|
||||
{{- $iconClass = "nb-icon-microsoft" -}}
|
||||
{{- else if or (contains "okta" $nameLower) (contains "okta" $idLower) -}}
|
||||
{{- $iconClass = "nb-icon-okta" -}}
|
||||
{{- else if or (contains "jumpcloud" $nameLower) (contains "jumpcloud" $idLower) -}}
|
||||
{{- $iconClass = "nb-icon-jumpcloud" -}}
|
||||
{{- else if or (contains "pocket" $nameLower) (contains "pocket" $idLower) -}}
|
||||
{{- $iconClass = "nb-icon-pocketid" -}}
|
||||
{{- else if or (contains "zitadel" $nameLower) (contains "zitadel" $idLower) -}}
|
||||
{{- $iconClass = "nb-icon-zitadel" -}}
|
||||
{{- end -}}
|
||||
<span class="nb-icon {{ $iconClass }}"></span>
|
||||
<span>Continue with {{ $c.Name }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ template "footer.html" . }}
|
||||
19
idp/dex/web/templates/oob.html
Executable file
19
idp/dex/web/templates/oob.html
Executable file
@@ -0,0 +1,19 @@
|
||||
{{ template "header.html" . }}
|
||||
|
||||
<div class="nb-card">
|
||||
<div style="text-align:center;margin-bottom:24px">
|
||||
<svg height="48" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" fill="none" r="45" stroke="#5cb85c" stroke-width="3"/>
|
||||
<path d="M30 50 L45 65 L70 35" fill="none" stroke="#5cb85c" stroke-width="5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="nb-heading">Login Successful</h1>
|
||||
<p class="nb-subheading">
|
||||
Copy this code back to your application:
|
||||
</p>
|
||||
<div style="background-color:rgba(63,68,75,.5);border-radius:8px;padding:16px;text-align:center;font-family:monospace;font-size:16px;color:#f68330;margin-top:16px">
|
||||
{{ .Code }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ template "footer.html" . }}
|
||||
60
idp/dex/web/templates/password.html
Executable file
60
idp/dex/web/templates/password.html
Executable file
@@ -0,0 +1,60 @@
|
||||
{{ template "header.html" . }}
|
||||
|
||||
<div class="nb-card">
|
||||
<h1 class="nb-heading">Sign in</h1>
|
||||
<p class="nb-subheading">Enter your credentials</p>
|
||||
|
||||
<form method="post" action="{{ .PostURL }}">
|
||||
{{ if .Invalid }}
|
||||
<div class="nb-error">
|
||||
Invalid {{ .UsernamePrompt }} or password.
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="nb-form-group">
|
||||
<label class="nb-label" for="login">{{ .UsernamePrompt }}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="login"
|
||||
name="login"
|
||||
class="nb-input"
|
||||
placeholder="Enter your {{ .UsernamePrompt | lower }}"
|
||||
{{ if .Username }}value="{{ .Username }}"{{ else }}autofocus{{ end }}
|
||||
required
|
||||
tabindex="1"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="nb-form-group">
|
||||
<label class="nb-label" for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
class="nb-input"
|
||||
placeholder="Enter your password"
|
||||
{{ if .Invalid }}autofocus{{ end }}
|
||||
required
|
||||
tabindex="2"
|
||||
>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submit-login" class="nb-btn" tabindex="3">
|
||||
Sign in
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{{ if .BackLink }}
|
||||
<div class="nb-back-link">
|
||||
<a href="{{ .BackLink }}" class="nb-link">Choose another login method</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector('form').onsubmit = function() {
|
||||
document.getElementById('submit-login').disabled = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
{{ template "footer.html" . }}
|
||||
BIN
idp/dex/web/themes/light/favicon.ico
Normal file
BIN
idp/dex/web/themes/light/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
BIN
idp/dex/web/themes/light/favicon.png
Executable file
BIN
idp/dex/web/themes/light/favicon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 300 B |
BIN
idp/dex/web/themes/light/logo.png
Executable file
BIN
idp/dex/web/themes/light/logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 300 B |
1
idp/dex/web/themes/light/styles.css
Executable file
1
idp/dex/web/themes/light/styles.css
Executable file
@@ -0,0 +1 @@
|
||||
/* NetBird DEX Theme - styles loaded but CSS is inline in header.html */
|
||||
14
idp/dex/web/web.go
Normal file
14
idp/dex/web/web.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed static/* templates/* themes/* robots.txt
|
||||
var files embed.FS
|
||||
|
||||
// FS returns the embedded web assets filesystem.
|
||||
func FS() fs.FS {
|
||||
return files
|
||||
}
|
||||
135
idp/sdk/sdk.go
Normal file
135
idp/sdk/sdk.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Package sdk provides an embeddable SDK for the Dex OIDC identity provider.
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dexidp/dex/storage"
|
||||
|
||||
"github.com/netbirdio/netbird/idp/dex"
|
||||
)
|
||||
|
||||
// DexIdP wraps the Dex provider with a builder pattern
|
||||
type DexIdP struct {
|
||||
provider *dex.Provider
|
||||
config *dex.Config
|
||||
yamlConfig *dex.YAMLConfig
|
||||
}
|
||||
|
||||
// Option configures a DexIdP instance
|
||||
type Option func(*dex.Config)
|
||||
|
||||
// WithIssuer sets the OIDC issuer URL
|
||||
func WithIssuer(issuer string) Option {
|
||||
return func(c *dex.Config) { c.Issuer = issuer }
|
||||
}
|
||||
|
||||
// WithPort sets the HTTP port
|
||||
func WithPort(port int) Option {
|
||||
return func(c *dex.Config) { c.Port = port }
|
||||
}
|
||||
|
||||
// WithDataDir sets the data directory for storage
|
||||
func WithDataDir(dir string) Option {
|
||||
return func(c *dex.Config) { c.DataDir = dir }
|
||||
}
|
||||
|
||||
// WithDevMode enables development mode (allows HTTP)
|
||||
func WithDevMode(dev bool) Option {
|
||||
return func(c *dex.Config) { c.DevMode = dev }
|
||||
}
|
||||
|
||||
// WithGRPCAddr sets the gRPC API address
|
||||
func WithGRPCAddr(addr string) Option {
|
||||
return func(c *dex.Config) { c.GRPCAddr = addr }
|
||||
}
|
||||
|
||||
// New creates a new DexIdP instance with the given options
|
||||
func New(opts ...Option) (*DexIdP, error) {
|
||||
config := &dex.Config{
|
||||
Port: 33081,
|
||||
DevMode: true,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
return &DexIdP{config: config}, nil
|
||||
}
|
||||
|
||||
// NewFromConfigFile creates a new DexIdP instance from a YAML config file
|
||||
func NewFromConfigFile(path string) (*DexIdP, error) {
|
||||
yamlConfig, err := dex.LoadConfig(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DexIdP{yamlConfig: yamlConfig}, nil
|
||||
}
|
||||
|
||||
// NewFromYAMLConfig creates a new DexIdP instance from a YAMLConfig
|
||||
func NewFromYAMLConfig(yamlConfig *dex.YAMLConfig) (*DexIdP, error) {
|
||||
return &DexIdP{yamlConfig: yamlConfig}, nil
|
||||
}
|
||||
|
||||
// Start initializes and starts the embedded OIDC provider
|
||||
func (d *DexIdP) Start(ctx context.Context) error {
|
||||
var err error
|
||||
if d.yamlConfig != nil {
|
||||
d.provider, err = dex.NewProviderFromYAML(ctx, d.yamlConfig)
|
||||
} else {
|
||||
d.provider, err = dex.NewProvider(ctx, d.config)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.provider.Start(ctx)
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down the provider
|
||||
func (d *DexIdP) Stop(ctx context.Context) error {
|
||||
if d.provider != nil {
|
||||
return d.provider.Stop(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureDefaultClients creates the default NetBird OAuth clients
|
||||
func (d *DexIdP) EnsureDefaultClients(ctx context.Context, dashboardURIs, cliURIs []string) error {
|
||||
return d.provider.EnsureDefaultClients(ctx, dashboardURIs, cliURIs)
|
||||
}
|
||||
|
||||
// Storage exposes Dex storage for direct user/client/connector management
|
||||
// Use storage.Client, storage.Password, storage.Connector directly
|
||||
func (d *DexIdP) Storage() storage.Storage {
|
||||
return d.provider.Storage()
|
||||
}
|
||||
|
||||
// CreateUser creates a new user with the given email, username, and password.
|
||||
// Returns the encoded user ID in Dex's format.
|
||||
func (d *DexIdP) CreateUser(ctx context.Context, email, username, password string) (string, error) {
|
||||
return d.provider.CreateUser(ctx, email, username, password)
|
||||
}
|
||||
|
||||
// DeleteUser removes a user by email
|
||||
func (d *DexIdP) DeleteUser(ctx context.Context, email string) error {
|
||||
return d.provider.DeleteUser(ctx, email)
|
||||
}
|
||||
|
||||
// ListUsers returns all users
|
||||
func (d *DexIdP) ListUsers(ctx context.Context) ([]storage.Password, error) {
|
||||
return d.provider.ListUsers(ctx)
|
||||
}
|
||||
|
||||
// IssuerURL returns the OIDC issuer URL
|
||||
func (d *DexIdP) IssuerURL() string {
|
||||
if d.yamlConfig != nil {
|
||||
return d.yamlConfig.Issuer
|
||||
}
|
||||
return d.config.Issuer
|
||||
}
|
||||
|
||||
// DiscoveryEndpoint returns the OIDC discovery endpoint URL
|
||||
func (d *DexIdP) DiscoveryEndpoint() string {
|
||||
return d.IssuerURL() + "/.well-known/openid-configuration"
|
||||
}
|
||||
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
|
||||
@@ -152,7 +152,7 @@ func loadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Confi
|
||||
}
|
||||
|
||||
oidcEndpoint := loadedConfig.HttpConfig.OIDCConfigEndpoint
|
||||
if oidcEndpoint != "" {
|
||||
if oidcEndpoint != "" && loadedConfig.EmbeddedIdP == nil {
|
||||
// if OIDCConfigEndpoint is specified, we can load DeviceAuthEndpoint and TokenEndpoint automatically
|
||||
log.WithContext(ctx).Infof("loading OIDC configuration from the provided IDP configuration endpoint %s", oidcEndpoint)
|
||||
oidcConfig, err := fetchOIDCConfig(ctx, oidcEndpoint)
|
||||
@@ -200,6 +200,10 @@ func loadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Confi
|
||||
}
|
||||
}
|
||||
|
||||
if loadedConfig.EmbeddedIdP != nil {
|
||||
log.Infof("running with the embedded IdP: %v", loadedConfig.EmbeddedIdP.Issuer)
|
||||
}
|
||||
|
||||
if loadedConfig.Relay != nil {
|
||||
log.Infof("Relay addresses: %v", loadedConfig.Relay.Addresses)
|
||||
}
|
||||
|
||||
@@ -309,7 +309,7 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string, dis
|
||||
setupKeys := map[string]*types.SetupKey{}
|
||||
nameServersGroups := make(map[string]*nbdns.NameServerGroup)
|
||||
|
||||
owner := types.NewOwnerUser(userID)
|
||||
owner := types.NewOwnerUser(userID, "", "")
|
||||
owner.AccountID = accountID
|
||||
users[userID] = owner
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/util/crypt"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -62,6 +63,14 @@ func (s *BaseServer) Store() store.Store {
|
||||
log.Fatalf("failed to create store: %v", err)
|
||||
}
|
||||
|
||||
if s.Config.DataStoreEncryptionKey != "" {
|
||||
fieldEncrypt, err := crypt.NewFieldEncrypt(s.Config.DataStoreEncryptionKey)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create field encryptor: %v", err)
|
||||
}
|
||||
store.SetFieldEncrypt(fieldEncrypt)
|
||||
}
|
||||
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/dexidp/dex/storage"
|
||||
"github.com/google/uuid"
|
||||
"github.com/netbirdio/netbird/idp/dex"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/client/common"
|
||||
@@ -57,6 +61,52 @@ type Config struct {
|
||||
|
||||
// disable default all-to-all policy
|
||||
DisableDefaultPolicy bool
|
||||
|
||||
// EmbeddedIdP contains configuration for the embedded Dex OIDC provider.
|
||||
// When set, Dex will be embedded in the management server and serve requests at /oauth2/
|
||||
EmbeddedIdP *EmbeddedIdPConfig
|
||||
}
|
||||
|
||||
// EmbeddedIdPConfig contains configuration for the embedded Dex OIDC identity provider
|
||||
type EmbeddedIdPConfig struct {
|
||||
// Enabled indicates whether the embedded IDP is enabled
|
||||
Enabled bool
|
||||
// Issuer is the OIDC issuer URL (e.g., "http://localhost:3002/oauth2")
|
||||
Issuer string
|
||||
// Storage configuration for the IdP database
|
||||
Storage EmbeddedStorageConfig
|
||||
// DashboardRedirectURIs are the OAuth2 redirect URIs for the dashboard client
|
||||
DashboardRedirectURIs []string
|
||||
// DashboardRedirectURIs are the OAuth2 redirect URIs for the dashboard client
|
||||
CLIRedirectURIs []string
|
||||
// Owner is the initial owner/admin user (optional, can be nil)
|
||||
Owner *OwnerConfig
|
||||
// SignKeyRefreshEnabled enables automatic key rotation for signing keys
|
||||
SignKeyRefreshEnabled bool
|
||||
}
|
||||
|
||||
// EmbeddedStorageConfig holds storage configuration for the embedded IdP.
|
||||
type EmbeddedStorageConfig struct {
|
||||
// Type is the storage type (currently only "sqlite3" is supported)
|
||||
Type string
|
||||
// Config contains type-specific configuration
|
||||
Config EmbeddedStorageTypeConfig
|
||||
}
|
||||
|
||||
// EmbeddedStorageTypeConfig contains type-specific storage configuration.
|
||||
type EmbeddedStorageTypeConfig struct {
|
||||
// File is the path to the SQLite database file (for sqlite3 type)
|
||||
File string
|
||||
}
|
||||
|
||||
// OwnerConfig represents the initial owner/admin user for the embedded IdP.
|
||||
type OwnerConfig struct {
|
||||
// Email is the user's email address (required)
|
||||
Email string
|
||||
// Hash is the bcrypt hash of the user's password (required)
|
||||
Hash string
|
||||
// Username is the display name for the user (optional, defaults to email)
|
||||
Username string
|
||||
}
|
||||
|
||||
// GetAuthAudiences returns the audience from the http config and device authorization flow config
|
||||
@@ -74,6 +124,73 @@ func (c Config) GetAuthAudiences() []string {
|
||||
return audiences
|
||||
}
|
||||
|
||||
// ToYAMLConfig converts EmbeddedIdPConfig to dex.YAMLConfig.
|
||||
func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
|
||||
if c.Issuer == "" {
|
||||
return nil, fmt.Errorf("issuer is required")
|
||||
}
|
||||
if c.Storage.Type == "" {
|
||||
c.Storage.Type = "sqlite3"
|
||||
}
|
||||
if c.Storage.Type == "sqlite3" && c.Storage.Config.File == "" {
|
||||
return nil, fmt.Errorf("storage file is required for sqlite3")
|
||||
}
|
||||
|
||||
cfg := &dex.YAMLConfig{
|
||||
Issuer: c.Issuer,
|
||||
Storage: dex.Storage{
|
||||
Type: c.Storage.Type,
|
||||
Config: map[string]interface{}{
|
||||
"file": c.Storage.Config.File,
|
||||
},
|
||||
},
|
||||
Web: dex.Web{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedHeaders: []string{"Authorization", "Content-Type"},
|
||||
},
|
||||
OAuth2: dex.OAuth2{
|
||||
SkipApprovalScreen: true,
|
||||
},
|
||||
Frontend: dex.Frontend{
|
||||
Issuer: "NetBird",
|
||||
Theme: "light",
|
||||
},
|
||||
EnablePasswordDB: true,
|
||||
StaticClients: []storage.Client{
|
||||
{
|
||||
ID: "netbird-dashboard",
|
||||
Name: "NetBird Dashboard",
|
||||
Public: true,
|
||||
RedirectURIs: c.DashboardRedirectURIs,
|
||||
},
|
||||
{
|
||||
ID: "netbird-cli",
|
||||
Name: "NetBird CLI",
|
||||
Public: true,
|
||||
RedirectURIs: c.CLIRedirectURIs,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add owner user if provided
|
||||
if c.Owner != nil && c.Owner.Email != "" && c.Owner.Hash != "" {
|
||||
username := c.Owner.Username
|
||||
if username == "" {
|
||||
username = c.Owner.Email
|
||||
}
|
||||
cfg.StaticPasswords = []dex.Password{
|
||||
{
|
||||
Email: c.Owner.Email,
|
||||
Hash: []byte(c.Owner.Hash),
|
||||
Username: username,
|
||||
UserID: uuid.New().String(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// TURNConfig is a config of the TURNCredentialsManager
|
||||
type TURNConfig struct {
|
||||
TimeBasedCredentials bool
|
||||
|
||||
@@ -55,14 +55,31 @@ func (s *BaseServer) SecretsManager() grpc.SecretsManager {
|
||||
}
|
||||
|
||||
func (s *BaseServer) AuthManager() auth.Manager {
|
||||
audiences := s.Config.GetAuthAudiences()
|
||||
audience := s.Config.HttpConfig.AuthAudience
|
||||
keysLocation := s.Config.HttpConfig.AuthKeysLocation
|
||||
signingKeyRefreshEnabled := s.Config.HttpConfig.IdpSignKeyRefreshEnabled
|
||||
issuer := s.Config.HttpConfig.AuthIssuer
|
||||
userIDClaim := s.Config.HttpConfig.AuthUserIDClaim
|
||||
if s.embeddedIdp != nil {
|
||||
// Use embedded IdP provider's methods to extract configuration
|
||||
audiences = s.embeddedIdp.GetClientIDs()
|
||||
if len(audiences) > 0 {
|
||||
audience = audiences[0] // Use the first client ID as the primary audience
|
||||
}
|
||||
keysLocation = s.embeddedIdp.GetKeysLocation()
|
||||
signingKeyRefreshEnabled = true
|
||||
issuer = s.embeddedIdp.GetIssuer()
|
||||
userIDClaim = s.embeddedIdp.GetUserIDClaim()
|
||||
}
|
||||
return Create(s, func() auth.Manager {
|
||||
return auth.NewManager(s.Store(),
|
||||
s.Config.HttpConfig.AuthIssuer,
|
||||
s.Config.HttpConfig.AuthAudience,
|
||||
s.Config.HttpConfig.AuthKeysLocation,
|
||||
s.Config.HttpConfig.AuthUserIDClaim,
|
||||
s.Config.GetAuthAudiences(),
|
||||
s.Config.HttpConfig.IdpSignKeyRefreshEnabled)
|
||||
issuer,
|
||||
audience,
|
||||
keysLocation,
|
||||
userIDClaim,
|
||||
audiences,
|
||||
signingKeyRefreshEnabled)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,18 @@ func (s *BaseServer) IdpManager() idp.Manager {
|
||||
return Create(s, func() idp.Manager {
|
||||
var idpManager idp.Manager
|
||||
var err error
|
||||
|
||||
// Use embedded IdP manager if embedded Dex is configured.
|
||||
// Legacy IdpManager won't be used anymore even if configured.
|
||||
if s.embeddedIdp != nil {
|
||||
idpManager, err = idp.NewEmbeddedIdPManager(s.embeddedIdp, s.Metrics())
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create embedded IDP manager: %v", err)
|
||||
}
|
||||
return idpManager
|
||||
}
|
||||
|
||||
// Fall back to external IdP manager
|
||||
if s.Config.IdpManagerConfig != nil {
|
||||
idpManager, err = idp.NewManager(context.Background(), *s.Config.IdpManagerConfig, s.Metrics())
|
||||
if err != nil {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/netbirdio/netbird/encryption"
|
||||
"github.com/netbirdio/netbird/idp/dex"
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
"github.com/netbirdio/netbird/management/server/metrics"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
@@ -40,7 +41,7 @@ type Server interface {
|
||||
SetContainer(key string, container any)
|
||||
}
|
||||
|
||||
// Server holds the HTTP BaseServer instance.
|
||||
// BaseServer holds the HTTP server instance.
|
||||
// Add any additional fields you need, such as database connections, Config, etc.
|
||||
type BaseServer struct {
|
||||
// Config holds the server configuration
|
||||
@@ -62,6 +63,9 @@ type BaseServer struct {
|
||||
certManager *autocert.Manager
|
||||
update *version.Update
|
||||
|
||||
// embeddedIdp is the embedded Dex OIDC identity provider
|
||||
embeddedIdp *dex.Provider
|
||||
|
||||
errCh chan error
|
||||
wg sync.WaitGroup
|
||||
cancel context.CancelFunc
|
||||
@@ -133,6 +137,19 @@ func (s *BaseServer) Start(ctx context.Context) error {
|
||||
go metricsWorker.Run(srvCtx)
|
||||
}
|
||||
|
||||
// Initialize embedded IDP if configured
|
||||
if s.Config.EmbeddedIdP != nil && s.Config.EmbeddedIdP.Enabled {
|
||||
yamlConfig, err := s.Config.EmbeddedIdP.ToYAMLConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create embedded IDP config: %v", err)
|
||||
}
|
||||
s.embeddedIdp, err = dex.NewProviderFromYAML(srvCtx, yamlConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create embedded IDP: %v", err)
|
||||
}
|
||||
log.WithContext(srvCtx).Infof("embedded Dex IDP initialized with issuer: %s", yamlConfig.Issuer)
|
||||
}
|
||||
|
||||
var compatListener net.Listener
|
||||
if s.mgmtPort != ManagementLegacyPort {
|
||||
// The Management gRPC server was running on port 33073 previously. Old agents that are already connected to it
|
||||
@@ -144,7 +161,7 @@ func (s *BaseServer) Start(ctx context.Context) error {
|
||||
log.WithContext(srvCtx).Infof("running gRPC backward compatibility server: %s", compatListener.Addr().String())
|
||||
}
|
||||
|
||||
rootHandler := s.handlerFunc(s.GRPCServer(), s.APIHandler(), s.Metrics().GetMeter())
|
||||
rootHandler := s.handlerFunc(srvCtx, s.GRPCServer(), s.APIHandler(), s.Metrics().GetMeter())
|
||||
switch {
|
||||
case s.certManager != nil:
|
||||
// a call to certManager.Listener() always creates a new listener so we do it once
|
||||
@@ -215,6 +232,9 @@ func (s *BaseServer) Stop() error {
|
||||
if s.update != nil {
|
||||
s.update.StopWatch()
|
||||
}
|
||||
if s.embeddedIdp != nil {
|
||||
_ = s.embeddedIdp.Stop(ctx)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.Errors():
|
||||
@@ -250,7 +270,7 @@ func updateMgmtConfig(ctx context.Context, path string, config *nbconfig.Config)
|
||||
return util.DirectWriteJson(ctx, path, config)
|
||||
}
|
||||
|
||||
func (s *BaseServer) handlerFunc(gRPCHandler *grpc.Server, httpHandler http.Handler, meter metric.Meter) http.Handler {
|
||||
func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, httpHandler http.Handler, meter metric.Meter) http.Handler {
|
||||
wsProxy := wsproxyserver.New(gRPCHandler, wsproxyserver.WithOTelMeter(meter))
|
||||
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
@@ -260,6 +280,23 @@ func (s *BaseServer) handlerFunc(gRPCHandler *grpc.Server, httpHandler http.Hand
|
||||
gRPCHandler.ServeHTTP(writer, request)
|
||||
case request.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:
|
||||
wsProxy.Handler().ServeHTTP(writer, request)
|
||||
case strings.HasPrefix(request.URL.Path, "/oauth2/"):
|
||||
// Add CORS headers for OAuth2 endpoints (needed for browser-based OIDC flows)
|
||||
writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
// Handle preflight OPTIONS request
|
||||
if request.Method == http.MethodOptions {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if s.embeddedIdp != nil {
|
||||
s.embeddedIdp.Handler().ServeHTTP(writer, request)
|
||||
} else {
|
||||
http.Error(writer, "Embedded IDP not configured", http.StatusNotFound)
|
||||
}
|
||||
default:
|
||||
httpHandler.ServeHTTP(writer, request)
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ func BuildManager(
|
||||
am.externalCacheManager = nbcache.NewUserDataCache(cacheStore)
|
||||
am.cacheManager = nbcache.NewAccountUserDataCache(am.loadAccount, cacheStore)
|
||||
|
||||
if !isNil(am.idpManager) {
|
||||
if !isNil(am.idpManager) && !isEmbeddedIdp(am.idpManager) {
|
||||
go func() {
|
||||
err := am.warmupIDPCache(ctx, cacheStore)
|
||||
if err != nil {
|
||||
@@ -556,7 +556,7 @@ func (am *DefaultAccountManager) checkAndSchedulePeerInactivityExpiration(ctx co
|
||||
|
||||
// newAccount creates a new Account with a generated ID and generated default setup keys.
|
||||
// If ID is already in use (due to collision) we try one more time before returning error
|
||||
func (am *DefaultAccountManager) newAccount(ctx context.Context, userID, domain string) (*types.Account, error) {
|
||||
func (am *DefaultAccountManager) newAccount(ctx context.Context, userID, domain, email, name string) (*types.Account, error) {
|
||||
for i := 0; i < 2; i++ {
|
||||
accountId := xid.New().String()
|
||||
|
||||
@@ -567,7 +567,7 @@ func (am *DefaultAccountManager) newAccount(ctx context.Context, userID, domain
|
||||
log.WithContext(ctx).Warnf("an account with ID already exists, retrying...")
|
||||
continue
|
||||
case statusErr.Type() == status.NotFound:
|
||||
newAccount := newAccountWithId(ctx, accountId, userID, domain, am.disableDefaultPolicy)
|
||||
newAccount := newAccountWithId(ctx, accountId, userID, domain, email, name, am.disableDefaultPolicy)
|
||||
am.StoreEvent(ctx, userID, newAccount.Id, accountId, activity.AccountCreated, nil)
|
||||
return newAccount, nil
|
||||
default:
|
||||
@@ -767,9 +767,19 @@ func isNil(i idp.Manager) bool {
|
||||
return i == nil || reflect.ValueOf(i).IsNil()
|
||||
}
|
||||
|
||||
// isEmbeddedIdp checks if the IDP manager is an embedded IDP (data stored locally in DB).
|
||||
// When true, user cache should be skipped and data fetched directly from the IDP manager.
|
||||
func isEmbeddedIdp(i idp.Manager) bool {
|
||||
if isNil(i) {
|
||||
return false
|
||||
}
|
||||
_, ok := i.(*idp.EmbeddedIdPManager)
|
||||
return ok
|
||||
}
|
||||
|
||||
// addAccountIDToIDPAppMeta update user's app metadata in idp manager
|
||||
func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, userID string, accountID string) error {
|
||||
if !isNil(am.idpManager) {
|
||||
if !isNil(am.idpManager) && !isEmbeddedIdp(am.idpManager) {
|
||||
// user can be nil if it wasn't found (e.g., just created)
|
||||
user, err := am.lookupUserInCache(ctx, userID, accountID)
|
||||
if err != nil {
|
||||
@@ -1015,6 +1025,9 @@ func (am *DefaultAccountManager) isCacheFresh(ctx context.Context, accountUsers
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) removeUserFromCache(ctx context.Context, accountID, userID string) error {
|
||||
if isEmbeddedIdp(am.idpManager) {
|
||||
return nil
|
||||
}
|
||||
data, err := am.getAccountFromCache(ctx, accountID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1106,7 +1119,7 @@ func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domai
|
||||
|
||||
lowerDomain := strings.ToLower(userAuth.Domain)
|
||||
|
||||
newAccount, err := am.newAccount(ctx, userAuth.UserId, lowerDomain)
|
||||
newAccount, err := am.newAccount(ctx, userAuth.UserId, lowerDomain, userAuth.Email, userAuth.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -1131,7 +1144,7 @@ func (am *DefaultAccountManager) addNewPrivateAccount(ctx context.Context, domai
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) addNewUserToDomainAccount(ctx context.Context, domainAccountID string, userAuth auth.UserAuth) (string, error) {
|
||||
newUser := types.NewRegularUser(userAuth.UserId)
|
||||
newUser := types.NewRegularUser(userAuth.UserId, userAuth.Email, userAuth.Name)
|
||||
newUser.AccountID = domainAccountID
|
||||
|
||||
settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthNone, domainAccountID)
|
||||
@@ -1735,7 +1748,7 @@ func (am *DefaultAccountManager) GetAccountSettings(ctx context.Context, account
|
||||
}
|
||||
|
||||
// newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id
|
||||
func newAccountWithId(ctx context.Context, accountID, userID, domain string, disableDefaultPolicy bool) *types.Account {
|
||||
func newAccountWithId(ctx context.Context, accountID, userID, domain, email, name string, disableDefaultPolicy bool) *types.Account {
|
||||
log.WithContext(ctx).Debugf("creating new account")
|
||||
|
||||
network := types.NewNetwork()
|
||||
@@ -1745,7 +1758,7 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string, dis
|
||||
setupKeys := map[string]*types.SetupKey{}
|
||||
nameServersGroups := make(map[string]*nbdns.NameServerGroup)
|
||||
|
||||
owner := types.NewOwnerUser(userID)
|
||||
owner := types.NewOwnerUser(userID, email, name)
|
||||
owner.AccountID = accountID
|
||||
users[userID] = owner
|
||||
|
||||
|
||||
@@ -123,4 +123,9 @@ type Manager interface {
|
||||
UpdateToPrimaryAccount(ctx context.Context, accountId string) error
|
||||
GetOwnerInfo(ctx context.Context, accountId string) (*types.UserInfo, error)
|
||||
GetCurrentUserInfo(ctx context.Context, userAuth auth.UserAuth) (*users.UserInfoWithPermissions, error)
|
||||
GetIdentityProvider(ctx context.Context, accountID, idpID, userID string) (*types.IdentityProvider, error)
|
||||
GetIdentityProviders(ctx context.Context, accountID, userID string) ([]*types.IdentityProvider, error)
|
||||
CreateIdentityProvider(ctx context.Context, accountID, userID string, idp *types.IdentityProvider) (*types.IdentityProvider, error)
|
||||
UpdateIdentityProvider(ctx context.Context, accountID, idpID, userID string, idp *types.IdentityProvider) (*types.IdentityProvider, error)
|
||||
DeleteIdentityProvider(ctx context.Context, accountID, idpID, userID string) error
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range tt {
|
||||
account := newAccountWithId(context.Background(), "account-1", userID, "netbird.io", false)
|
||||
account := newAccountWithId(context.Background(), "account-1", userID, "netbird.io", "", "", false)
|
||||
account.UpdateSettings(&testCase.accountSettings)
|
||||
account.Network = network
|
||||
account.Peers = testCase.peers
|
||||
@@ -407,7 +407,7 @@ func TestNewAccount(t *testing.T) {
|
||||
domain := "netbird.io"
|
||||
userId := "account_creator"
|
||||
accountID := "account_id"
|
||||
account := newAccountWithId(context.Background(), accountID, userId, domain, false)
|
||||
account := newAccountWithId(context.Background(), accountID, userId, domain, "", "", false)
|
||||
verifyNewAccountHasDefaultFields(t, account, userId, domain, []string{userId})
|
||||
}
|
||||
|
||||
@@ -649,7 +649,7 @@ func TestDefaultAccountManager_GetAccountIDFromToken(t *testing.T) {
|
||||
func TestDefaultAccountManager_SyncUserJWTGroups(t *testing.T) {
|
||||
userId := "user-id"
|
||||
domain := "test.domain"
|
||||
_ = newAccountWithId(context.Background(), "", userId, domain, false)
|
||||
_ = newAccountWithId(context.Background(), "", userId, domain, "", "", false)
|
||||
manager, _, err := createManager(t)
|
||||
require.NoError(t, err, "unable to create account manager")
|
||||
accountID, err := manager.GetAccountIDByUserID(context.Background(), userId, domain)
|
||||
@@ -802,7 +802,7 @@ func TestAccountManager_GetAccountByUserID(t *testing.T) {
|
||||
}
|
||||
|
||||
func createAccount(am *DefaultAccountManager, accountID, userID, domain string) (*types.Account, error) {
|
||||
account := newAccountWithId(context.Background(), accountID, userID, domain, false)
|
||||
account := newAccountWithId(context.Background(), accountID, userID, domain, "", "", false)
|
||||
err := am.Store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -3717,7 +3717,7 @@ func TestAddNewUserToDomainAccountWithApproval(t *testing.T) {
|
||||
|
||||
// Create a domain-based account with user approval enabled
|
||||
existingAccountID := "existing-account"
|
||||
account := newAccountWithId(context.Background(), existingAccountID, "owner-user", "example.com", false)
|
||||
account := newAccountWithId(context.Background(), existingAccountID, "owner-user", "example.com", "", "", false)
|
||||
account.Settings.Extra = &types.ExtraSettings{
|
||||
UserApprovalRequired: true,
|
||||
}
|
||||
|
||||
@@ -183,6 +183,10 @@ const (
|
||||
|
||||
AccountAutoUpdateVersionUpdated Activity = 92
|
||||
|
||||
IdentityProviderCreated Activity = 93
|
||||
IdentityProviderUpdated Activity = 94
|
||||
IdentityProviderDeleted Activity = 95
|
||||
|
||||
AccountDeleted Activity = 99999
|
||||
)
|
||||
|
||||
@@ -295,6 +299,10 @@ var activityMap = map[Activity]Code{
|
||||
UserCreated: {"User created", "user.create"},
|
||||
|
||||
AccountAutoUpdateVersionUpdated: {"Account AutoUpdate Version updated", "account.settings.auto.version.update"},
|
||||
|
||||
IdentityProviderCreated: {"Identity provider created", "identityprovider.create"},
|
||||
IdentityProviderUpdated: {"Identity provider updated", "identityprovider.update"},
|
||||
IdentityProviderDeleted: {"Identity provider deleted", "identityprovider.delete"},
|
||||
}
|
||||
|
||||
// StringCode returns a string code of the activity
|
||||
|
||||
@@ -49,8 +49,7 @@ func NewManager(store store.Store, issuer, audience, keysLocation, userIdClaim s
|
||||
)
|
||||
|
||||
return &manager{
|
||||
store: store,
|
||||
|
||||
store: store,
|
||||
validator: jwtValidator,
|
||||
extractor: claimsExtractor,
|
||||
}
|
||||
@@ -62,7 +61,7 @@ func (m *manager) ValidateAndParseToken(ctx context.Context, value string) (auth
|
||||
return auth.UserAuth{}, nil, err
|
||||
}
|
||||
|
||||
userAuth, err := m.extractor.ToUserAuth(token)
|
||||
userAuth, err := m.extractor.ToUserAuth(ctx, token)
|
||||
if err != nil {
|
||||
return auth.UserAuth{}, nil, err
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*types.Account
|
||||
|
||||
domain := "example.com"
|
||||
|
||||
account := newAccountWithId(context.Background(), dnsAccountID, dnsAdminUserID, domain, false)
|
||||
account := newAccountWithId(context.Background(), dnsAccountID, dnsAdminUserID, domain, "", "", false)
|
||||
|
||||
account.Users[dnsRegularUserID] = &types.User{
|
||||
Id: dnsRegularUserID,
|
||||
|
||||
@@ -379,7 +379,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t
|
||||
Id: "example user",
|
||||
AutoGroups: []string{groupForUsers.ID},
|
||||
}
|
||||
account := newAccountWithId(context.Background(), accountID, groupAdminUserID, domain, false)
|
||||
account := newAccountWithId(context.Background(), accountID, groupAdminUserID, domain, "", "", false)
|
||||
account.Routes[routeResource.ID] = routeResource
|
||||
account.Routes[routePeerGroupResource.ID] = routePeerGroupResource
|
||||
account.NameServerGroups[nameServerGroup.ID] = nameServerGroup
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/dns"
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/events"
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/groups"
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/idp"
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/networks"
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/peers"
|
||||
"github.com/netbirdio/netbird/management/server/http/handlers/policies"
|
||||
@@ -134,6 +135,7 @@ func NewAPIHandler(
|
||||
dns.AddEndpoints(accountManager, router)
|
||||
events.AddEndpoints(accountManager, router)
|
||||
networks.AddEndpoints(networksManager, resourceManager, routerManager, groupsManager, accountManager, router)
|
||||
idp.AddEndpoints(accountManager, router)
|
||||
|
||||
return rootRouter, nil
|
||||
}
|
||||
|
||||
199
management/server/http/handlers/idp/idp_handler.go
Normal file
199
management/server/http/handlers/idp/idp_handler.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
// handler handles identity provider HTTP endpoints
|
||||
type handler struct {
|
||||
accountManager account.Manager
|
||||
}
|
||||
|
||||
// AddEndpoints registers identity provider endpoints
|
||||
func AddEndpoints(accountManager account.Manager, router *mux.Router) {
|
||||
h := newHandler(accountManager)
|
||||
router.HandleFunc("/identity-providers", h.getAllIdentityProviders).Methods("GET", "OPTIONS")
|
||||
router.HandleFunc("/identity-providers", h.createIdentityProvider).Methods("POST", "OPTIONS")
|
||||
router.HandleFunc("/identity-providers/{idpId}", h.getIdentityProvider).Methods("GET", "OPTIONS")
|
||||
router.HandleFunc("/identity-providers/{idpId}", h.updateIdentityProvider).Methods("PUT", "OPTIONS")
|
||||
router.HandleFunc("/identity-providers/{idpId}", h.deleteIdentityProvider).Methods("DELETE", "OPTIONS")
|
||||
}
|
||||
|
||||
func newHandler(accountManager account.Manager) *handler {
|
||||
return &handler{
|
||||
accountManager: accountManager,
|
||||
}
|
||||
}
|
||||
|
||||
// getAllIdentityProviders returns all identity providers for the account
|
||||
func (h *handler) getAllIdentityProviders(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
|
||||
providers, err := h.accountManager.GetIdentityProviders(r.Context(), accountID, userID)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
response := make([]api.IdentityProvider, 0, len(providers))
|
||||
for _, p := range providers {
|
||||
response = append(response, toAPIResponse(p))
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, response)
|
||||
}
|
||||
|
||||
// getIdentityProvider returns a specific identity provider
|
||||
func (h *handler) getIdentityProvider(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
|
||||
vars := mux.Vars(r)
|
||||
idpID := vars["idpId"]
|
||||
if idpID == "" {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "identity provider ID is required"), w)
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := h.accountManager.GetIdentityProvider(r.Context(), accountID, idpID, userID)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, toAPIResponse(provider))
|
||||
}
|
||||
|
||||
// createIdentityProvider creates a new identity provider
|
||||
func (h *handler) createIdentityProvider(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
|
||||
var req api.IdentityProviderRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
idp := fromAPIRequest(&req)
|
||||
|
||||
created, err := h.accountManager.CreateIdentityProvider(r.Context(), accountID, userID, idp)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, toAPIResponse(created))
|
||||
}
|
||||
|
||||
// updateIdentityProvider updates an existing identity provider
|
||||
func (h *handler) updateIdentityProvider(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
|
||||
vars := mux.Vars(r)
|
||||
idpID := vars["idpId"]
|
||||
if idpID == "" {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "identity provider ID is required"), w)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.IdentityProviderRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
idp := fromAPIRequest(&req)
|
||||
|
||||
updated, err := h.accountManager.UpdateIdentityProvider(r.Context(), accountID, idpID, userID, idp)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, toAPIResponse(updated))
|
||||
}
|
||||
|
||||
// deleteIdentityProvider deletes an identity provider
|
||||
func (h *handler) deleteIdentityProvider(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
accountID, userID := userAuth.AccountId, userAuth.UserId
|
||||
|
||||
vars := mux.Vars(r)
|
||||
idpID := vars["idpId"]
|
||||
if idpID == "" {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "identity provider ID is required"), w)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.accountManager.DeleteIdentityProvider(r.Context(), accountID, idpID, userID); err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
|
||||
}
|
||||
|
||||
func toAPIResponse(idp *types.IdentityProvider) api.IdentityProvider {
|
||||
resp := api.IdentityProvider{
|
||||
Type: api.IdentityProviderType(idp.Type),
|
||||
Name: idp.Name,
|
||||
Issuer: idp.Issuer,
|
||||
ClientId: idp.ClientID,
|
||||
}
|
||||
if idp.ID != "" {
|
||||
resp.Id = &idp.ID
|
||||
}
|
||||
if idp.RedirectURL != "" {
|
||||
resp.RedirectUrl = &idp.RedirectURL
|
||||
}
|
||||
// Note: ClientSecret is never returned in responses for security
|
||||
return resp
|
||||
}
|
||||
|
||||
func fromAPIRequest(req *api.IdentityProviderRequest) *types.IdentityProvider {
|
||||
return &types.IdentityProvider{
|
||||
Type: types.IdentityProviderType(req.Type),
|
||||
Name: req.Name,
|
||||
Issuer: req.Issuer,
|
||||
ClientID: req.ClientId,
|
||||
ClientSecret: req.ClientSecret,
|
||||
}
|
||||
}
|
||||
438
management/server/http/handlers/idp/idp_handler_test.go
Normal file
438
management/server/http/handlers/idp/idp_handler_test.go
Normal file
@@ -0,0 +1,438 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/auth"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
const (
|
||||
testAccountID = "test-account-id"
|
||||
testUserID = "test-user-id"
|
||||
existingIDPID = "existing-idp-id"
|
||||
newIDPID = "new-idp-id"
|
||||
)
|
||||
|
||||
func initIDPTestData(existingIDP *types.IdentityProvider) *handler {
|
||||
return &handler{
|
||||
accountManager: &mock_server.MockAccountManager{
|
||||
GetIdentityProvidersFunc: func(_ context.Context, accountID, userID string) ([]*types.IdentityProvider, error) {
|
||||
if accountID != testAccountID {
|
||||
return nil, status.Errorf(status.NotFound, "account not found")
|
||||
}
|
||||
if existingIDP != nil {
|
||||
return []*types.IdentityProvider{existingIDP}, nil
|
||||
}
|
||||
return []*types.IdentityProvider{}, nil
|
||||
},
|
||||
GetIdentityProviderFunc: func(_ context.Context, accountID, idpID, userID string) (*types.IdentityProvider, error) {
|
||||
if accountID != testAccountID {
|
||||
return nil, status.Errorf(status.NotFound, "account not found")
|
||||
}
|
||||
if existingIDP != nil && idpID == existingIDP.ID {
|
||||
return existingIDP, nil
|
||||
}
|
||||
return nil, status.Errorf(status.NotFound, "identity provider not found")
|
||||
},
|
||||
CreateIdentityProviderFunc: func(_ context.Context, accountID, userID string, idp *types.IdentityProvider) (*types.IdentityProvider, error) {
|
||||
if accountID != testAccountID {
|
||||
return nil, status.Errorf(status.NotFound, "account not found")
|
||||
}
|
||||
if idp.Name == "" {
|
||||
return nil, status.Errorf(status.InvalidArgument, "name is required")
|
||||
}
|
||||
created := idp.Copy()
|
||||
created.ID = newIDPID
|
||||
created.AccountID = accountID
|
||||
return created, nil
|
||||
},
|
||||
UpdateIdentityProviderFunc: func(_ context.Context, accountID, idpID, userID string, idp *types.IdentityProvider) (*types.IdentityProvider, error) {
|
||||
if accountID != testAccountID {
|
||||
return nil, status.Errorf(status.NotFound, "account not found")
|
||||
}
|
||||
if existingIDP == nil || idpID != existingIDP.ID {
|
||||
return nil, status.Errorf(status.NotFound, "identity provider not found")
|
||||
}
|
||||
updated := idp.Copy()
|
||||
updated.ID = idpID
|
||||
updated.AccountID = accountID
|
||||
return updated, nil
|
||||
},
|
||||
DeleteIdentityProviderFunc: func(_ context.Context, accountID, idpID, userID string) error {
|
||||
if accountID != testAccountID {
|
||||
return status.Errorf(status.NotFound, "account not found")
|
||||
}
|
||||
if existingIDP == nil || idpID != existingIDP.ID {
|
||||
return status.Errorf(status.NotFound, "identity provider not found")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllIdentityProviders(t *testing.T) {
|
||||
existingIDP := &types.IdentityProvider{
|
||||
ID: existingIDPID,
|
||||
Name: "Test IDP",
|
||||
Type: types.IdentityProviderTypeOIDC,
|
||||
Issuer: "https://issuer.example.com",
|
||||
ClientID: "client-id",
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
expectedStatus int
|
||||
expectedCount int
|
||||
}{
|
||||
{
|
||||
name: "Get All Identity Providers",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCount: 1,
|
||||
},
|
||||
}
|
||||
|
||||
h := initIDPTestData(existingIDP)
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/identity-providers", nil)
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: testUserID,
|
||||
AccountId: testAccountID,
|
||||
})
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/identity-providers", h.getAllIdentityProviders).Methods("GET")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, tc.expectedStatus, recorder.Code)
|
||||
|
||||
content, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var idps []api.IdentityProvider
|
||||
err = json.Unmarshal(content, &idps)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, idps, tc.expectedCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIdentityProvider(t *testing.T) {
|
||||
existingIDP := &types.IdentityProvider{
|
||||
ID: existingIDPID,
|
||||
Name: "Test IDP",
|
||||
Type: types.IdentityProviderTypeOIDC,
|
||||
Issuer: "https://issuer.example.com",
|
||||
ClientID: "client-id",
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
idpID string
|
||||
expectedStatus int
|
||||
expectedBody bool
|
||||
}{
|
||||
{
|
||||
name: "Get Existing Identity Provider",
|
||||
idpID: existingIDPID,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
},
|
||||
{
|
||||
name: "Get Non-Existing Identity Provider",
|
||||
idpID: "non-existing-id",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectedBody: false,
|
||||
},
|
||||
}
|
||||
|
||||
h := initIDPTestData(existingIDP)
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/identity-providers/%s", tc.idpID), nil)
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: testUserID,
|
||||
AccountId: testAccountID,
|
||||
})
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/identity-providers/{idpId}", h.getIdentityProvider).Methods("GET")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, tc.expectedStatus, recorder.Code)
|
||||
|
||||
if tc.expectedBody {
|
||||
content, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var idp api.IdentityProvider
|
||||
err = json.Unmarshal(content, &idp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingIDPID, *idp.Id)
|
||||
assert.Equal(t, existingIDP.Name, idp.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateIdentityProvider(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
requestBody string
|
||||
expectedStatus int
|
||||
expectedBody bool
|
||||
}{
|
||||
{
|
||||
name: "Create Identity Provider",
|
||||
requestBody: `{
|
||||
"name": "New IDP",
|
||||
"type": "oidc",
|
||||
"issuer": "https://new-issuer.example.com",
|
||||
"client_id": "new-client-id",
|
||||
"client_secret": "new-client-secret"
|
||||
}`,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
},
|
||||
{
|
||||
name: "Create Identity Provider with Invalid JSON",
|
||||
requestBody: `{invalid json`,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: false,
|
||||
},
|
||||
}
|
||||
|
||||
h := initIDPTestData(nil)
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/identity-providers", bytes.NewBufferString(tc.requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: testUserID,
|
||||
AccountId: testAccountID,
|
||||
})
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/identity-providers", h.createIdentityProvider).Methods("POST")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, tc.expectedStatus, recorder.Code)
|
||||
|
||||
if tc.expectedBody {
|
||||
content, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var idp api.IdentityProvider
|
||||
err = json.Unmarshal(content, &idp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newIDPID, *idp.Id)
|
||||
assert.Equal(t, "New IDP", idp.Name)
|
||||
assert.Equal(t, api.IdentityProviderTypeOidc, idp.Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateIdentityProvider(t *testing.T) {
|
||||
existingIDP := &types.IdentityProvider{
|
||||
ID: existingIDPID,
|
||||
Name: "Test IDP",
|
||||
Type: types.IdentityProviderTypeOIDC,
|
||||
Issuer: "https://issuer.example.com",
|
||||
ClientID: "client-id",
|
||||
ClientSecret: "client-secret",
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
idpID string
|
||||
requestBody string
|
||||
expectedStatus int
|
||||
expectedBody bool
|
||||
}{
|
||||
{
|
||||
name: "Update Existing Identity Provider",
|
||||
idpID: existingIDPID,
|
||||
requestBody: `{
|
||||
"name": "Updated IDP",
|
||||
"type": "oidc",
|
||||
"issuer": "https://updated-issuer.example.com",
|
||||
"client_id": "updated-client-id"
|
||||
}`,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: true,
|
||||
},
|
||||
{
|
||||
name: "Update Non-Existing Identity Provider",
|
||||
idpID: "non-existing-id",
|
||||
requestBody: `{
|
||||
"name": "Updated IDP",
|
||||
"type": "oidc",
|
||||
"issuer": "https://updated-issuer.example.com",
|
||||
"client_id": "updated-client-id"
|
||||
}`,
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Update Identity Provider with Invalid JSON",
|
||||
idpID: existingIDPID,
|
||||
requestBody: `{invalid json`,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: false,
|
||||
},
|
||||
}
|
||||
|
||||
h := initIDPTestData(existingIDP)
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/identity-providers/%s", tc.idpID), bytes.NewBufferString(tc.requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: testUserID,
|
||||
AccountId: testAccountID,
|
||||
})
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/identity-providers/{idpId}", h.updateIdentityProvider).Methods("PUT")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, tc.expectedStatus, recorder.Code)
|
||||
|
||||
if tc.expectedBody {
|
||||
content, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var idp api.IdentityProvider
|
||||
err = json.Unmarshal(content, &idp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingIDPID, *idp.Id)
|
||||
assert.Equal(t, "Updated IDP", idp.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteIdentityProvider(t *testing.T) {
|
||||
existingIDP := &types.IdentityProvider{
|
||||
ID: existingIDPID,
|
||||
Name: "Test IDP",
|
||||
Type: types.IdentityProviderTypeOIDC,
|
||||
Issuer: "https://issuer.example.com",
|
||||
ClientID: "client-id",
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
idpID string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Delete Existing Identity Provider",
|
||||
idpID: existingIDPID,
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Delete Non-Existing Identity Provider",
|
||||
idpID: "non-existing-id",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
h := initIDPTestData(existingIDP)
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/identity-providers/%s", tc.idpID), nil)
|
||||
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
||||
UserId: testUserID,
|
||||
AccountId: testAccountID,
|
||||
})
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/identity-providers/{idpId}", h.deleteIdentityProvider).Methods("DELETE")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
res := recorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, tc.expectedStatus, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToAPIResponse(t *testing.T) {
|
||||
idp := &types.IdentityProvider{
|
||||
ID: "test-id",
|
||||
Name: "Test IDP",
|
||||
Type: types.IdentityProviderTypeGoogle,
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "client-id",
|
||||
ClientSecret: "should-not-be-returned",
|
||||
}
|
||||
|
||||
response := toAPIResponse(idp)
|
||||
|
||||
assert.Equal(t, "test-id", *response.Id)
|
||||
assert.Equal(t, "Test IDP", response.Name)
|
||||
assert.Equal(t, api.IdentityProviderTypeGoogle, response.Type)
|
||||
assert.Equal(t, "https://accounts.google.com", response.Issuer)
|
||||
assert.Equal(t, "client-id", response.ClientId)
|
||||
// Note: ClientSecret is not included in response type by design
|
||||
}
|
||||
|
||||
func TestFromAPIRequest(t *testing.T) {
|
||||
req := &api.IdentityProviderRequest{
|
||||
Name: "New IDP",
|
||||
Type: api.IdentityProviderTypeOkta,
|
||||
Issuer: "https://dev-123456.okta.com",
|
||||
ClientId: "okta-client-id",
|
||||
ClientSecret: "okta-client-secret",
|
||||
}
|
||||
|
||||
idp := fromAPIRequest(req)
|
||||
|
||||
assert.Equal(t, "New IDP", idp.Name)
|
||||
assert.Equal(t, types.IdentityProviderTypeOkta, idp.Type)
|
||||
assert.Equal(t, "https://dev-123456.okta.com", idp.Issuer)
|
||||
assert.Equal(t, "okta-client-id", idp.ClientID)
|
||||
assert.Equal(t, "okta-client-secret", idp.ClientSecret)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func initTestMetaData(t *testing.T, peers ...*nbpeer.Peer) *Handler {
|
||||
},
|
||||
}
|
||||
|
||||
srvUser := types.NewRegularUser(serviceUser)
|
||||
srvUser := types.NewRegularUser(serviceUser, "", "")
|
||||
srvUser.IsServiceUser = true
|
||||
|
||||
account := &types.Account{
|
||||
@@ -75,7 +75,7 @@ func initTestMetaData(t *testing.T, peers ...*nbpeer.Peer) *Handler {
|
||||
Peers: peersMap,
|
||||
Users: map[string]*types.User{
|
||||
adminUser: types.NewAdminUser(adminUser),
|
||||
regularUser: types.NewRegularUser(regularUser),
|
||||
regularUser: types.NewRegularUser(regularUser, "", ""),
|
||||
serviceUser: srvUser,
|
||||
},
|
||||
Groups: map[string]*types.Group{
|
||||
|
||||
@@ -326,6 +326,16 @@ func toUserResponse(user *types.UserInfo, currenUserID string) *api.User {
|
||||
|
||||
isCurrent := user.ID == currenUserID
|
||||
|
||||
var password *string
|
||||
if user.Password != "" {
|
||||
password = &user.Password
|
||||
}
|
||||
|
||||
var idpID *string
|
||||
if user.IdPID != "" {
|
||||
idpID = &user.IdPID
|
||||
}
|
||||
|
||||
return &api.User{
|
||||
Id: user.ID,
|
||||
Name: user.Name,
|
||||
@@ -339,6 +349,8 @@ func toUserResponse(user *types.UserInfo, currenUserID string) *api.User {
|
||||
LastLogin: &user.LastLogin,
|
||||
Issued: &user.Issued,
|
||||
PendingApproval: user.PendingApproval,
|
||||
Password: password,
|
||||
IdpId: idpID,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,9 @@ func (m *AuthMiddleware) checkJWTFromRequest(r *http.Request, authHeaderParts []
|
||||
userAuth.IsChild = ok
|
||||
}
|
||||
|
||||
// Email is now extracted in ToUserAuth (from claims or userinfo endpoint)
|
||||
// Available as userAuth.Email
|
||||
|
||||
// we need to call this method because if user is new, we will automatically add it to existing or create a new account
|
||||
accountId, _, err := m.ensureAccount(ctx, userAuth)
|
||||
if err != nil {
|
||||
|
||||
258
management/server/identity_provider.go
Normal file
258
management/server/identity_provider.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/dexidp/dex/storage"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/idp/dex"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
// GetIdentityProviders returns all identity providers for an account
|
||||
func (am *DefaultAccountManager) GetIdentityProviders(ctx context.Context, accountID, userID string) ([]*types.IdentityProvider, error) {
|
||||
ok, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Read)
|
||||
if err != nil {
|
||||
return nil, status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return nil, status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
embeddedManager, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
||||
if !ok {
|
||||
log.Warn("identity provider management requires embedded IdP")
|
||||
return []*types.IdentityProvider{}, nil
|
||||
}
|
||||
|
||||
connectors, err := embeddedManager.ListConnectors(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(status.Internal, "failed to list identity providers: %v", err)
|
||||
}
|
||||
|
||||
result := make([]*types.IdentityProvider, 0, len(connectors))
|
||||
for _, conn := range connectors {
|
||||
result = append(result, connectorConfigToIdentityProvider(conn, accountID))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetIdentityProvider returns a specific identity provider by ID
|
||||
func (am *DefaultAccountManager) GetIdentityProvider(ctx context.Context, accountID, idpID, userID string) (*types.IdentityProvider, error) {
|
||||
ok, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Read)
|
||||
if err != nil {
|
||||
return nil, status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return nil, status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
embeddedManager, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
||||
if !ok {
|
||||
return nil, status.Errorf(status.Internal, "identity provider management requires embedded IdP")
|
||||
}
|
||||
|
||||
conn, err := embeddedManager.GetConnector(ctx, idpID)
|
||||
if err != nil {
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
return nil, status.Errorf(status.NotFound, "identity provider not found")
|
||||
}
|
||||
return nil, status.Errorf(status.Internal, "failed to get identity provider: %v", err)
|
||||
}
|
||||
|
||||
return connectorConfigToIdentityProvider(conn, accountID), nil
|
||||
}
|
||||
|
||||
// CreateIdentityProvider creates a new identity provider
|
||||
func (am *DefaultAccountManager) CreateIdentityProvider(ctx context.Context, accountID, userID string, idpConfig *types.IdentityProvider) (*types.IdentityProvider, error) {
|
||||
ok, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Create)
|
||||
if err != nil {
|
||||
return nil, status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return nil, status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
if err := validateIdentityProvider(idpConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
embeddedManager, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
||||
if !ok {
|
||||
return nil, status.Errorf(status.Internal, "identity provider management requires embedded IdP")
|
||||
}
|
||||
|
||||
// Generate ID if not provided
|
||||
if idpConfig.ID == "" {
|
||||
idpConfig.ID = generateIdentityProviderID(idpConfig.Type)
|
||||
}
|
||||
idpConfig.AccountID = accountID
|
||||
|
||||
connCfg := identityProviderToConnectorConfig(idpConfig)
|
||||
|
||||
createdConn, err := embeddedManager.CreateConnector(ctx, connCfg)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(status.Internal, "failed to create identity provider: %v", err)
|
||||
}
|
||||
|
||||
// Set the redirect URL from the created connector
|
||||
idpConfig.RedirectURL = createdConn.RedirectURI
|
||||
|
||||
return idpConfig, nil
|
||||
}
|
||||
|
||||
// UpdateIdentityProvider updates an existing identity provider
|
||||
func (am *DefaultAccountManager) UpdateIdentityProvider(ctx context.Context, accountID, idpID, userID string, idpConfig *types.IdentityProvider) (*types.IdentityProvider, error) {
|
||||
ok, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Update)
|
||||
if err != nil {
|
||||
return nil, status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return nil, status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
if err := validateIdentityProvider(idpConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
embeddedManager, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
||||
if !ok {
|
||||
return nil, status.Errorf(status.Internal, "identity provider management requires embedded IdP")
|
||||
}
|
||||
|
||||
// Verify the connector exists
|
||||
_, err = embeddedManager.GetConnector(ctx, idpID)
|
||||
if err != nil {
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
return nil, status.Errorf(status.NotFound, "identity provider not found")
|
||||
}
|
||||
return nil, status.Errorf(status.Internal, "failed to get identity provider: %v", err)
|
||||
}
|
||||
|
||||
idpConfig.ID = idpID
|
||||
idpConfig.AccountID = accountID
|
||||
|
||||
connCfg := identityProviderToConnectorConfig(idpConfig)
|
||||
|
||||
if err := embeddedManager.UpdateConnector(ctx, connCfg); err != nil {
|
||||
return nil, status.Errorf(status.Internal, "failed to update identity provider: %v", err)
|
||||
}
|
||||
|
||||
return idpConfig, nil
|
||||
}
|
||||
|
||||
// DeleteIdentityProvider deletes an identity provider
|
||||
func (am *DefaultAccountManager) DeleteIdentityProvider(ctx context.Context, accountID, idpID, userID string) error {
|
||||
ok, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Settings, operations.Delete)
|
||||
if err != nil {
|
||||
return status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
embeddedManager, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
||||
if !ok {
|
||||
return status.Errorf(status.Internal, "identity provider management requires embedded IdP")
|
||||
}
|
||||
|
||||
if err := embeddedManager.DeleteConnector(ctx, idpID); err != nil {
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
return status.Errorf(status.NotFound, "identity provider not found")
|
||||
}
|
||||
return status.Errorf(status.Internal, "failed to delete identity provider: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateIdentityProvider(idpConfig *types.IdentityProvider) error {
|
||||
if idpConfig.Name == "" {
|
||||
return status.Errorf(status.InvalidArgument, "identity provider name is required")
|
||||
}
|
||||
if idpConfig.Type == "" {
|
||||
return status.Errorf(status.InvalidArgument, "identity provider type is required")
|
||||
}
|
||||
// Validate type is supported
|
||||
switch idpConfig.Type {
|
||||
case types.IdentityProviderTypeOIDC,
|
||||
types.IdentityProviderTypeZitadel,
|
||||
types.IdentityProviderTypeEntra,
|
||||
types.IdentityProviderTypeGoogle,
|
||||
types.IdentityProviderTypeOkta,
|
||||
types.IdentityProviderTypePocketID,
|
||||
types.IdentityProviderTypeMicrosoft:
|
||||
// Valid types
|
||||
default:
|
||||
return status.Errorf(status.InvalidArgument, "unsupported identity provider type: %s", idpConfig.Type)
|
||||
}
|
||||
// Issuer is required for OIDC-based types
|
||||
if idpConfig.Type != types.IdentityProviderTypeGoogle && idpConfig.Type != types.IdentityProviderTypeMicrosoft {
|
||||
if idpConfig.Issuer == "" {
|
||||
return status.Errorf(status.InvalidArgument, "identity provider issuer is required")
|
||||
}
|
||||
}
|
||||
if idpConfig.ClientID == "" {
|
||||
return status.Errorf(status.InvalidArgument, "identity provider client ID is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// connectorConfigToIdentityProvider converts a dex.ConnectorConfig to types.IdentityProvider
|
||||
func connectorConfigToIdentityProvider(conn *dex.ConnectorConfig, accountID string) *types.IdentityProvider {
|
||||
return &types.IdentityProvider{
|
||||
ID: conn.ID,
|
||||
AccountID: accountID,
|
||||
Type: types.IdentityProviderType(conn.Type),
|
||||
Name: conn.Name,
|
||||
Issuer: conn.Issuer,
|
||||
ClientID: conn.ClientID,
|
||||
ClientSecret: conn.ClientSecret,
|
||||
RedirectURL: conn.RedirectURI,
|
||||
}
|
||||
}
|
||||
|
||||
// identityProviderToConnectorConfig converts a types.IdentityProvider to dex.ConnectorConfig
|
||||
func identityProviderToConnectorConfig(idpConfig *types.IdentityProvider) *dex.ConnectorConfig {
|
||||
return &dex.ConnectorConfig{
|
||||
ID: idpConfig.ID,
|
||||
Name: idpConfig.Name,
|
||||
Type: string(idpConfig.Type),
|
||||
Issuer: idpConfig.Issuer,
|
||||
ClientID: idpConfig.ClientID,
|
||||
ClientSecret: idpConfig.ClientSecret,
|
||||
}
|
||||
}
|
||||
|
||||
// generateIdentityProviderID generates a unique ID for an identity provider.
|
||||
// For specific provider types (okta, zitadel, entra, google, pocketid, microsoft),
|
||||
// the ID is prefixed with the type name. Generic OIDC providers get no prefix.
|
||||
func generateIdentityProviderID(idpType types.IdentityProviderType) string {
|
||||
id := xid.New().String()
|
||||
|
||||
switch idpType {
|
||||
case types.IdentityProviderTypeOkta:
|
||||
return "okta-" + id
|
||||
case types.IdentityProviderTypeZitadel:
|
||||
return "zitadel-" + id
|
||||
case types.IdentityProviderTypeEntra:
|
||||
return "entra-" + id
|
||||
case types.IdentityProviderTypeGoogle:
|
||||
return "google-" + id
|
||||
case types.IdentityProviderTypePocketID:
|
||||
return "pocketid-" + id
|
||||
case types.IdentityProviderTypeMicrosoft:
|
||||
return "microsoft-" + id
|
||||
default:
|
||||
// Generic OIDC - no prefix
|
||||
return id
|
||||
}
|
||||
}
|
||||
124
management/server/identity_provider_test.go
Normal file
124
management/server/identity_provider_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
func TestDefaultAccountManager_CreateIdentityProvider_Validation(t *testing.T) {
|
||||
manager, _, err := createManager(t)
|
||||
require.NoError(t, err)
|
||||
|
||||
userID := "testingUser"
|
||||
account, err := manager.GetOrCreateAccountByUser(context.Background(), userID, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
idp *types.IdentityProvider
|
||||
expectError bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "Missing Name",
|
||||
idp: &types.IdentityProvider{
|
||||
Type: types.IdentityProviderTypeOIDC,
|
||||
Issuer: "https://issuer.example.com",
|
||||
ClientID: "client-id",
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "name is required",
|
||||
},
|
||||
{
|
||||
name: "Missing Type",
|
||||
idp: &types.IdentityProvider{
|
||||
Name: "Test IDP",
|
||||
Issuer: "https://issuer.example.com",
|
||||
ClientID: "client-id",
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "type is required",
|
||||
},
|
||||
{
|
||||
name: "Missing Issuer",
|
||||
idp: &types.IdentityProvider{
|
||||
Name: "Test IDP",
|
||||
Type: types.IdentityProviderTypeOIDC,
|
||||
ClientID: "client-id",
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "issuer is required",
|
||||
},
|
||||
{
|
||||
name: "Missing ClientID",
|
||||
idp: &types.IdentityProvider{
|
||||
Name: "Test IDP",
|
||||
Type: types.IdentityProviderTypeOIDC,
|
||||
Issuer: "https://issuer.example.com",
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "client ID is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := manager.CreateIdentityProvider(context.Background(), account.Id, userID, tc.idp)
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.errorMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_GetIdentityProviders(t *testing.T) {
|
||||
manager, _, err := createManager(t)
|
||||
require.NoError(t, err)
|
||||
|
||||
userID := "testingUser"
|
||||
account, err := manager.GetOrCreateAccountByUser(context.Background(), userID, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should return empty list (stub implementation)
|
||||
providers, err := manager.GetIdentityProviders(context.Background(), account.Id, userID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, providers)
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_GetIdentityProvider_NotFound(t *testing.T) {
|
||||
manager, _, err := createManager(t)
|
||||
require.NoError(t, err)
|
||||
|
||||
userID := "testingUser"
|
||||
account, err := manager.GetOrCreateAccountByUser(context.Background(), userID, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should return not found error (stub implementation)
|
||||
_, err = manager.GetIdentityProvider(context.Background(), account.Id, "any-id", userID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_UpdateIdentityProvider_Validation(t *testing.T) {
|
||||
manager, _, err := createManager(t)
|
||||
require.NoError(t, err)
|
||||
|
||||
userID := "testingUser"
|
||||
account, err := manager.GetOrCreateAccountByUser(context.Background(), userID, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should fail validation before reaching "not implemented" error
|
||||
invalidIDP := &types.IdentityProvider{
|
||||
Name: "", // Empty name should fail validation
|
||||
}
|
||||
|
||||
_, err = manager.UpdateIdentityProvider(context.Background(), account.Id, "some-id", userID, invalidIDP)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "name is required")
|
||||
}
|
||||
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")
|
||||
})
|
||||
}
|
||||
}
|
||||
243
management/server/idp/embedded.go
Normal file
243
management/server/idp/embedded.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/dexidp/dex/storage"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/idp/dex"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
// Compile-time check that EmbeddedIdPManager implements Manager interface
|
||||
var _ Manager = (*EmbeddedIdPManager)(nil)
|
||||
|
||||
// EmbeddedIdPManager implements the Manager interface using the embedded Dex IdP.
|
||||
type EmbeddedIdPManager struct {
|
||||
provider *dex.Provider
|
||||
appMetrics telemetry.AppMetrics
|
||||
}
|
||||
|
||||
// NewEmbeddedIdPManager creates a new instance of EmbeddedIdPManager with an existing provider.
|
||||
func NewEmbeddedIdPManager(provider *dex.Provider, appMetrics telemetry.AppMetrics) (*EmbeddedIdPManager, error) {
|
||||
if provider == nil {
|
||||
return nil, fmt.Errorf("embedded IdP provider is required")
|
||||
}
|
||||
|
||||
return &EmbeddedIdPManager{
|
||||
provider: provider,
|
||||
appMetrics: appMetrics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||
func (m *EmbeddedIdPManager) UpdateUserAppMetadata(ctx context.Context, userID string, appMetadata AppMetadata) error {
|
||||
// TODO: implement
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserDataByID requests user data from the embedded IdP via user ID.
|
||||
func (m *EmbeddedIdPManager) GetUserDataByID(ctx context.Context, userID string, appMetadata AppMetadata) (*UserData, error) {
|
||||
user, err := m.provider.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user by ID: %w", err)
|
||||
}
|
||||
|
||||
return &UserData{
|
||||
Email: user.Email,
|
||||
Name: user.Username,
|
||||
ID: user.UserID,
|
||||
AppMetadata: appMetadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAccount returns all the users for a given account.
|
||||
// Note: Embedded dex doesn't store account metadata, so this returns all users.
|
||||
func (m *EmbeddedIdPManager) GetAccount(ctx context.Context, accountID string) ([]*UserData, error) {
|
||||
users, err := m.provider.ListUsers(ctx)
|
||||
if err != nil {
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to list users: %w", err)
|
||||
}
|
||||
|
||||
result := make([]*UserData, 0, len(users))
|
||||
for _, user := range users {
|
||||
result = append(result, &UserData{
|
||||
Email: user.Email,
|
||||
Name: user.Username,
|
||||
ID: user.UserID,
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: accountID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||
// Note: Embedded dex doesn't store account metadata, so all users are indexed under UnsetAccountID.
|
||||
func (m *EmbeddedIdPManager) GetAllAccounts(ctx context.Context) (map[string][]*UserData, error) {
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||
}
|
||||
|
||||
users, err := m.provider.ListUsers(ctx)
|
||||
if err != nil {
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to list users: %w", err)
|
||||
}
|
||||
|
||||
indexedUsers := make(map[string][]*UserData)
|
||||
for _, user := range users {
|
||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], &UserData{
|
||||
Email: user.Email,
|
||||
Name: user.Username,
|
||||
ID: user.UserID,
|
||||
})
|
||||
}
|
||||
|
||||
return indexedUsers, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in the embedded IdP.
|
||||
func (m *EmbeddedIdPManager) CreateUser(ctx context.Context, email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountCreateUser()
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
_, err := m.provider.GetUser(ctx, email)
|
||||
if err == nil {
|
||||
return nil, fmt.Errorf("user with email %s already exists", email)
|
||||
}
|
||||
if !errors.Is(err, storage.ErrNotFound) {
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to check existing user: %w", err)
|
||||
}
|
||||
|
||||
// Generate a random password for the new user
|
||||
password := GeneratePassword(16, 2, 2, 2)
|
||||
|
||||
// Create the user via provider (handles hashing and ID generation)
|
||||
// The provider returns an encoded user ID in Dex's format (base64 protobuf with connector ID)
|
||||
userID, err := m.provider.CreateUser(ctx, email, name, password)
|
||||
if err != nil {
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to create user in embedded IdP: %w", err)
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("created user %s in embedded IdP", email)
|
||||
|
||||
return &UserData{
|
||||
Email: email,
|
||||
Name: name,
|
||||
ID: userID,
|
||||
Password: password,
|
||||
AppMetadata: AppMetadata{
|
||||
WTAccountID: accountID,
|
||||
WTInvitedBy: invitedByEmail,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail searches users with a given email.
|
||||
func (m *EmbeddedIdPManager) GetUserByEmail(ctx context.Context, email string) ([]*UserData, error) {
|
||||
user, err := m.provider.GetUser(ctx, email)
|
||||
if err != nil {
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
return nil, nil // Return empty slice for not found
|
||||
}
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user by email: %w", err)
|
||||
}
|
||||
|
||||
return []*UserData{
|
||||
{
|
||||
Email: user.Email,
|
||||
Name: user.Username,
|
||||
ID: user.UserID,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InviteUserByID resends an invitation to a user.
|
||||
func (m *EmbeddedIdPManager) InviteUserByID(ctx context.Context, userID string) error {
|
||||
// TODO: implement
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user from the embedded IdP by user ID.
|
||||
func (m *EmbeddedIdPManager) DeleteUser(ctx context.Context, userID string) error {
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountDeleteUser()
|
||||
}
|
||||
|
||||
// Get user by ID to retrieve email (provider.DeleteUser requires email)
|
||||
user, err := m.provider.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return fmt.Errorf("failed to get user for deletion: %w", err)
|
||||
}
|
||||
|
||||
err = m.provider.DeleteUser(ctx, user.Email)
|
||||
if err != nil {
|
||||
if m.appMetrics != nil {
|
||||
m.appMetrics.IDPMetrics().CountRequestError()
|
||||
}
|
||||
return fmt.Errorf("failed to delete user from embedded IdP: %w", err)
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Debugf("deleted user %s from embedded IdP", user.Email)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateConnector creates a new identity provider connector in Dex.
|
||||
// Returns the created connector config with the redirect URL populated.
|
||||
func (m *EmbeddedIdPManager) CreateConnector(ctx context.Context, cfg *dex.ConnectorConfig) (*dex.ConnectorConfig, error) {
|
||||
return m.provider.CreateConnector(ctx, cfg)
|
||||
}
|
||||
|
||||
// GetConnector retrieves an identity provider connector by ID.
|
||||
func (m *EmbeddedIdPManager) GetConnector(ctx context.Context, id string) (*dex.ConnectorConfig, error) {
|
||||
return m.provider.GetConnector(ctx, id)
|
||||
}
|
||||
|
||||
// ListConnectors returns all identity provider connectors.
|
||||
func (m *EmbeddedIdPManager) ListConnectors(ctx context.Context) ([]*dex.ConnectorConfig, error) {
|
||||
return m.provider.ListConnectors(ctx)
|
||||
}
|
||||
|
||||
// UpdateConnector updates an existing identity provider connector.
|
||||
func (m *EmbeddedIdPManager) UpdateConnector(ctx context.Context, cfg *dex.ConnectorConfig) error {
|
||||
return m.provider.UpdateConnector(ctx, cfg)
|
||||
}
|
||||
|
||||
// DeleteConnector removes an identity provider connector.
|
||||
func (m *EmbeddedIdPManager) DeleteConnector(ctx context.Context, id string) error {
|
||||
return m.provider.DeleteConnector(ctx, id)
|
||||
}
|
||||
|
||||
// GetRedirectURI returns the Dex callback redirect URI for configuring connectors.
|
||||
func (m *EmbeddedIdPManager) GetRedirectURI() string {
|
||||
return m.provider.GetRedirectURI()
|
||||
}
|
||||
240
management/server/idp/embedded_test.go
Normal file
240
management/server/idp/embedded_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/idp/dex"
|
||||
)
|
||||
|
||||
func TestEmbeddedIdPManager_CreateUser_EndToEnd(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a temporary directory for the test
|
||||
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create the Dex provider
|
||||
dexConfig := &dex.Config{
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Port: 5556,
|
||||
DataDir: tmpDir,
|
||||
}
|
||||
|
||||
provider, err := dex.NewProvider(ctx, dexConfig)
|
||||
require.NoError(t, err)
|
||||
defer provider.Stop(ctx)
|
||||
|
||||
// Create the embedded IDP manager
|
||||
manager, err := NewEmbeddedIdPManager(EmbeddedIdPConfig{
|
||||
Provider: provider,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test data
|
||||
email := "newuser@example.com"
|
||||
name := "New User"
|
||||
accountID := "test-account-id"
|
||||
invitedByEmail := "admin@example.com"
|
||||
|
||||
// Create the user
|
||||
userData, err := manager.CreateUser(ctx, email, name, accountID, invitedByEmail)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, userData)
|
||||
|
||||
t.Logf("Created user: ID=%s, Email=%s, Name=%s, Password=%s",
|
||||
userData.ID, userData.Email, userData.Name, userData.Password)
|
||||
|
||||
// Verify user data
|
||||
assert.Equal(t, email, userData.Email)
|
||||
assert.Equal(t, name, userData.Name)
|
||||
assert.NotEmpty(t, userData.ID)
|
||||
assert.NotEmpty(t, userData.Password)
|
||||
assert.Equal(t, accountID, userData.AppMetadata.WTAccountID)
|
||||
assert.Equal(t, invitedByEmail, userData.AppMetadata.WTInvitedBy)
|
||||
|
||||
// Verify the user ID is in Dex's encoded format (base64 protobuf)
|
||||
rawUserID, connectorID, err := dex.DecodeDexUserID(userData.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rawUserID)
|
||||
assert.Equal(t, "local", connectorID)
|
||||
|
||||
t.Logf("Decoded user ID: rawUserID=%s, connectorID=%s", rawUserID, connectorID)
|
||||
|
||||
// Verify we can look up the user by the encoded ID
|
||||
lookedUpUser, err := manager.GetUserDataByID(ctx, userData.ID, AppMetadata{WTAccountID: accountID})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, email, lookedUpUser.Email)
|
||||
|
||||
// Verify we can look up by email
|
||||
users, err := manager.GetUserByEmail(ctx, email)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users, 1)
|
||||
assert.Equal(t, email, users[0].Email)
|
||||
|
||||
// Verify creating duplicate user fails
|
||||
_, err = manager.CreateUser(ctx, email, name, accountID, invitedByEmail)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already exists")
|
||||
}
|
||||
|
||||
func TestEmbeddedIdPManager_GetUserDataByID_WithEncodedID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
provider, err := dex.NewProvider(ctx, &dex.Config{
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Port: 5556,
|
||||
DataDir: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer provider.Stop(ctx)
|
||||
|
||||
manager, err := NewEmbeddedIdPManager(EmbeddedIdPConfig{
|
||||
Provider: provider,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a user first
|
||||
userData, err := manager.CreateUser(ctx, "test@example.com", "Test User", "account1", "admin@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
// The returned ID should be encoded
|
||||
encodedID := userData.ID
|
||||
|
||||
// Lookup should work with the encoded ID
|
||||
lookedUp, err := manager.GetUserDataByID(ctx, encodedID, AppMetadata{WTAccountID: "account1"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test@example.com", lookedUp.Email)
|
||||
assert.Equal(t, "Test User", lookedUp.Name)
|
||||
}
|
||||
|
||||
func TestEmbeddedIdPManager_DeleteUser(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
provider, err := dex.NewProvider(ctx, &dex.Config{
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Port: 5556,
|
||||
DataDir: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer provider.Stop(ctx)
|
||||
|
||||
manager, err := NewEmbeddedIdPManager(EmbeddedIdPConfig{
|
||||
Provider: provider,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a user
|
||||
userData, err := manager.CreateUser(ctx, "delete-me@example.com", "Delete Me", "account1", "admin@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete the user using the encoded ID
|
||||
err = manager.DeleteUser(ctx, userData.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify user no longer exists
|
||||
_, err = manager.GetUserDataByID(ctx, userData.ID, AppMetadata{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEmbeddedIdPManager_GetAccount(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
provider, err := dex.NewProvider(ctx, &dex.Config{
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Port: 5556,
|
||||
DataDir: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer provider.Stop(ctx)
|
||||
|
||||
manager, err := NewEmbeddedIdPManager(EmbeddedIdPConfig{
|
||||
Provider: provider,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create multiple users
|
||||
_, err = manager.CreateUser(ctx, "user1@example.com", "User 1", "account1", "admin@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = manager.CreateUser(ctx, "user2@example.com", "User 2", "account1", "admin@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get all users for the account
|
||||
users, err := manager.GetAccount(ctx, "account1")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, users, 2)
|
||||
|
||||
emails := make([]string, len(users))
|
||||
for i, u := range users {
|
||||
emails[i] = u.Email
|
||||
}
|
||||
assert.Contains(t, emails, "user1@example.com")
|
||||
assert.Contains(t, emails, "user2@example.com")
|
||||
}
|
||||
|
||||
func TestEmbeddedIdPManager_UserIDFormat_MatchesJWT(t *testing.T) {
|
||||
// This test verifies that the user ID returned by CreateUser
|
||||
// matches the format that Dex uses in JWT tokens (the 'sub' claim)
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
provider, err := dex.NewProvider(ctx, &dex.Config{
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Port: 5556,
|
||||
DataDir: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer provider.Stop(ctx)
|
||||
|
||||
manager, err := NewEmbeddedIdPManager(EmbeddedIdPConfig{
|
||||
Provider: provider,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a user
|
||||
userData, err := manager.CreateUser(ctx, "jwt-test@example.com", "JWT Test", "account1", "admin@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
// The ID should be in the format: base64(protobuf{user_id, connector_id})
|
||||
// Example: CiQ3YWFkOGMwNS0zMjg3LTQ3M2YtYjQyYS0zNjU1MDRiZjI1ZTcSBWxvY2Fs
|
||||
|
||||
// Verify it can be decoded
|
||||
rawUserID, connectorID, err := dex.DecodeDexUserID(userData.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Raw user ID should be a UUID
|
||||
assert.Regexp(t, `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, rawUserID)
|
||||
|
||||
// Connector ID should be "local" for password-based auth
|
||||
assert.Equal(t, "local", connectorID)
|
||||
|
||||
// Re-encoding should produce the same result
|
||||
reEncoded := dex.EncodeDexUserID(rawUserID, connectorID)
|
||||
assert.Equal(t, userData.ID, reEncoded)
|
||||
|
||||
t.Logf("User ID format verified:")
|
||||
t.Logf(" Encoded ID: %s", userData.ID)
|
||||
t.Logf(" Raw UUID: %s", rawUserID)
|
||||
t.Logf(" Connector: %s", connectorID)
|
||||
}
|
||||
@@ -72,6 +72,7 @@ type UserData struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"user_id"`
|
||||
AppMetadata AppMetadata `json:"app_metadata"`
|
||||
Password string `json:"-"` // Plain password, only set on user creation, excluded from JSON
|
||||
}
|
||||
|
||||
func (u *UserData) MarshalBinary() (data []byte, err error) {
|
||||
@@ -173,40 +174,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)
|
||||
}
|
||||
|
||||
@@ -128,6 +128,12 @@ type MockAccountManager struct {
|
||||
UpdateAccountPeersFunc func(ctx context.Context, accountID string)
|
||||
BufferUpdateAccountPeersFunc func(ctx context.Context, accountID string)
|
||||
RecalculateNetworkMapCacheFunc func(ctx context.Context, accountId string) error
|
||||
|
||||
GetIdentityProviderFunc func(ctx context.Context, accountID, idpID, userID string) (*types.IdentityProvider, error)
|
||||
GetIdentityProvidersFunc func(ctx context.Context, accountID, userID string) ([]*types.IdentityProvider, error)
|
||||
CreateIdentityProviderFunc func(ctx context.Context, accountID, userID string, idp *types.IdentityProvider) (*types.IdentityProvider, error)
|
||||
UpdateIdentityProviderFunc func(ctx context.Context, accountID, idpID, userID string, idp *types.IdentityProvider) (*types.IdentityProvider, error)
|
||||
DeleteIdentityProviderFunc func(ctx context.Context, accountID, idpID, userID string) error
|
||||
}
|
||||
|
||||
func (am *MockAccountManager) CreateGroup(ctx context.Context, accountID, userID string, group *types.Group) error {
|
||||
@@ -988,3 +994,43 @@ func (am *MockAccountManager) RecalculateNetworkMapCache(ctx context.Context, ac
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIdentityProvider mocks GetIdentityProvider of the AccountManager interface
|
||||
func (am *MockAccountManager) GetIdentityProvider(ctx context.Context, accountID, idpID, userID string) (*types.IdentityProvider, error) {
|
||||
if am.GetIdentityProviderFunc != nil {
|
||||
return am.GetIdentityProviderFunc(ctx, accountID, idpID, userID)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetIdentityProvider is not implemented")
|
||||
}
|
||||
|
||||
// GetIdentityProviders mocks GetIdentityProviders of the AccountManager interface
|
||||
func (am *MockAccountManager) GetIdentityProviders(ctx context.Context, accountID, userID string) ([]*types.IdentityProvider, error) {
|
||||
if am.GetIdentityProvidersFunc != nil {
|
||||
return am.GetIdentityProvidersFunc(ctx, accountID, userID)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetIdentityProviders is not implemented")
|
||||
}
|
||||
|
||||
// CreateIdentityProvider mocks CreateIdentityProvider of the AccountManager interface
|
||||
func (am *MockAccountManager) CreateIdentityProvider(ctx context.Context, accountID, userID string, idp *types.IdentityProvider) (*types.IdentityProvider, error) {
|
||||
if am.CreateIdentityProviderFunc != nil {
|
||||
return am.CreateIdentityProviderFunc(ctx, accountID, userID, idp)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateIdentityProvider is not implemented")
|
||||
}
|
||||
|
||||
// UpdateIdentityProvider mocks UpdateIdentityProvider of the AccountManager interface
|
||||
func (am *MockAccountManager) UpdateIdentityProvider(ctx context.Context, accountID, idpID, userID string, idp *types.IdentityProvider) (*types.IdentityProvider, error) {
|
||||
if am.UpdateIdentityProviderFunc != nil {
|
||||
return am.UpdateIdentityProviderFunc(ctx, accountID, idpID, userID, idp)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateIdentityProvider is not implemented")
|
||||
}
|
||||
|
||||
// DeleteIdentityProvider mocks DeleteIdentityProvider of the AccountManager interface
|
||||
func (am *MockAccountManager) DeleteIdentityProvider(ctx context.Context, accountID, idpID, userID string) error {
|
||||
if am.DeleteIdentityProviderFunc != nil {
|
||||
return am.DeleteIdentityProviderFunc(ctx, accountID, idpID, userID)
|
||||
}
|
||||
return status.Errorf(codes.Unimplemented, "method DeleteIdentityProvider is not implemented")
|
||||
}
|
||||
|
||||
@@ -865,7 +865,7 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*types.Account,
|
||||
userID := testUserID
|
||||
domain := "example.com"
|
||||
|
||||
account := newAccountWithId(context.Background(), accountID, userID, domain, false)
|
||||
account := newAccountWithId(context.Background(), accountID, userID, domain, "", "", false)
|
||||
|
||||
account.NameServerGroups[existingNSGroup.ID] = &existingNSGroup
|
||||
|
||||
|
||||
@@ -502,7 +502,7 @@ func TestDefaultAccountManager_GetPeer(t *testing.T) {
|
||||
accountID := "test_account"
|
||||
adminUser := "account_creator"
|
||||
someUser := "some_user"
|
||||
account := newAccountWithId(context.Background(), accountID, adminUser, "", false)
|
||||
account := newAccountWithId(context.Background(), accountID, adminUser, "", "", "", false)
|
||||
account.Users[someUser] = &types.User{
|
||||
Id: someUser,
|
||||
Role: types.UserRoleUser,
|
||||
@@ -689,7 +689,7 @@ func TestDefaultAccountManager_GetPeers(t *testing.T) {
|
||||
accountID := "test_account"
|
||||
adminUser := "account_creator"
|
||||
someUser := "some_user"
|
||||
account := newAccountWithId(context.Background(), accountID, adminUser, "", false)
|
||||
account := newAccountWithId(context.Background(), accountID, adminUser, "", "", "", false)
|
||||
account.Users[someUser] = &types.User{
|
||||
Id: someUser,
|
||||
Role: testCase.role,
|
||||
@@ -759,7 +759,7 @@ func setupTestAccountManager(b testing.TB, peers int, groups int) (*DefaultAccou
|
||||
adminUser := "account_creator"
|
||||
regularUser := "regular_user"
|
||||
|
||||
account := newAccountWithId(context.Background(), accountID, adminUser, "", false)
|
||||
account := newAccountWithId(context.Background(), accountID, adminUser, "", "", "", false)
|
||||
account.Users[regularUser] = &types.User{
|
||||
Id: regularUser,
|
||||
Role: types.UserRoleUser,
|
||||
@@ -2124,7 +2124,7 @@ func Test_DeletePeer(t *testing.T) {
|
||||
// account with an admin and a regular user
|
||||
accountID := "test_account"
|
||||
adminUser := "account_creator"
|
||||
account := newAccountWithId(context.Background(), accountID, adminUser, "", false)
|
||||
account := newAccountWithId(context.Background(), accountID, adminUser, "", "", "", false)
|
||||
account.Peers = map[string]*nbpeer.Peer{
|
||||
"peer1": {
|
||||
ID: "peer1",
|
||||
@@ -2307,12 +2307,12 @@ func TestAddPeer_UserPendingApprovalBlocked(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create account
|
||||
account := newAccountWithId(context.Background(), "test-account", "owner", "", false)
|
||||
account := newAccountWithId(context.Background(), "test-account", "owner", "", "", "", false)
|
||||
err = manager.Store.SaveAccount(context.Background(), account)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create user pending approval
|
||||
pendingUser := types.NewRegularUser("pending-user")
|
||||
pendingUser := types.NewRegularUser("pending-user", "", "")
|
||||
pendingUser.AccountID = account.Id
|
||||
pendingUser.Blocked = true
|
||||
pendingUser.PendingApproval = true
|
||||
@@ -2344,12 +2344,12 @@ func TestAddPeer_ApprovedUserCanAddPeers(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create account
|
||||
account := newAccountWithId(context.Background(), "test-account", "owner", "", false)
|
||||
account := newAccountWithId(context.Background(), "test-account", "owner", "", "", "", false)
|
||||
err = manager.Store.SaveAccount(context.Background(), account)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create regular user (not pending approval)
|
||||
regularUser := types.NewRegularUser("regular-user")
|
||||
regularUser := types.NewRegularUser("regular-user", "", "")
|
||||
regularUser.AccountID = account.Id
|
||||
err = manager.Store.SaveUser(context.Background(), regularUser)
|
||||
require.NoError(t, err)
|
||||
@@ -2378,12 +2378,12 @@ func TestLoginPeer_UserPendingApprovalBlocked(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create account
|
||||
account := newAccountWithId(context.Background(), "test-account", "owner", "", false)
|
||||
account := newAccountWithId(context.Background(), "test-account", "owner", "", "", "", false)
|
||||
err = manager.Store.SaveAccount(context.Background(), account)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create user pending approval
|
||||
pendingUser := types.NewRegularUser("pending-user")
|
||||
pendingUser := types.NewRegularUser("pending-user", "", "")
|
||||
pendingUser.AccountID = account.Id
|
||||
pendingUser.Blocked = true
|
||||
pendingUser.PendingApproval = true
|
||||
@@ -2443,12 +2443,12 @@ func TestLoginPeer_ApprovedUserCanLogin(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create account
|
||||
account := newAccountWithId(context.Background(), "test-account", "owner", "", false)
|
||||
account := newAccountWithId(context.Background(), "test-account", "owner", "", "", "", false)
|
||||
err = manager.Store.SaveAccount(context.Background(), account)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create regular user (not pending approval)
|
||||
regularUser := types.NewRegularUser("regular-user")
|
||||
regularUser := types.NewRegularUser("regular-user", "", "")
|
||||
regularUser.AccountID = account.Id
|
||||
err = manager.Store.SaveUser(context.Background(), regularUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -109,7 +109,7 @@ func initTestPostureChecksAccount(am *DefaultAccountManager) (*types.Account, er
|
||||
ID: "peer1",
|
||||
}
|
||||
|
||||
account := newAccountWithId(context.Background(), accountID, groupAdminUserID, domain, false)
|
||||
account := newAccountWithId(context.Background(), accountID, groupAdminUserID, domain, "", "", false)
|
||||
account.Users[admin.Id] = admin
|
||||
account.Users[user.Id] = user
|
||||
account.Peers["peer1"] = peer1
|
||||
|
||||
@@ -1320,7 +1320,7 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
|
||||
accountID := "testingAcc"
|
||||
domain := "example.com"
|
||||
|
||||
account := newAccountWithId(context.Background(), accountID, userID, domain, false)
|
||||
account := newAccountWithId(context.Background(), accountID, userID, domain, "", "", false)
|
||||
err := am.Store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
nbutil "github.com/netbirdio/netbird/management/server/util"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
"github.com/netbirdio/netbird/util/crypt"
|
||||
)
|
||||
|
||||
// storeFileName Store file name. Stored in the datadir
|
||||
@@ -263,3 +264,7 @@ func (s *FileStore) Close(ctx context.Context) error {
|
||||
func (s *FileStore) GetStoreEngine() types.Engine {
|
||||
return types.FileStoreEngine
|
||||
}
|
||||
|
||||
// SetFieldEncrypt is a no-op for FileStore as it doesn't support field encryption.
|
||||
func (s *FileStore) SetFieldEncrypt(_ *crypt.FieldEncrypt) {
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/util"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
"github.com/netbirdio/netbird/util/crypt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -63,6 +64,7 @@ type SqlStore struct {
|
||||
installationPK int
|
||||
storeEngine types.Engine
|
||||
pool *pgxpool.Pool
|
||||
fieldEncrypt *crypt.FieldEncrypt
|
||||
}
|
||||
|
||||
type installation struct {
|
||||
@@ -165,6 +167,13 @@ func (s *SqlStore) SaveAccount(ctx context.Context, account *types.Account) erro
|
||||
|
||||
generateAccountSQLTypes(account)
|
||||
|
||||
// Encrypt sensitive user data before saving
|
||||
for i := range account.UsersG {
|
||||
if err := account.UsersG[i].Encrypt(s.fieldEncrypt); err != nil {
|
||||
return fmt.Errorf("encrypt user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range account.GroupsG {
|
||||
group.StoreGroupPeers()
|
||||
}
|
||||
@@ -430,7 +439,18 @@ func (s *SqlStore) SaveUsers(ctx context.Context, users []*types.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&users)
|
||||
usersCopy := make([]*types.User, len(users))
|
||||
for i, user := range users {
|
||||
userCopy := user.Copy()
|
||||
userCopy.Email = user.Email
|
||||
userCopy.Name = user.Name
|
||||
if err := userCopy.Encrypt(s.fieldEncrypt); err != nil {
|
||||
return fmt.Errorf("encrypt user: %w", err)
|
||||
}
|
||||
usersCopy[i] = userCopy
|
||||
}
|
||||
|
||||
result := s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&usersCopy)
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to save users to store: %s", result.Error)
|
||||
return status.Errorf(status.Internal, "failed to save users to store")
|
||||
@@ -440,7 +460,15 @@ func (s *SqlStore) SaveUsers(ctx context.Context, users []*types.User) error {
|
||||
|
||||
// SaveUser saves the given user to the database.
|
||||
func (s *SqlStore) SaveUser(ctx context.Context, user *types.User) error {
|
||||
result := s.db.Save(user)
|
||||
userCopy := user.Copy()
|
||||
userCopy.Email = user.Email
|
||||
userCopy.Name = user.Name
|
||||
|
||||
if err := userCopy.Encrypt(s.fieldEncrypt); err != nil {
|
||||
return fmt.Errorf("encrypt user: %w", err)
|
||||
}
|
||||
|
||||
result := s.db.Save(userCopy)
|
||||
if result.Error != nil {
|
||||
log.WithContext(ctx).Errorf("failed to save user to store: %s", result.Error)
|
||||
return status.Errorf(status.Internal, "failed to save user to store")
|
||||
@@ -590,6 +618,10 @@ func (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStren
|
||||
return nil, status.NewGetUserFromStoreError()
|
||||
}
|
||||
|
||||
if err := user.Decrypt(s.fieldEncrypt); err != nil {
|
||||
return nil, fmt.Errorf("decrypt user: %w", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
@@ -608,6 +640,10 @@ func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStre
|
||||
return nil, status.NewGetUserFromStoreError()
|
||||
}
|
||||
|
||||
if err := user.Decrypt(s.fieldEncrypt); err != nil {
|
||||
return nil, fmt.Errorf("decrypt user: %w", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
@@ -644,6 +680,12 @@ func (s *SqlStore) GetAccountUsers(ctx context.Context, lockStrength LockingStre
|
||||
return nil, status.Errorf(status.Internal, "issue getting users from store")
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if err := user.Decrypt(s.fieldEncrypt); err != nil {
|
||||
return nil, fmt.Errorf("decrypt user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
@@ -662,6 +704,10 @@ func (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStre
|
||||
return nil, status.Errorf(status.Internal, "failed to get account owner from the store")
|
||||
}
|
||||
|
||||
if err := user.Decrypt(s.fieldEncrypt); err != nil {
|
||||
return nil, fmt.Errorf("decrypt user: %w", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
@@ -856,6 +902,9 @@ func (s *SqlStore) getAccountGorm(ctx context.Context, accountID string) (*types
|
||||
if user.AutoGroups == nil {
|
||||
user.AutoGroups = []string{}
|
||||
}
|
||||
if err := user.Decrypt(s.fieldEncrypt); err != nil {
|
||||
return nil, fmt.Errorf("decrypt user: %w", err)
|
||||
}
|
||||
account.Users[user.Id] = &user
|
||||
user.PATsG = nil
|
||||
}
|
||||
@@ -1131,6 +1180,9 @@ func (s *SqlStore) getAccountPgx(ctx context.Context, accountID string) (*types.
|
||||
account.Users = make(map[string]*types.User, len(account.UsersG))
|
||||
for i := range account.UsersG {
|
||||
user := &account.UsersG[i]
|
||||
if err := user.Decrypt(s.fieldEncrypt); err != nil {
|
||||
return nil, fmt.Errorf("decrypt user: %w", err)
|
||||
}
|
||||
user.PATs = make(map[string]*types.PersonalAccessToken)
|
||||
if userPats, ok := patsByUserID[user.Id]; ok {
|
||||
for j := range userPats {
|
||||
@@ -2983,6 +3035,11 @@ func (s *SqlStore) GetDB() *gorm.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
// SetFieldEncrypt sets the field encryptor for encrypting sensitive user data.
|
||||
func (s *SqlStore) SetFieldEncrypt(enc *crypt.FieldEncrypt) {
|
||||
s.fieldEncrypt = enc
|
||||
}
|
||||
|
||||
func (s *SqlStore) GetAccountDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.DNSSettings, error) {
|
||||
tx := s.db
|
||||
if lockStrength != LockingStrengthNone {
|
||||
|
||||
@@ -2090,7 +2090,7 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain string) *ty
|
||||
setupKeys := map[string]*types.SetupKey{}
|
||||
nameServersGroups := make(map[string]*nbdns.NameServerGroup)
|
||||
|
||||
owner := types.NewOwnerUser(userID)
|
||||
owner := types.NewOwnerUser(userID, "", "")
|
||||
owner.AccountID = accountID
|
||||
users[userID] = owner
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/testutil"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
"github.com/netbirdio/netbird/util/crypt"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/migration"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
@@ -204,6 +205,9 @@ type Store interface {
|
||||
MarkAccountPrimary(ctx context.Context, accountID string) error
|
||||
UpdateAccountNetwork(ctx context.Context, accountID string, ipNet net.IPNet) error
|
||||
GetPolicyRulesByResourceID(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) ([]*types.PolicyRule, error)
|
||||
|
||||
// SetFieldEncrypt sets the field encryptor for encrypting sensitive user data.
|
||||
SetFieldEncrypt(enc *crypt.FieldEncrypt)
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
64
management/server/types/identity_provider.go
Normal file
64
management/server/types/identity_provider.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package types
|
||||
|
||||
// IdentityProviderType is the type of identity provider
|
||||
type IdentityProviderType string
|
||||
|
||||
const (
|
||||
// IdentityProviderTypeOIDC is a generic OIDC identity provider
|
||||
IdentityProviderTypeOIDC IdentityProviderType = "oidc"
|
||||
// IdentityProviderTypeZitadel is the Zitadel identity provider
|
||||
IdentityProviderTypeZitadel IdentityProviderType = "zitadel"
|
||||
// IdentityProviderTypeEntra is the Microsoft Entra (Azure AD) identity provider
|
||||
IdentityProviderTypeEntra IdentityProviderType = "entra"
|
||||
// IdentityProviderTypeGoogle is the Google identity provider
|
||||
IdentityProviderTypeGoogle IdentityProviderType = "google"
|
||||
// IdentityProviderTypeOkta is the Okta identity provider
|
||||
IdentityProviderTypeOkta IdentityProviderType = "okta"
|
||||
// IdentityProviderTypePocketID is the PocketID identity provider
|
||||
IdentityProviderTypePocketID IdentityProviderType = "pocketid"
|
||||
// IdentityProviderTypeMicrosoft is the Microsoft identity provider
|
||||
IdentityProviderTypeMicrosoft IdentityProviderType = "microsoft"
|
||||
)
|
||||
|
||||
// IdentityProvider represents an identity provider configuration
|
||||
type IdentityProvider struct {
|
||||
// ID is the unique identifier of the identity provider
|
||||
ID string `gorm:"primaryKey"`
|
||||
// AccountID is a reference to Account that this object belongs
|
||||
AccountID string `json:"-" gorm:"index"`
|
||||
// Type is the type of identity provider
|
||||
Type IdentityProviderType
|
||||
// Name is a human-readable name for the identity provider
|
||||
Name string
|
||||
// Issuer is the OIDC issuer URL
|
||||
Issuer string
|
||||
// ClientID is the OAuth2 client ID
|
||||
ClientID string
|
||||
// ClientSecret is the OAuth2 client secret
|
||||
ClientSecret string
|
||||
// RedirectURL is the OAuth2 redirect URL for configuring the identity provider (not stored in DB)
|
||||
RedirectURL string `gorm:"-"`
|
||||
}
|
||||
|
||||
// Copy returns a copy of the IdentityProvider
|
||||
func (idp *IdentityProvider) Copy() *IdentityProvider {
|
||||
return &IdentityProvider{
|
||||
ID: idp.ID,
|
||||
AccountID: idp.AccountID,
|
||||
Type: idp.Type,
|
||||
Name: idp.Name,
|
||||
Issuer: idp.Issuer,
|
||||
ClientID: idp.ClientID,
|
||||
ClientSecret: idp.ClientSecret,
|
||||
RedirectURL: idp.RedirectURL,
|
||||
}
|
||||
}
|
||||
|
||||
// EventMeta returns a map of metadata for activity events
|
||||
func (idp *IdentityProvider) EventMeta() map[string]any {
|
||||
return map[string]any{
|
||||
"name": idp.Name,
|
||||
"type": string(idp.Type),
|
||||
"issuer": idp.Issuer,
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/integration_reference"
|
||||
"github.com/netbirdio/netbird/util/crypt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -65,7 +66,11 @@ type UserInfo struct {
|
||||
LastLogin time.Time `json:"last_login"`
|
||||
Issued string `json:"issued"`
|
||||
PendingApproval bool `json:"pending_approval"`
|
||||
Password string `json:"password"`
|
||||
IntegrationReference integration_reference.IntegrationReference `json:"-"`
|
||||
// IdPID is the identity provider ID (connector ID) extracted from the Dex-encoded user ID.
|
||||
// This field is only populated when the user ID can be decoded from Dex's format.
|
||||
IdPID string `json:"idp_id,omitempty"`
|
||||
}
|
||||
|
||||
// User represents a user of the system
|
||||
@@ -96,6 +101,9 @@ type User struct {
|
||||
Issued string `gorm:"default:api"`
|
||||
|
||||
IntegrationReference integration_reference.IntegrationReference `gorm:"embedded;embeddedPrefix:integration_ref_"`
|
||||
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
// IsBlocked returns true if the user is blocked, false otherwise
|
||||
@@ -143,10 +151,16 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
|
||||
}
|
||||
|
||||
if userData == nil {
|
||||
|
||||
name := u.Name
|
||||
if u.IsServiceUser {
|
||||
name = u.ServiceUserName
|
||||
}
|
||||
|
||||
return &UserInfo{
|
||||
ID: u.Id,
|
||||
Email: "",
|
||||
Name: u.ServiceUserName,
|
||||
Email: u.Email,
|
||||
Name: name,
|
||||
Role: string(u.Role),
|
||||
AutoGroups: u.AutoGroups,
|
||||
Status: string(UserStatusActive),
|
||||
@@ -178,6 +192,7 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
|
||||
LastLogin: u.GetLastLogin(),
|
||||
Issued: u.Issued,
|
||||
PendingApproval: u.PendingApproval,
|
||||
Password: userData.Password,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -204,11 +219,13 @@ func (u *User) Copy() *User {
|
||||
CreatedAt: u.CreatedAt,
|
||||
Issued: u.Issued,
|
||||
IntegrationReference: u.IntegrationReference,
|
||||
Email: u.Email,
|
||||
Name: u.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// NewUser creates a new user
|
||||
func NewUser(id string, role UserRole, isServiceUser bool, nonDeletable bool, serviceUserName string, autoGroups []string, issued string) *User {
|
||||
func NewUser(id string, role UserRole, isServiceUser bool, nonDeletable bool, serviceUserName string, autoGroups []string, issued string, email string, name string) *User {
|
||||
return &User{
|
||||
Id: id,
|
||||
Role: role,
|
||||
@@ -218,20 +235,70 @@ func NewUser(id string, role UserRole, isServiceUser bool, nonDeletable bool, se
|
||||
AutoGroups: autoGroups,
|
||||
Issued: issued,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Name: name,
|
||||
Email: email,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRegularUser creates a new user with role UserRoleUser
|
||||
func NewRegularUser(id string) *User {
|
||||
return NewUser(id, UserRoleUser, false, false, "", []string{}, UserIssuedAPI)
|
||||
func NewRegularUser(id, email, name string) *User {
|
||||
return NewUser(id, UserRoleUser, false, false, "", []string{}, UserIssuedAPI, email, name)
|
||||
}
|
||||
|
||||
// NewAdminUser creates a new user with role UserRoleAdmin
|
||||
func NewAdminUser(id string) *User {
|
||||
return NewUser(id, UserRoleAdmin, false, false, "", []string{}, UserIssuedAPI)
|
||||
return NewUser(id, UserRoleAdmin, false, false, "", []string{}, UserIssuedAPI, "", "")
|
||||
}
|
||||
|
||||
// NewOwnerUser creates a new user with role UserRoleOwner
|
||||
func NewOwnerUser(id string) *User {
|
||||
return NewUser(id, UserRoleOwner, false, false, "", []string{}, UserIssuedAPI)
|
||||
func NewOwnerUser(id string, email string, name string) *User {
|
||||
return NewUser(id, UserRoleOwner, false, false, "", []string{}, UserIssuedAPI, email, name)
|
||||
}
|
||||
|
||||
// Encrypt encrypts the user's sensitive fields (Email and Name) in place.
|
||||
func (u *User) Encrypt(enc *crypt.FieldEncrypt) error {
|
||||
if enc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
if u.Email != "" {
|
||||
u.Email, err = enc.Encrypt(u.Email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt email: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if u.Name != "" {
|
||||
u.Name, err = enc.Encrypt(u.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt name: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts the user's sensitive fields (Email and Name) in place.
|
||||
func (u *User) Decrypt(enc *crypt.FieldEncrypt) error {
|
||||
if enc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
if u.Email != "" {
|
||||
u.Email, err = enc.Decrypt(u.Email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt email: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if u.Name != "" {
|
||||
u.Name, err = enc.Decrypt(u.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt name: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/idp/dex"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
@@ -40,7 +41,7 @@ func (am *DefaultAccountManager) createServiceUser(ctx context.Context, accountI
|
||||
}
|
||||
|
||||
newUserID := uuid.New().String()
|
||||
newUser := types.NewUser(newUserID, role, true, nonDeletable, serviceUserName, autoGroups, types.UserIssuedAPI)
|
||||
newUser := types.NewUser(newUserID, role, true, nonDeletable, serviceUserName, autoGroups, types.UserIssuedAPI, "", "")
|
||||
newUser.AccountID = accountID
|
||||
log.WithContext(ctx).Debugf("New User: %v", newUser)
|
||||
|
||||
@@ -104,7 +105,12 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
|
||||
inviterID = createdBy
|
||||
}
|
||||
|
||||
idpUser, err := am.createNewIdpUser(ctx, accountID, inviterID, invite)
|
||||
var idpUser *idp.UserData
|
||||
if isEmbeddedIdp(am.idpManager) {
|
||||
idpUser, err = am.createEmbeddedIdpUser(ctx, accountID, inviterID, invite)
|
||||
} else {
|
||||
idpUser, err = am.createNewIdpUser(ctx, accountID, inviterID, invite)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -117,15 +123,19 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
|
||||
Issued: invite.Issued,
|
||||
IntegrationReference: invite.IntegrationReference,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Email: invite.Email,
|
||||
Name: invite.Name,
|
||||
}
|
||||
|
||||
if err = am.Store.SaveUser(ctx, newUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = am.refreshCache(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !isEmbeddedIdp(am.idpManager) {
|
||||
_, err = am.refreshCache(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
am.StoreEvent(ctx, userID, newUser.Id, accountID, activity.UserInvited, nil)
|
||||
@@ -172,6 +182,34 @@ func (am *DefaultAccountManager) createNewIdpUser(ctx context.Context, accountID
|
||||
return am.idpManager.CreateUser(ctx, invite.Email, invite.Name, accountID, inviterUser.Email)
|
||||
}
|
||||
|
||||
// createEmbeddedIdpUser validates the invite and creates a new user in the embedded IdP.
|
||||
// Unlike createNewIdpUser, this method fetches user data directly from the database
|
||||
// since the embedded IdP usage ensures the username and email are stored locally in the User table.
|
||||
func (am *DefaultAccountManager) createEmbeddedIdpUser(ctx context.Context, accountID string, inviterID string, invite *types.UserInfo) (*idp.UserData, error) {
|
||||
inviter, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, inviterID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get inviter user: %w", err)
|
||||
}
|
||||
|
||||
if inviter == nil {
|
||||
return nil, status.Errorf(status.NotFound, "inviter user with ID %s doesn't exist", inviterID)
|
||||
}
|
||||
|
||||
// check if the user is already registered with this email => reject
|
||||
existingUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthNone, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range existingUsers {
|
||||
if strings.ToLower(user.Email) == strings.ToLower(invite.Email) {
|
||||
return nil, status.Errorf(status.UserAlreadyExists, "can't invite a user with an existing NetBird account")
|
||||
}
|
||||
}
|
||||
|
||||
return am.idpManager.CreateUser(ctx, invite.Email, invite.Name, accountID, inviter.Email)
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) GetUserByID(ctx context.Context, id string) (*types.User, error) {
|
||||
return am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, id)
|
||||
}
|
||||
@@ -761,7 +799,7 @@ func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initi
|
||||
// If the AccountManager has a non-nil idpManager and the User is not a service user,
|
||||
// it will attempt to look up the UserData from the cache.
|
||||
func (am *DefaultAccountManager) getUserInfo(ctx context.Context, user *types.User, accountID string) (*types.UserInfo, error) {
|
||||
if !isNil(am.idpManager) && !user.IsServiceUser {
|
||||
if !isNil(am.idpManager) && !user.IsServiceUser && !isEmbeddedIdp(am.idpManager) {
|
||||
userData, err := am.lookupUserInCache(ctx, user.Id, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -823,7 +861,7 @@ func (am *DefaultAccountManager) GetOrCreateAccountByUser(ctx context.Context, u
|
||||
account, err := am.Store.GetAccountByUser(ctx, userID)
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Type() == status.NotFound {
|
||||
account, err = am.newAccount(ctx, userID, lowerDomain)
|
||||
account, err = am.newAccount(ctx, userID, lowerDomain, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -888,7 +926,8 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
|
||||
var queriedUsers []*idp.UserData
|
||||
var err error
|
||||
|
||||
if !isNil(am.idpManager) {
|
||||
// embedded IdP ensures that we have user data (email and name) stored in the database.
|
||||
if !isNil(am.idpManager) && !isEmbeddedIdp(am.idpManager) {
|
||||
users := make(map[string]userLoggedInOnce, len(accountUsers))
|
||||
usersFromIntegration := make([]*idp.UserData, 0)
|
||||
for _, user := range accountUsers {
|
||||
@@ -925,6 +964,10 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Try to decode Dex user ID to extract the IdP ID (connector ID)
|
||||
if _, connectorID, decodeErr := dex.DecodeDexUserID(accountUser.Id); decodeErr == nil && connectorID != "" {
|
||||
info.IdPID = connectorID
|
||||
}
|
||||
userInfosMap[accountUser.Id] = info
|
||||
}
|
||||
|
||||
@@ -946,7 +989,7 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
|
||||
|
||||
info = &types.UserInfo{
|
||||
ID: localUser.Id,
|
||||
Email: "",
|
||||
Email: localUser.Email,
|
||||
Name: name,
|
||||
Role: string(localUser.Role),
|
||||
AutoGroups: localUser.AutoGroups,
|
||||
@@ -955,6 +998,10 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
|
||||
NonDeletable: localUser.NonDeletable,
|
||||
}
|
||||
}
|
||||
// Try to decode Dex user ID to extract the IdP ID (connector ID)
|
||||
if _, connectorID, decodeErr := dex.DecodeDexUserID(localUser.Id); decodeErr == nil && connectorID != "" {
|
||||
info.IdPID = connectorID
|
||||
}
|
||||
userInfosMap[info.ID] = info
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -29,6 +30,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
|
||||
"github.com/netbirdio/netbird/idp/dex"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/integration_reference"
|
||||
@@ -58,7 +60,7 @@ func TestUser_CreatePAT_ForSameUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
err = s.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
@@ -105,7 +107,7 @@ func TestUser_CreatePAT_ForDifferentUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
account.Users[mockTargetUserId] = &types.User{
|
||||
Id: mockTargetUserId,
|
||||
IsServiceUser: false,
|
||||
@@ -133,7 +135,7 @@ func TestUser_CreatePAT_ForServiceUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
account.Users[mockTargetUserId] = &types.User{
|
||||
Id: mockTargetUserId,
|
||||
IsServiceUser: true,
|
||||
@@ -165,7 +167,7 @@ func TestUser_CreatePAT_WithWrongExpiration(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
@@ -190,7 +192,7 @@ func TestUser_CreatePAT_WithEmptyName(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
@@ -215,7 +217,7 @@ func TestUser_DeletePAT(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
account.Users[mockUserID] = &types.User{
|
||||
Id: mockUserID,
|
||||
PATs: map[string]*types.PersonalAccessToken{
|
||||
@@ -258,7 +260,7 @@ func TestUser_GetPAT(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
account.Users[mockUserID] = &types.User{
|
||||
Id: mockUserID,
|
||||
AccountID: mockAccountID,
|
||||
@@ -298,7 +300,7 @@ func TestUser_GetAllPATs(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
account.Users[mockUserID] = &types.User{
|
||||
Id: mockUserID,
|
||||
AccountID: mockAccountID,
|
||||
@@ -362,6 +364,8 @@ func TestUser_Copy(t *testing.T) {
|
||||
ID: 0,
|
||||
IntegrationType: "test",
|
||||
},
|
||||
Email: "whatever@gmail.com",
|
||||
Name: "John Doe",
|
||||
}
|
||||
|
||||
err := validateStruct(user)
|
||||
@@ -408,7 +412,7 @@ func TestUser_CreateServiceUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
@@ -455,7 +459,7 @@ func TestUser_CreateUser_ServiceUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
@@ -503,7 +507,7 @@ func TestUser_CreateUser_RegularUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
@@ -534,7 +538,7 @@ func TestUser_InviteNewUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
@@ -641,7 +645,7 @@ func TestUser_DeleteUser_ServiceUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
account.Users[mockServiceUserID] = tt.serviceUser
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
@@ -680,7 +684,7 @@ func TestUser_DeleteUser_SelfDelete(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
@@ -707,7 +711,7 @@ func TestUser_DeleteUser_regularUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
targetId := "user2"
|
||||
account.Users[targetId] = &types.User{
|
||||
@@ -801,7 +805,7 @@ func TestUser_DeleteUser_RegularUsers(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
targetId := "user2"
|
||||
account.Users[targetId] = &types.User{
|
||||
@@ -969,7 +973,7 @@ func TestDefaultAccountManager_GetUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
@@ -1005,9 +1009,9 @@ func TestDefaultAccountManager_ListUsers(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account.Users["normal_user1"] = types.NewRegularUser("normal_user1")
|
||||
account.Users["normal_user2"] = types.NewRegularUser("normal_user2")
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
account.Users["normal_user1"] = types.NewRegularUser("normal_user1", "", "")
|
||||
account.Users["normal_user2"] = types.NewRegularUser("normal_user2", "", "")
|
||||
|
||||
err = store.SaveAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
@@ -1047,7 +1051,7 @@ func TestDefaultAccountManager_ExternalCache(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
externalUser := &types.User{
|
||||
Id: "externalUser",
|
||||
Role: types.UserRoleUser,
|
||||
@@ -1104,7 +1108,7 @@ func TestUser_IsAdmin(t *testing.T) {
|
||||
user := types.NewAdminUser(mockUserID)
|
||||
assert.True(t, user.HasAdminPower())
|
||||
|
||||
user = types.NewRegularUser(mockUserID)
|
||||
user = types.NewRegularUser(mockUserID, "", "")
|
||||
assert.False(t, user.HasAdminPower())
|
||||
}
|
||||
|
||||
@@ -1115,7 +1119,7 @@ func TestUser_GetUsersFromAccount_ForAdmin(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
account.Users[mockServiceUserID] = &types.User{
|
||||
Id: mockServiceUserID,
|
||||
Role: "user",
|
||||
@@ -1149,7 +1153,7 @@ func TestUser_GetUsersFromAccount_ForUser(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", false)
|
||||
account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "", "", "", false)
|
||||
account.Users[mockServiceUserID] = &types.User{
|
||||
Id: mockServiceUserID,
|
||||
Role: "user",
|
||||
@@ -1326,7 +1330,7 @@ func TestDefaultAccountManager_SaveUser(t *testing.T) {
|
||||
}
|
||||
|
||||
// create other users
|
||||
account.Users[regularUserID] = types.NewRegularUser(regularUserID)
|
||||
account.Users[regularUserID] = types.NewRegularUser(regularUserID, "", "")
|
||||
account.Users[adminUserID] = types.NewAdminUser(adminUserID)
|
||||
account.Users[serviceUserID] = &types.User{IsServiceUser: true, Id: serviceUserID, Role: types.UserRoleAdmin, ServiceUserName: "service"}
|
||||
err = manager.Store.SaveAccount(context.Background(), account)
|
||||
@@ -1516,7 +1520,7 @@ func TestSaveOrAddUser_PreventAccountSwitch(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account1 := newAccountWithId(context.Background(), "account1", "ownerAccount1", "", false)
|
||||
account1 := newAccountWithId(context.Background(), "account1", "ownerAccount1", "", "", "", false)
|
||||
targetId := "user2"
|
||||
account1.Users[targetId] = &types.User{
|
||||
Id: targetId,
|
||||
@@ -1525,7 +1529,7 @@ func TestSaveOrAddUser_PreventAccountSwitch(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, s.SaveAccount(context.Background(), account1))
|
||||
|
||||
account2 := newAccountWithId(context.Background(), "account2", "ownerAccount2", "", false)
|
||||
account2 := newAccountWithId(context.Background(), "account2", "ownerAccount2", "", "", "", false)
|
||||
require.NoError(t, s.SaveAccount(context.Background(), account2))
|
||||
|
||||
permissionsManager := permissions.NewManager(s)
|
||||
@@ -1552,7 +1556,7 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
account1 := newAccountWithId(context.Background(), "account1", "account1Owner", "", false)
|
||||
account1 := newAccountWithId(context.Background(), "account1", "account1Owner", "", "", "", false)
|
||||
account1.Settings.RegularUsersViewBlocked = false
|
||||
account1.Users["blocked-user"] = &types.User{
|
||||
Id: "blocked-user",
|
||||
@@ -1574,7 +1578,7 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, store.SaveAccount(context.Background(), account1))
|
||||
|
||||
account2 := newAccountWithId(context.Background(), "account2", "account2Owner", "", false)
|
||||
account2 := newAccountWithId(context.Background(), "account2", "account2Owner", "", "", "", false)
|
||||
account2.Users["settings-blocked-user"] = &types.User{
|
||||
Id: "settings-blocked-user",
|
||||
Role: types.UserRoleUser,
|
||||
@@ -1771,7 +1775,7 @@ func TestApproveUser(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create account with admin and pending approval user
|
||||
account := newAccountWithId(context.Background(), "account-1", "admin-user", "example.com", false)
|
||||
account := newAccountWithId(context.Background(), "account-1", "admin-user", "example.com", "", "", false)
|
||||
err = manager.Store.SaveAccount(context.Background(), account)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1782,7 +1786,7 @@ func TestApproveUser(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create user pending approval
|
||||
pendingUser := types.NewRegularUser("pending-user")
|
||||
pendingUser := types.NewRegularUser("pending-user", "", "")
|
||||
pendingUser.AccountID = account.Id
|
||||
pendingUser.Blocked = true
|
||||
pendingUser.PendingApproval = true
|
||||
@@ -1807,12 +1811,12 @@ func TestApproveUser(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "not pending approval")
|
||||
|
||||
// Test approval by non-admin should fail
|
||||
regularUser := types.NewRegularUser("regular-user")
|
||||
regularUser := types.NewRegularUser("regular-user", "", "")
|
||||
regularUser.AccountID = account.Id
|
||||
err = manager.Store.SaveUser(context.Background(), regularUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
pendingUser2 := types.NewRegularUser("pending-user-2")
|
||||
pendingUser2 := types.NewRegularUser("pending-user-2", "", "")
|
||||
pendingUser2.AccountID = account.Id
|
||||
pendingUser2.Blocked = true
|
||||
pendingUser2.PendingApproval = true
|
||||
@@ -1830,7 +1834,7 @@ func TestRejectUser(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create account with admin and pending approval user
|
||||
account := newAccountWithId(context.Background(), "account-1", "admin-user", "example.com", false)
|
||||
account := newAccountWithId(context.Background(), "account-1", "admin-user", "example.com", "", "", false)
|
||||
err = manager.Store.SaveAccount(context.Background(), account)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1841,7 +1845,7 @@ func TestRejectUser(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create user pending approval
|
||||
pendingUser := types.NewRegularUser("pending-user")
|
||||
pendingUser := types.NewRegularUser("pending-user", "", "")
|
||||
pendingUser.AccountID = account.Id
|
||||
pendingUser.Blocked = true
|
||||
pendingUser.PendingApproval = true
|
||||
@@ -1857,7 +1861,7 @@ func TestRejectUser(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
|
||||
// Test rejection of non-pending user should fail
|
||||
regularUser := types.NewRegularUser("regular-user")
|
||||
regularUser := types.NewRegularUser("regular-user", "", "")
|
||||
regularUser.AccountID = account.Id
|
||||
err = manager.Store.SaveUser(context.Background(), regularUser)
|
||||
require.NoError(t, err)
|
||||
@@ -1867,7 +1871,7 @@ func TestRejectUser(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "not pending approval")
|
||||
|
||||
// Test rejection by non-admin should fail
|
||||
pendingUser2 := types.NewRegularUser("pending-user-2")
|
||||
pendingUser2 := types.NewRegularUser("pending-user-2", "", "")
|
||||
pendingUser2.AccountID = account.Id
|
||||
pendingUser2.Blocked = true
|
||||
pendingUser2.PendingApproval = true
|
||||
@@ -1877,3 +1881,147 @@ func TestRejectUser(t *testing.T) {
|
||||
err = manager.RejectUser(context.Background(), account.Id, regularUser.Id, pendingUser2.Id)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUser_CreateUser_WithEmbeddedIDP(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create temporary directory for Dex
|
||||
tmpDir := t.TempDir()
|
||||
dexDataDir := tmpDir + "/dex"
|
||||
require.NoError(t, os.MkdirAll(dexDataDir, 0700))
|
||||
|
||||
// Create the Dex provider
|
||||
provider, err := dex.NewProvider(ctx, &dex.Config{
|
||||
Issuer: "http://localhost:5556/dex",
|
||||
Port: 5556,
|
||||
DataDir: dexDataDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer provider.Stop(ctx)
|
||||
|
||||
// Create embedded IDP manager
|
||||
embeddedIdp, err := idp.NewEmbeddedIdPManager(idp.EmbeddedIdPConfig{
|
||||
Provider: provider,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create test store
|
||||
testStore, cleanup, err := store.NewTestStoreFromSQL(ctx, "", tmpDir)
|
||||
require.NoError(t, err)
|
||||
defer cleanup()
|
||||
|
||||
// Create account with owner user
|
||||
account := newAccountWithId(ctx, mockAccountID, mockUserID, "", "owner@test.com", "Owner User", false)
|
||||
require.NoError(t, testStore.SaveAccount(ctx, account))
|
||||
|
||||
// Create mock network map controller
|
||||
ctrl := gomock.NewController(t)
|
||||
networkMapControllerMock := network_map.NewMockController(ctrl)
|
||||
networkMapControllerMock.EXPECT().
|
||||
OnPeersDeleted(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil).
|
||||
AnyTimes()
|
||||
|
||||
// Create account manager with embedded IDP
|
||||
permissionsManager := permissions.NewManager(testStore)
|
||||
am := DefaultAccountManager{
|
||||
Store: testStore,
|
||||
eventStore: &activity.InMemoryEventStore{},
|
||||
permissionsManager: permissionsManager,
|
||||
idpManager: embeddedIdp,
|
||||
cacheLoading: map[string]chan struct{}{},
|
||||
networkMapController: networkMapControllerMock,
|
||||
}
|
||||
|
||||
// Initialize cache manager
|
||||
cacheStore, err := nbcache.NewStore(ctx, nbcache.DefaultIDPCacheExpirationMax, nbcache.DefaultIDPCacheCleanupInterval, nbcache.DefaultIDPCacheOpenConn)
|
||||
require.NoError(t, err)
|
||||
am.cacheManager = nbcache.NewAccountUserDataCache(am.loadAccount, cacheStore)
|
||||
am.externalCacheManager = nbcache.NewUserDataCache(cacheStore)
|
||||
|
||||
t.Run("create regular user returns password", func(t *testing.T) {
|
||||
userInfo, err := am.CreateUser(ctx, mockAccountID, mockUserID, &types.UserInfo{
|
||||
Email: "newuser@test.com",
|
||||
Name: "New User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
IsServiceUser: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, userInfo)
|
||||
|
||||
// Verify user data
|
||||
assert.Equal(t, "newuser@test.com", userInfo.Email)
|
||||
assert.Equal(t, "New User", userInfo.Name)
|
||||
assert.Equal(t, "user", userInfo.Role)
|
||||
assert.NotEmpty(t, userInfo.ID)
|
||||
|
||||
// IMPORTANT: Password should be returned for embedded IDP
|
||||
assert.NotEmpty(t, userInfo.Password, "Password should be returned for embedded IDP user")
|
||||
t.Logf("Created user: ID=%s, Email=%s, Password=%s", userInfo.ID, userInfo.Email, userInfo.Password)
|
||||
|
||||
// Verify user ID is in Dex encoded format
|
||||
rawUserID, connectorID, err := dex.DecodeDexUserID(userInfo.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rawUserID)
|
||||
assert.Equal(t, "local", connectorID)
|
||||
t.Logf("Decoded user ID: rawUserID=%s, connectorID=%s", rawUserID, connectorID)
|
||||
|
||||
// Verify user exists in database with correct data
|
||||
dbUser, err := testStore.GetUserByUserID(ctx, store.LockingStrengthNone, userInfo.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "newuser@test.com", dbUser.Email)
|
||||
assert.Equal(t, "New User", dbUser.Name)
|
||||
|
||||
// Store user ID for delete test
|
||||
createdUserID := userInfo.ID
|
||||
|
||||
t.Run("delete user works", func(t *testing.T) {
|
||||
err := am.DeleteUser(ctx, mockAccountID, mockUserID, createdUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify user is deleted from database
|
||||
_, err = testStore.GetUserByUserID(ctx, store.LockingStrengthNone, createdUserID)
|
||||
assert.Error(t, err, "User should be deleted from database")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("create service user does not return password", func(t *testing.T) {
|
||||
userInfo, err := am.CreateUser(ctx, mockAccountID, mockUserID, &types.UserInfo{
|
||||
Name: "Service User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
IsServiceUser: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, userInfo)
|
||||
|
||||
assert.True(t, userInfo.IsServiceUser)
|
||||
assert.Equal(t, "Service User", userInfo.Name)
|
||||
// Service users don't have passwords
|
||||
assert.Empty(t, userInfo.Password, "Service users should not have passwords")
|
||||
})
|
||||
|
||||
t.Run("duplicate email fails", func(t *testing.T) {
|
||||
// Create first user
|
||||
_, err := am.CreateUser(ctx, mockAccountID, mockUserID, &types.UserInfo{
|
||||
Email: "duplicate@test.com",
|
||||
Name: "First User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
IsServiceUser: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to create second user with same email
|
||||
_, err = am.CreateUser(ctx, mockAccountID, mockUserID, &types.UserInfo{
|
||||
Email: "duplicate@test.com",
|
||||
Name: "Second User",
|
||||
Role: "user",
|
||||
AutoGroups: []string{},
|
||||
IsServiceUser: false,
|
||||
})
|
||||
assert.Error(t, err, "Creating user with duplicate email should fail")
|
||||
t.Logf("Duplicate email error: %v", err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
"time"
|
||||
@@ -78,17 +79,19 @@ func parseTime(timeString string) time.Time {
|
||||
return parsedTime
|
||||
}
|
||||
|
||||
func (c ClaimsExtractor) audienceClaim(claimName string) string {
|
||||
url, err := url.JoinPath(c.authAudience, claimName)
|
||||
func (c *ClaimsExtractor) audienceClaim(claimName string) string {
|
||||
audienceURL, err := url.JoinPath(c.authAudience, claimName)
|
||||
if err != nil {
|
||||
return c.authAudience + claimName // as it was previously
|
||||
}
|
||||
|
||||
return url
|
||||
return audienceURL
|
||||
}
|
||||
|
||||
// ToUserAuth extracts user authentication information from a JWT token
|
||||
func (c *ClaimsExtractor) ToUserAuth(token *jwt.Token) (auth.UserAuth, error) {
|
||||
// ToUserAuth extracts user authentication information from a JWT token.
|
||||
// The token should contain standard claims like email, name, preferred_username.
|
||||
// When using Dex, make sure to set getUserInfo: true to have these claims populated.
|
||||
func (c *ClaimsExtractor) ToUserAuth(ctx context.Context, token *jwt.Token) (auth.UserAuth, error) {
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
userAuth := auth.UserAuth{}
|
||||
|
||||
@@ -120,6 +123,21 @@ func (c *ClaimsExtractor) ToUserAuth(token *jwt.Token) (auth.UserAuth, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract email from standard "email" claim
|
||||
if email, ok := claims["email"].(string); ok {
|
||||
userAuth.Email = email
|
||||
}
|
||||
|
||||
// Extract name from standard "name" claim
|
||||
if name, ok := claims["name"].(string); ok {
|
||||
userAuth.Name = name
|
||||
}
|
||||
|
||||
// Extract name from standard "preferred_username" claim
|
||||
if preferredName, ok := claims["preferred_username"].(string); ok {
|
||||
userAuth.PreferredName = preferredName
|
||||
}
|
||||
|
||||
return userAuth, nil
|
||||
}
|
||||
|
||||
|
||||
323
shared/auth/jwt/extractor_test.go
Normal file
323
shared/auth/jwt/extractor_test.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClaimsExtractor_ToUserAuth_ExtractsEmailAndName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
claims jwt.MapClaims
|
||||
userIDClaim string
|
||||
audience string
|
||||
expectedUserID string
|
||||
expectedEmail string
|
||||
expectedName string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "extracts email and name from standard claims",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"email": "test@example.com",
|
||||
"name": "Test User",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "user-123",
|
||||
expectedEmail: "test@example.com",
|
||||
expectedName: "Test User",
|
||||
},
|
||||
{
|
||||
name: "extracts Dex encoded user ID",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "CiQ3YWFkOGMwNS0zMjg3LTQ3M2YtYjQyYS0zNjU1MDRiZjI1ZTcSBWxvY2Fs",
|
||||
"email": "dex-user@example.com",
|
||||
"name": "Dex User",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "CiQ3YWFkOGMwNS0zMjg3LTQ3M2YtYjQyYS0zNjU1MDRiZjI1ZTcSBWxvY2Fs",
|
||||
expectedEmail: "dex-user@example.com",
|
||||
expectedName: "Dex User",
|
||||
},
|
||||
{
|
||||
name: "handles missing email claim",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-456",
|
||||
"name": "User Without Email",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "user-456",
|
||||
expectedEmail: "",
|
||||
expectedName: "User Without Email",
|
||||
},
|
||||
{
|
||||
name: "handles missing name claim",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-789",
|
||||
"email": "noname@example.com",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "user-789",
|
||||
expectedEmail: "noname@example.com",
|
||||
expectedName: "",
|
||||
},
|
||||
{
|
||||
name: "handles missing both email and name",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-minimal",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "user-minimal",
|
||||
expectedEmail: "",
|
||||
expectedName: "",
|
||||
},
|
||||
{
|
||||
name: "extracts preferred_username",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-pref",
|
||||
"email": "pref@example.com",
|
||||
"name": "Preferred User",
|
||||
"preferred_username": "prefuser",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectedUserID: "user-pref",
|
||||
expectedEmail: "pref@example.com",
|
||||
expectedName: "Preferred User",
|
||||
},
|
||||
{
|
||||
name: "fails when user ID claim is empty",
|
||||
claims: jwt.MapClaims{
|
||||
"email": "test@example.com",
|
||||
"name": "Test User",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "uses custom user ID claim",
|
||||
claims: jwt.MapClaims{
|
||||
"user_id": "custom-user-id",
|
||||
"email": "custom@example.com",
|
||||
"name": "Custom User",
|
||||
},
|
||||
userIDClaim: "user_id",
|
||||
expectedUserID: "custom-user-id",
|
||||
expectedEmail: "custom@example.com",
|
||||
expectedName: "Custom User",
|
||||
},
|
||||
{
|
||||
name: "extracts account ID with audience prefix",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-with-account",
|
||||
"email": "account@example.com",
|
||||
"name": "Account User",
|
||||
"https://api.netbird.io/wt_account_id": "account-123",
|
||||
"https://api.netbird.io/wt_account_domain": "example.com",
|
||||
},
|
||||
userIDClaim: "sub",
|
||||
audience: "https://api.netbird.io",
|
||||
expectedUserID: "user-with-account",
|
||||
expectedEmail: "account@example.com",
|
||||
expectedName: "Account User",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create extractor with options
|
||||
opts := []ClaimsExtractorOption{}
|
||||
if tt.userIDClaim != "" {
|
||||
opts = append(opts, WithUserIDClaim(tt.userIDClaim))
|
||||
}
|
||||
if tt.audience != "" {
|
||||
opts = append(opts, WithAudience(tt.audience))
|
||||
}
|
||||
extractor := NewClaimsExtractor(opts...)
|
||||
|
||||
// Create a mock token with the claims
|
||||
token := &jwt.Token{
|
||||
Claims: tt.claims,
|
||||
}
|
||||
|
||||
// Extract user auth
|
||||
userAuth, err := extractor.ToUserAuth(context.Background(), token)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedUserID, userAuth.UserId)
|
||||
assert.Equal(t, tt.expectedEmail, userAuth.Email)
|
||||
assert.Equal(t, tt.expectedName, userAuth.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_ToUserAuth_PreferredUsername(t *testing.T) {
|
||||
extractor := NewClaimsExtractor(WithUserIDClaim("sub"))
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"email": "test@example.com",
|
||||
"name": "Test User",
|
||||
"preferred_username": "testuser",
|
||||
}
|
||||
|
||||
token := &jwt.Token{Claims: claims}
|
||||
|
||||
userAuth, err := extractor.ToUserAuth(context.Background(), token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "user-123", userAuth.UserId)
|
||||
assert.Equal(t, "test@example.com", userAuth.Email)
|
||||
assert.Equal(t, "Test User", userAuth.Name)
|
||||
assert.Equal(t, "testuser", userAuth.PreferredName)
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_ToUserAuth_LastLogin(t *testing.T) {
|
||||
extractor := NewClaimsExtractor(
|
||||
WithUserIDClaim("sub"),
|
||||
WithAudience("https://api.netbird.io"),
|
||||
)
|
||||
|
||||
expectedTime := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"email": "test@example.com",
|
||||
"https://api.netbird.io/nb_last_login": expectedTime.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
token := &jwt.Token{Claims: claims}
|
||||
|
||||
userAuth, err := extractor.ToUserAuth(context.Background(), token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedTime, userAuth.LastLogin)
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_ToUserAuth_Invited(t *testing.T) {
|
||||
extractor := NewClaimsExtractor(
|
||||
WithUserIDClaim("sub"),
|
||||
WithAudience("https://api.netbird.io"),
|
||||
)
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"email": "invited@example.com",
|
||||
"https://api.netbird.io/nb_invited": true,
|
||||
}
|
||||
|
||||
token := &jwt.Token{Claims: claims}
|
||||
|
||||
userAuth, err := extractor.ToUserAuth(context.Background(), token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, userAuth.Invited)
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_ToGroups(t *testing.T) {
|
||||
extractor := NewClaimsExtractor(WithUserIDClaim("sub"))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
claims jwt.MapClaims
|
||||
groupClaimName string
|
||||
expectedGroups []string
|
||||
}{
|
||||
{
|
||||
name: "extracts groups from claim",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"groups": []interface{}{"admin", "users", "developers"},
|
||||
},
|
||||
groupClaimName: "groups",
|
||||
expectedGroups: []string{"admin", "users", "developers"},
|
||||
},
|
||||
{
|
||||
name: "returns empty slice when claim missing",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
},
|
||||
groupClaimName: "groups",
|
||||
expectedGroups: []string{},
|
||||
},
|
||||
{
|
||||
name: "handles custom claim name",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"user_roles": []interface{}{"role1", "role2"},
|
||||
},
|
||||
groupClaimName: "user_roles",
|
||||
expectedGroups: []string{"role1", "role2"},
|
||||
},
|
||||
{
|
||||
name: "filters non-string values",
|
||||
claims: jwt.MapClaims{
|
||||
"sub": "user-123",
|
||||
"groups": []interface{}{"admin", 123, "users", true},
|
||||
},
|
||||
groupClaimName: "groups",
|
||||
expectedGroups: []string{"admin", "users"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
token := &jwt.Token{Claims: tt.claims}
|
||||
groups := extractor.ToGroups(token, tt.groupClaimName)
|
||||
assert.Equal(t, tt.expectedGroups, groups)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_DefaultUserIDClaim(t *testing.T) {
|
||||
// When no user ID claim is specified, it should default to "sub"
|
||||
extractor := NewClaimsExtractor()
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "default-user-id",
|
||||
"email": "default@example.com",
|
||||
}
|
||||
|
||||
token := &jwt.Token{Claims: claims}
|
||||
|
||||
userAuth, err := extractor.ToUserAuth(context.Background(), token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "default-user-id", userAuth.UserId)
|
||||
}
|
||||
|
||||
func TestClaimsExtractor_DexUserIDFormat(t *testing.T) {
|
||||
// Test that the extractor correctly handles Dex's encoded user ID format
|
||||
// Dex encodes user IDs as base64(protobuf{user_id, connector_id})
|
||||
extractor := NewClaimsExtractor(WithUserIDClaim("sub"))
|
||||
|
||||
// This is an actual Dex-encoded user ID
|
||||
dexEncodedID := "CiQ3YWFkOGMwNS0zMjg3LTQ3M2YtYjQyYS0zNjU1MDRiZjI1ZTcSBWxvY2Fs"
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": dexEncodedID,
|
||||
"email": "dex@example.com",
|
||||
"name": "Dex User",
|
||||
}
|
||||
|
||||
token := &jwt.Token{Claims: claims}
|
||||
|
||||
userAuth, err := extractor.ToUserAuth(context.Background(), token)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The extractor should pass through the encoded ID as-is
|
||||
// Decoding is done elsewhere (e.g., in the Dex provider)
|
||||
assert.Equal(t, dexEncodedID, userAuth.UserId)
|
||||
assert.Equal(t, "dex@example.com", userAuth.Email)
|
||||
assert.Equal(t, "Dex User", userAuth.Name)
|
||||
}
|
||||
@@ -18,6 +18,15 @@ type UserAuth struct {
|
||||
|
||||
// The user id
|
||||
UserId string
|
||||
// The user's email address
|
||||
// (optional, may be empty if not in token, make sure to set getUserInfo: true in Dex to have this field)
|
||||
Email string
|
||||
// The user's name
|
||||
// (optional, may be empty if not in token, make sure to set getUserInfo: true in Dex to have this field)
|
||||
Name string
|
||||
// The user's preferred name
|
||||
// (optional, may be empty if not in token, make sure to set getUserInfo: true in Dex to have this field)
|
||||
PreferredName string
|
||||
// Last login time for this user
|
||||
LastLogin time.Time
|
||||
// The Groups the user belongs to on this account
|
||||
|
||||
@@ -32,6 +32,8 @@ tags:
|
||||
- name: Ingress Ports
|
||||
description: Interact with and view information about the ingress peers and ports.
|
||||
x-cloud-only: true
|
||||
- name: Identity Providers
|
||||
description: Interact with and view information about identity providers.
|
||||
components:
|
||||
schemas:
|
||||
Account:
|
||||
@@ -206,6 +208,10 @@ components:
|
||||
description: User's email address
|
||||
type: string
|
||||
example: demo@netbird.io
|
||||
password:
|
||||
description: User's password. Only present when user is created (create user endpoint is called) and only when IdP supports user creation with password.
|
||||
type: string
|
||||
example: super_secure_password
|
||||
name:
|
||||
description: User's name from idp provider
|
||||
type: string
|
||||
@@ -252,6 +258,10 @@ components:
|
||||
description: How user was issued by API or Integration
|
||||
type: string
|
||||
example: api
|
||||
idp_id:
|
||||
description: Identity provider ID (connector ID) that the user authenticated with. Only populated for users with Dex-encoded user IDs.
|
||||
type: string
|
||||
example: okta-abc123
|
||||
permissions:
|
||||
$ref: '#/components/schemas/UserPermissions'
|
||||
required:
|
||||
@@ -2197,6 +2207,75 @@ components:
|
||||
- page_size
|
||||
- total_records
|
||||
- total_pages
|
||||
IdentityProviderType:
|
||||
type: string
|
||||
description: Type of identity provider
|
||||
enum:
|
||||
- oidc
|
||||
- zitadel
|
||||
- entra
|
||||
- google
|
||||
- okta
|
||||
- pocketid
|
||||
- microsoft
|
||||
example: oidc
|
||||
IdentityProvider:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
description: Identity provider ID
|
||||
type: string
|
||||
example: ch8i4ug6lnn4g9hqv7l0
|
||||
type:
|
||||
$ref: '#/components/schemas/IdentityProviderType'
|
||||
name:
|
||||
description: Human-readable name for the identity provider
|
||||
type: string
|
||||
example: My OIDC Provider
|
||||
issuer:
|
||||
description: OIDC issuer URL
|
||||
type: string
|
||||
example: https://accounts.google.com
|
||||
client_id:
|
||||
description: OAuth2 client ID
|
||||
type: string
|
||||
example: 123456789.apps.googleusercontent.com
|
||||
redirect_url:
|
||||
description: OAuth2 redirect URL for configuring the identity provider
|
||||
type: string
|
||||
example: https://example.com/oauth2/callback
|
||||
required:
|
||||
- type
|
||||
- name
|
||||
- issuer
|
||||
- client_id
|
||||
IdentityProviderRequest:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
$ref: '#/components/schemas/IdentityProviderType'
|
||||
name:
|
||||
description: Human-readable name for the identity provider
|
||||
type: string
|
||||
example: My OIDC Provider
|
||||
issuer:
|
||||
description: OIDC issuer URL
|
||||
type: string
|
||||
example: https://accounts.google.com
|
||||
client_id:
|
||||
description: OAuth2 client ID
|
||||
type: string
|
||||
example: 123456789.apps.googleusercontent.com
|
||||
client_secret:
|
||||
description: OAuth2 client secret
|
||||
type: string
|
||||
example: secret123
|
||||
required:
|
||||
- type
|
||||
- name
|
||||
- issuer
|
||||
- client_id
|
||||
- client_secret
|
||||
responses:
|
||||
not_found:
|
||||
description: Resource not found
|
||||
@@ -4824,3 +4903,147 @@ paths:
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/identity-providers:
|
||||
get:
|
||||
summary: List all Identity Providers
|
||||
description: Returns a list of all identity providers configured for the account
|
||||
tags: [ Identity Providers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON array of identity providers
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/IdentityProvider'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
post:
|
||||
summary: Create an Identity Provider
|
||||
description: Creates a new identity provider configuration
|
||||
tags: [ Identity Providers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
requestBody:
|
||||
description: Identity provider configuration
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/IdentityProviderRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: An Identity Provider object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IdentityProvider'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
/api/identity-providers/{idpId}:
|
||||
get:
|
||||
summary: Retrieve an Identity Provider
|
||||
description: Get information about a specific identity provider
|
||||
tags: [ Identity Providers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: idpId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an identity provider
|
||||
responses:
|
||||
'200':
|
||||
description: An Identity Provider object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IdentityProvider'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
put:
|
||||
summary: Update an Identity Provider
|
||||
description: Update an existing identity provider configuration
|
||||
tags: [ Identity Providers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: idpId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an identity provider
|
||||
requestBody:
|
||||
description: Identity provider update
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/IdentityProviderRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: An Identity Provider object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IdentityProvider'
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
delete:
|
||||
summary: Delete an Identity Provider
|
||||
description: Delete an identity provider configuration
|
||||
tags: [ Identity Providers ]
|
||||
security:
|
||||
- BearerAuth: [ ]
|
||||
- TokenAuth: [ ]
|
||||
parameters:
|
||||
- in: path
|
||||
name: idpId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The unique identifier of an identity provider
|
||||
responses:
|
||||
'200':
|
||||
description: Delete status code
|
||||
content: { }
|
||||
'400':
|
||||
"$ref": "#/components/responses/bad_request"
|
||||
'401':
|
||||
"$ref": "#/components/responses/requires_authentication"
|
||||
'403':
|
||||
"$ref": "#/components/responses/forbidden"
|
||||
'500':
|
||||
"$ref": "#/components/responses/internal_error"
|
||||
|
||||
@@ -83,6 +83,17 @@ const (
|
||||
GroupMinimumIssuedJwt GroupMinimumIssued = "jwt"
|
||||
)
|
||||
|
||||
// Defines values for IdentityProviderType.
|
||||
const (
|
||||
IdentityProviderTypeEntra IdentityProviderType = "entra"
|
||||
IdentityProviderTypeGoogle IdentityProviderType = "google"
|
||||
IdentityProviderTypeMicrosoft IdentityProviderType = "microsoft"
|
||||
IdentityProviderTypeOidc IdentityProviderType = "oidc"
|
||||
IdentityProviderTypeOkta IdentityProviderType = "okta"
|
||||
IdentityProviderTypePocketid IdentityProviderType = "pocketid"
|
||||
IdentityProviderTypeZitadel IdentityProviderType = "zitadel"
|
||||
)
|
||||
|
||||
// Defines values for IngressPortAllocationPortMappingProtocol.
|
||||
const (
|
||||
IngressPortAllocationPortMappingProtocolTcp IngressPortAllocationPortMappingProtocol = "tcp"
|
||||
@@ -517,6 +528,48 @@ type GroupRequest struct {
|
||||
Resources *[]Resource `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
// IdentityProvider defines model for IdentityProvider.
|
||||
type IdentityProvider struct {
|
||||
// ClientId OAuth2 client ID
|
||||
ClientId string `json:"client_id"`
|
||||
|
||||
// Id Identity provider ID
|
||||
Id *string `json:"id,omitempty"`
|
||||
|
||||
// Issuer OIDC issuer URL
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// Name Human-readable name for the identity provider
|
||||
Name string `json:"name"`
|
||||
|
||||
// RedirectUrl OAuth2 redirect URL for configuring the identity provider
|
||||
RedirectUrl *string `json:"redirect_url,omitempty"`
|
||||
|
||||
// Type Type of identity provider
|
||||
Type IdentityProviderType `json:"type"`
|
||||
}
|
||||
|
||||
// IdentityProviderRequest defines model for IdentityProviderRequest.
|
||||
type IdentityProviderRequest struct {
|
||||
// ClientId OAuth2 client ID
|
||||
ClientId string `json:"client_id"`
|
||||
|
||||
// ClientSecret OAuth2 client secret
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
// Issuer OIDC issuer URL
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// Name Human-readable name for the identity provider
|
||||
Name string `json:"name"`
|
||||
|
||||
// Type Type of identity provider
|
||||
Type IdentityProviderType `json:"type"`
|
||||
}
|
||||
|
||||
// IdentityProviderType Type of identity provider
|
||||
type IdentityProviderType string
|
||||
|
||||
// IngressPeer defines model for IngressPeer.
|
||||
type IngressPeer struct {
|
||||
AvailablePorts AvailablePorts `json:"available_ports"`
|
||||
@@ -1797,6 +1850,9 @@ type User struct {
|
||||
// Id User ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// IdpId Identity provider ID (connector ID) that the user authenticated with. Only populated for users with Dex-encoded user IDs.
|
||||
IdpId *string `json:"idp_id,omitempty"`
|
||||
|
||||
// IsBlocked Is true if this user is blocked. Blocked users can't use the system
|
||||
IsBlocked bool `json:"is_blocked"`
|
||||
|
||||
@@ -1815,6 +1871,9 @@ type User struct {
|
||||
// Name User's name from idp provider
|
||||
Name string `json:"name"`
|
||||
|
||||
// Password User's password. Only present when user is created (create user endpoint is called) and only when IdP supports user creation with password.
|
||||
Password *string `json:"password,omitempty"`
|
||||
|
||||
// PendingApproval Is true if this user requires approval before being activated. Only applicable for users joining via domain matching when user_approval_required is enabled.
|
||||
PendingApproval bool `json:"pending_approval"`
|
||||
Permissions *UserPermissions `json:"permissions,omitempty"`
|
||||
@@ -1956,6 +2015,12 @@ type PostApiGroupsJSONRequestBody = GroupRequest
|
||||
// PutApiGroupsGroupIdJSONRequestBody defines body for PutApiGroupsGroupId for application/json ContentType.
|
||||
type PutApiGroupsGroupIdJSONRequestBody = GroupRequest
|
||||
|
||||
// PostApiIdentityProvidersJSONRequestBody defines body for PostApiIdentityProviders for application/json ContentType.
|
||||
type PostApiIdentityProvidersJSONRequestBody = IdentityProviderRequest
|
||||
|
||||
// PutApiIdentityProvidersIdpIdJSONRequestBody defines body for PutApiIdentityProvidersIdpId for application/json ContentType.
|
||||
type PutApiIdentityProvidersIdpIdJSONRequestBody = IdentityProviderRequest
|
||||
|
||||
// PostApiIngressPeersJSONRequestBody defines body for PostApiIngressPeers for application/json ContentType.
|
||||
type PostApiIngressPeersJSONRequestBody = IngressPeerCreateRequest
|
||||
|
||||
|
||||
96
util/crypt/crypt.go
Normal file
96
util/crypt/crypt.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// FieldEncrypt provides AES-GCM encryption for sensitive fields.
|
||||
type FieldEncrypt struct {
|
||||
block cipher.Block
|
||||
}
|
||||
|
||||
// NewFieldEncrypt creates a new FieldEncrypt with the given base64-encoded key.
|
||||
// The key must be 32 bytes when decoded (for AES-256).
|
||||
func NewFieldEncrypt(base64Key string) (*FieldEncrypt, error) {
|
||||
key, err := base64.StdEncoding.DecodeString(base64Key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode encryption key: %w", err)
|
||||
}
|
||||
|
||||
if len(key) != 32 {
|
||||
return nil, fmt.Errorf("encryption key must be 32 bytes, got %d", len(key))
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create cipher: %w", err)
|
||||
}
|
||||
|
||||
return &FieldEncrypt{block: block}, nil
|
||||
}
|
||||
|
||||
// Encrypt encrypts the given plaintext and returns base64-encoded ciphertext.
|
||||
// Returns empty string for empty input.
|
||||
func (f *FieldEncrypt) Encrypt(plaintext string) (string, error) {
|
||||
if plaintext == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(f.block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create GCM: %w", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", fmt.Errorf("generate nonce: %w", err)
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts the given base64-encoded ciphertext and returns the plaintext.
|
||||
// Returns empty string for empty input.
|
||||
func (f *FieldEncrypt) Decrypt(ciphertext string) (string, error) {
|
||||
if ciphertext == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode ciphertext: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(f.block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create GCM: %w", err)
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertextBytes := data[:nonceSize], data[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertextBytes, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decrypt: %w", err)
|
||||
}
|
||||
|
||||
return string(plaintext), nil
|
||||
}
|
||||
|
||||
// GenerateKey generates a new random 32-byte encryption key and returns it as base64.
|
||||
func GenerateKey() (string, error) {
|
||||
key := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return "", fmt.Errorf("generate key: %w", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(key), nil
|
||||
}
|
||||
Reference in New Issue
Block a user