diff --git a/generic-oidc-auth-provider/go.mod b/generic-oidc-auth-provider/go.mod new file mode 100644 index 00000000..8cc4e49e --- /dev/null +++ b/generic-oidc-auth-provider/go.mod @@ -0,0 +1,85 @@ +module github.com/obot-platform/tools/generic-oidc-auth-provider + +go 1.26.2 + +replace ( + github.com/oauth2-proxy/oauth2-proxy/v7 => github.com/obot-platform/oauth2-proxy/v7 v7.0.0-20260410175959-7ef5428d1af3 + github.com/obot-platform/tools/auth-providers-common => ../auth-providers-common +) + +require ( + github.com/oauth2-proxy/oauth2-proxy/v7 v7.8.1 + github.com/obot-platform/tools/auth-providers-common v0.0.0-20241008222508-3c6174b443e7 +) + +require ( + cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/a8m/envsubst v1.4.3 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bitly/go-simplejson v0.5.1 // indirect + github.com/bsm/redislock v0.9.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.19.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.9.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/justinas/alice v1.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/redis/go-redis/v9 v9.18.0 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/api v0.272.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gorm.io/driver/postgres v1.6.0 // indirect + gorm.io/gorm v1.31.1 // indirect + k8s.io/apimachinery v0.35.3 // indirect +) diff --git a/generic-oidc-auth-provider/go.sum b/generic-oidc-auth-provider/go.sum new file mode 100644 index 00000000..981e1d59 --- /dev/null +++ b/generic-oidc-auth-provider/go.sum @@ -0,0 +1,259 @@ +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +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.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw= +github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= +github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU= +github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68= +github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +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/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= +github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw= +github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk= +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/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= +github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +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.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= +github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= +github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= +github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk= +github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= +github.com/obot-platform/oauth2-proxy/v7 v7.0.0-20260410175959-7ef5428d1af3 h1:9ALNL/prah32FbhdTqDZvhqm/QTWq4FRFjOxH/T3o+Q= +github.com/obot-platform/oauth2-proxy/v7 v7.0.0-20260410175959-7ef5428d1af3/go.mod h1:Fzn9iO2soLotFAHmSgVA9wMcnnKw4dew+FbZucQsHZ4= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/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.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +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/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +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.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= +google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE= +google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= +google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js= +google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= diff --git a/generic-oidc-auth-provider/main.go b/generic-oidc-auth-provider/main.go new file mode 100644 index 00000000..1e9a7e49 --- /dev/null +++ b/generic-oidc-auth-provider/main.go @@ -0,0 +1,396 @@ +package main + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" + + oauth2proxy "github.com/oauth2-proxy/oauth2-proxy/v7" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation" + "github.com/obot-platform/tools/auth-providers-common/pkg/env" + "github.com/obot-platform/tools/auth-providers-common/pkg/state" + "github.com/obot-platform/tools/generic-oidc-auth-provider/pkg/profile" +) + +type Options struct { + ClientID string `env:"OBOT_OIDC_AUTH_PROVIDER_CLIENT_ID"` + ClientSecret string `env:"OBOT_OIDC_AUTH_PROVIDER_CLIENT_SECRET"` + // IssuerURL is the OIDC issuer (e.g. https://auth.example.com/realms/myrealm). + // oauth2-proxy performs discovery against {IssuerURL}/.well-known/openid-configuration. + IssuerURL string `env:"OBOT_OIDC_AUTH_PROVIDER_ISSUER_URL"` + // Scopes requested at login. "openid" is required; email/profile recommended. + Scopes string `env:"OBOT_OIDC_AUTH_PROVIDER_SCOPES" default:"openid email profile" optional:"true"` + // Claim names — defaults match the OIDC spec / Keycloak conventions. + EmailClaim string `env:"OBOT_OIDC_AUTH_PROVIDER_EMAIL_CLAIM" default:"email" optional:"true"` + GroupsClaim string `env:"OBOT_OIDC_AUTH_PROVIDER_GROUPS_CLAIM" default:"groups" optional:"true"` + // AllowUnverifiedEmail lets users without email_verified=true sign in. + // Many self-hosted IdPs (e.g. Keycloak) do not set email_verified by default. + AllowUnverifiedEmail string `env:"OBOT_OIDC_AUTH_PROVIDER_ALLOW_UNVERIFIED_EMAIL" default:"true" optional:"true"` + // GroupAdmin opts in to IdP admin-API group features: enumerating all groups + // for Obot's admin group picker, and resolving a specific user's real group + // memberships. Generic OIDC has no standard endpoint for either, so this is + // vendor-specific. Set to "keycloak" to enable (the only backend today); it + // uses the OIDC client's own service account (client_credentials) against the + // Keycloak Admin API — the client must have realm-management roles query-groups + // and view-users. Leave empty to rely solely on the token's groups claim (works + // with any OIDC IdP, same as the OSS Google/GitHub providers). See + // groupAdminEnabled for accepted values. + GroupAdmin string `env:"OBOT_OIDC_AUTH_PROVIDER_GROUP_ADMIN" default:"" optional:"true"` + + ObotServerURL string `env:"OBOT_SERVER_PUBLIC_URL,OBOT_SERVER_URL"` + PostgresConnectionDSN string `env:"OBOT_AUTH_PROVIDER_POSTGRES_CONNECTION_DSN" optional:"true"` + AuthCookieSecret string `usage:"Secret used to encrypt cookie" env:"OBOT_AUTH_PROVIDER_COOKIE_SECRET"` + AuthEmailDomains string `usage:"Email domains allowed for authentication" default:"*" env:"OBOT_AUTH_PROVIDER_EMAIL_DOMAINS"` + AuthTokenRefreshDuration string `usage:"Duration to refresh auth token after" optional:"true" default:"1h" env:"OBOT_AUTH_PROVIDER_TOKEN_REFRESH_DURATION"` + LoggingEnabled string `usage:"Enable oauth2-proxy logging" optional:"true" env:"OBOT_AUTH_PROVIDER_ENABLE_LOGGING"` +} + +// groupAdminEnabled reports whether the IdP admin-API group features are opted +// in. The canonical value is the backend name "keycloak"; common truthy values +// are also accepted so a stray "true"/"yes" still works. Anything unrecognized +// (blank, a typo, an unsupported backend) is treated as off — that only disables +// group enumeration / membership lookup, it never blocks login. +func groupAdminEnabled(v string) bool { + switch strings.ToLower(strings.TrimSpace(v)) { + case "keycloak", "true", "1", "yes", "on", "enabled": + return true + default: + return false + } +} + +type idpGroup struct { + ID string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + SubGroupCount int `json:"subGroupCount"` + SubGroups []idpGroup `json:"subGroups"` +} + +var keycloakHTTP = &http.Client{Timeout: 15 * time.Second} + +// keycloakAdmin parses a Keycloak issuer into base+realm and obtains a service- +// account access token via client_credentials. Returns an error for non-Keycloak +// issuers or when no client secret is configured, so callers can fall back. +func keycloakAdmin(ctx context.Context, issuer, clientID, clientSecret string) (base, realm, token string, err error) { + issuer = strings.TrimRight(issuer, "/") + idx := strings.Index(issuer, "/realms/") + if idx < 0 || clientSecret == "" { + return "", "", "", fmt.Errorf("not a keycloak issuer or no client secret") + } + base = issuer[:idx] + realm = issuer[idx+len("/realms/"):] + if i := strings.IndexByte(realm, '/'); i >= 0 { + realm = realm[:i] + } + + form := url.Values{"grant_type": {"client_credentials"}, "client_id": {clientID}, "client_secret": {clientSecret}} + req, err := http.NewRequestWithContext(ctx, http.MethodPost, issuer+"/protocol/openid-connect/token", strings.NewReader(form.Encode())) + if err != nil { + return "", "", "", err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := keycloakHTTP.Do(req) + if err != nil { + return "", "", "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", "", "", fmt.Errorf("client_credentials token request returned %d", resp.StatusCode) + } + var tok struct { + AccessToken string `json:"access_token"` + } + if err = json.NewDecoder(resp.Body).Decode(&tok); err != nil || tok.AccessToken == "" { + return "", "", "", fmt.Errorf("no service-account token") + } + return base, realm, tok.AccessToken, nil +} + +// groupDisplayName renders a Keycloak group path as "parent / child" so nested +// groups read as subgroups in Obot's flat picker (e.g. "offices / DEVBORAS"). +func groupDisplayName(path, leaf string) string { + if path == "" { + return leaf + } + return strings.ReplaceAll(strings.TrimPrefix(path, "/"), "/", " / ") +} + +// keycloakPageSize is the number of groups requested per Keycloak admin API page. +const keycloakPageSize = 100 + +// fetchKeycloakGroups GETs a Keycloak admin groups endpoint and decodes the list, +// following Keycloak's first/max pagination until a short (final) page is returned +// so realms/children/memberships larger than one page are fully enumerated. The +// endpoint may already carry query params (e.g. briefRepresentation, search); the +// paging params are appended. +func fetchKeycloakGroups(ctx context.Context, token, endpoint string) ([]idpGroup, error) { + sep := "&" + if !strings.Contains(endpoint, "?") { + sep = "?" + } + var all []idpGroup + for first := 0; ; first += keycloakPageSize { + u := fmt.Sprintf("%s%sfirst=%d&max=%d", endpoint, sep, first, keycloakPageSize) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + resp, err := keycloakHTTP.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("keycloak admin query %s returned %d", endpoint, resp.StatusCode) + } + var page []idpGroup + err = json.NewDecoder(resp.Body).Decode(&page) + resp.Body.Close() + if err != nil { + return nil, err + } + all = append(all, page...) + if len(page) < keycloakPageSize { + break + } + } + return all, nil +} + +// listIdPGroupsViaKeycloakAdmin enumerates realm groups through Keycloak's Admin +// API using the OIDC client's own service-account (client_credentials). This is +// how Obot's enterprise connectors populate the group picker; generic OIDC has +// no group-list endpoint, so we offer it for Keycloak when the client has a +// service account with `query-groups`/`view-users`. Returns an error (so the +// caller falls back to per-user groups) for non-Keycloak issuers or missing +// permissions. Group names are returned as IDs to match the token groups claim. +func listIdPGroupsViaKeycloakAdmin(ctx context.Context, issuer, clientID, clientSecret, search string) (state.GroupInfoList, error) { + base, realm, token, err := keycloakAdmin(ctx, issuer, clientID, clientSecret) + if err != nil { + return nil, err + } + + groupsURL := fmt.Sprintf("%s/admin/realms/%s/groups?briefRepresentation=true", base, realm) + if search != "" { + groupsURL += "&search=" + url.QueryEscape(search) + } + roots, err := fetchKeycloakGroups(ctx, token, groupsURL) + if err != nil { + return nil, err + } + + // Keycloak (v23+) does not inline subGroups in the groups listing — it returns + // only subGroupCount — so nested groups are fetched via the /children endpoint. + out := state.GroupInfoList{} + var walk func([]idpGroup) + walk = func(gs []idpGroup) { + for _, g := range gs { + // ID = leaf name to match the token's groups claim (full.path=false); + // Name = hierarchical path so subgroups read as such in the picker. + out = append(out, state.GroupInfo{ID: g.Name, Name: groupDisplayName(g.Path, g.Name)}) + children := g.SubGroups + if len(children) == 0 && g.SubGroupCount > 0 && g.ID != "" { + if kids, err := fetchKeycloakGroups(ctx, token, + fmt.Sprintf("%s/admin/realms/%s/groups/%s/children?briefRepresentation=true", base, realm, url.PathEscape(g.ID))); err == nil { + children = kids + } + } + walk(children) + } + } + walk(roots) + return out, nil +} + +// listKeycloakUserGroups returns the groups a SPECIFIC user (by Keycloak user +// UUID) belongs to, via the admin API. Obot calls /obot-list-user-auth-groups to +// sync per-user memberships, so this MUST return only that user's groups — never +// the whole realm (that would make every user a member of every group). +func listKeycloakUserGroups(ctx context.Context, issuer, clientID, clientSecret, userID string) (state.GroupInfoList, error) { + if userID == "" { + return nil, fmt.Errorf("no user id") + } + base, realm, token, err := keycloakAdmin(ctx, issuer, clientID, clientSecret) + if err != nil { + return nil, err + } + groups, err := fetchKeycloakGroups(ctx, token, + fmt.Sprintf("%s/admin/realms/%s/users/%s/groups?briefRepresentation=true", base, realm, url.PathEscape(userID))) + if err != nil { + return nil, err + } + out := make(state.GroupInfoList, 0, len(groups)) + for _, g := range groups { + out = append(out, state.GroupInfo{ID: g.Name, Name: groupDisplayName(g.Path, g.Name)}) + } + return out, nil +} + +func main() { + var opts Options + if err := env.LoadEnvForStruct(&opts); err != nil { + fmt.Printf("ERROR: generic-oidc-auth-provider: failed to load options: %v\n", err) + os.Exit(1) + } + + if opts.IssuerURL == "" { + fmt.Printf("ERROR: generic-oidc-auth-provider: OBOT_OIDC_AUTH_PROVIDER_ISSUER_URL is required\n") + os.Exit(1) + } + + refreshDuration, err := time.ParseDuration(opts.AuthTokenRefreshDuration) + if err != nil { + fmt.Printf("ERROR: generic-oidc-auth-provider: failed to parse token refresh duration: %v\n", err) + os.Exit(1) + } + + if refreshDuration < 0 { + fmt.Printf("ERROR: generic-oidc-auth-provider: token refresh duration must be greater than 0\n") + os.Exit(1) + } + + cookieSecret, err := base64.StdEncoding.DecodeString(opts.AuthCookieSecret) + if err != nil { + fmt.Printf("ERROR: generic-oidc-auth-provider: failed to decode cookie secret: %v\n", err) + os.Exit(1) + } + + legacyOpts := options.NewLegacyOptions() + legacyOpts.LegacyProvider.ProviderType = "oidc" + legacyOpts.LegacyProvider.ProviderName = "oidc" + legacyOpts.LegacyProvider.ClientID = opts.ClientID + legacyOpts.LegacyProvider.ClientSecret = opts.ClientSecret + legacyOpts.LegacyProvider.OIDCIssuerURL = opts.IssuerURL + legacyOpts.LegacyProvider.Scope = opts.Scopes + legacyOpts.LegacyProvider.OIDCEmailClaim = opts.EmailClaim + legacyOpts.LegacyProvider.OIDCGroupsClaim = opts.GroupsClaim + legacyOpts.LegacyProvider.InsecureOIDCAllowUnverifiedEmail = strings.EqualFold(opts.AllowUnverifiedEmail, "true") + + oauthProxyOpts, err := legacyOpts.ToOptions() + if err != nil { + fmt.Printf("ERROR: generic-oidc-auth-provider: failed to convert legacy options to new options: %v\n", err) + os.Exit(1) + } + + oauthProxyOpts.Server.BindAddress = "" + oauthProxyOpts.MetricsServer.BindAddress = "" + if opts.PostgresConnectionDSN != "" { + oauthProxyOpts.Session.Type = options.PostgresSessionStoreType + oauthProxyOpts.Session.Postgres.ConnectionDSN = opts.PostgresConnectionDSN + oauthProxyOpts.Session.Postgres.TableNamePrefix = "oidc_" + } + oauthProxyOpts.Cookie.Refresh = refreshDuration + oauthProxyOpts.Cookie.Name = "obot_access_token" + oauthProxyOpts.Cookie.Secret = string(bytes.TrimSpace(cookieSecret)) + oauthProxyOpts.Cookie.Secure = strings.HasPrefix(opts.ObotServerURL, "https://") + oauthProxyOpts.Cookie.CSRFExpire = 30 * time.Minute + oauthProxyOpts.Templates.Path = os.Getenv("GPTSCRIPT_TOOL_DIR") + "/../auth-providers-common/templates" + oauthProxyOpts.RawRedirectURL = opts.ObotServerURL + "/" + if opts.AuthEmailDomains != "" { + emailDomains := strings.Split(opts.AuthEmailDomains, ",") + for i := range emailDomains { + emailDomains[i] = strings.TrimSpace(emailDomains[i]) + } + oauthProxyOpts.EmailDomains = emailDomains + } + + loggingEnabled := strings.EqualFold(opts.LoggingEnabled, "true") + oauthProxyOpts.Logging.RequestEnabled = loggingEnabled + oauthProxyOpts.Logging.AuthEnabled = loggingEnabled + oauthProxyOpts.Logging.StandardEnabled = loggingEnabled + + if err = validation.Validate(oauthProxyOpts); err != nil { + fmt.Printf("ERROR: generic-oidc-auth-provider: failed to validate options: %v\n", err) + os.Exit(1) + } + + oauthProxy, err := oauth2proxy.NewOAuthProxy(oauthProxyOpts, oauth2proxy.NewValidator(oauthProxyOpts.EmailDomains, oauthProxyOpts.AuthenticatedEmailsFile)) + if err != nil { + fmt.Printf("ERROR: generic-oidc-auth-provider: failed to create oauth2 proxy: %v\n", err) + os.Exit(1) + } + + port := os.Getenv("PORT") + if port == "" { + port = "9999" + } + + issuerURL := strings.TrimRight(opts.IssuerURL, "/") + // Opt-in, vendor-specific group admin (enumeration + per-user lookup). When + // off, the provider is pure generic OIDC: groups come only from the token claim. + keycloakGroupAdmin := groupAdminEnabled(opts.GroupAdmin) + + mux := http.NewServeMux() + mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(fmt.Sprintf("http://127.0.0.1:%s", port))) + }) + mux.HandleFunc("/obot-get-state", state.ObotGetState(oauthProxy)) + mux.HandleFunc("/obot-get-user-info", func(w http.ResponseWriter, r *http.Request) { + userInfo, err := profile.FetchOIDCProfile(r.Context(), issuerURL, r.Header.Get("Authorization")) + if err != nil { + http.Error(w, fmt.Sprintf("failed to fetch user info: %v", err), http.StatusBadRequest) + return + } + + json.NewEncoder(w).Encode(userInfo) + }) + // /obot-list-auth-groups: enumerate ALL realm groups for the admin group + // picker (group-scoped registries / role assignments). Only meaningful with an + // IdP admin API (opt-in); generic OIDC has no such endpoint, so we return an + // empty list and Obot falls back to the groups it has discovered in its DB. + mux.HandleFunc("/obot-list-auth-groups", func(w http.ResponseWriter, r *http.Request) { + groups := state.GroupInfoList{} + if keycloakGroupAdmin { + if g, err := listIdPGroupsViaKeycloakAdmin(r.Context(), issuerURL, opts.ClientID, opts.ClientSecret, r.URL.Query().Get("name")); err == nil { + groups = g + } + } + json.NewEncoder(w).Encode(groups) + }) + // /obot-list-user-auth-groups: the groups a SPECIFIC user belongs to. Obot + // POSTs the provider user ID as the request body and uses the result to sync + // that user's group memberships, so we must return ONLY that user's groups. + mux.HandleFunc("/obot-list-user-auth-groups", func(w http.ResponseWriter, r *http.Request) { + if !keycloakGroupAdmin { + // Generic OIDC has no per-user group endpoint; a user's groups reach Obot + // via the token's groups claim (surfaced by /obot-get-state). Signal "not + // implemented" like the OSS Google/GitHub providers do. + w.WriteHeader(http.StatusNotFound) + return + } + var userID string + if r.Body != nil { + body, _ := io.ReadAll(r.Body) + userID = strings.TrimSpace(string(body)) + } + groups, err := listKeycloakUserGroups(r.Context(), issuerURL, opts.ClientID, opts.ClientSecret, userID) + if err != nil { + // Return an error (not an empty list) on a lookup failure: Obot treats an + // empty list as "user is in no groups" and deletes their memberships, so a + // transient Keycloak Admin API failure would wipe group memberships. An + // error makes Obot skip the sync and keep the existing memberships. + http.Error(w, fmt.Sprintf("failed to list user groups: %v", err), http.StatusBadGateway) + return + } + json.NewEncoder(w).Encode(groups) + }) + mux.HandleFunc("/", oauthProxy.ServeHTTP) + + fmt.Printf("listening on 127.0.0.1:%s\n", port) + if err := http.ListenAndServe("127.0.0.1:"+port, mux); !errors.Is(err, http.ErrServerClosed) { + fmt.Printf("ERROR: generic-oidc-auth-provider: failed to listen and serve: %v\n", err) + os.Exit(1) + } +} diff --git a/generic-oidc-auth-provider/pkg/profile/profile.go b/generic-oidc-auth-provider/pkg/profile/profile.go new file mode 100644 index 00000000..7071f080 --- /dev/null +++ b/generic-oidc-auth-provider/pkg/profile/profile.go @@ -0,0 +1,146 @@ +package profile + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "sync" + "time" +) + +// OIDCProfile is the normalized user info returned to Obot. Field names mirror +// the Google auth provider (id/email/verified_email/name/picture) so Obot's +// server can consume them uniformly, with OIDC-standard extras added. +type OIDCProfile struct { + ID string `json:"id"` + Sub string `json:"sub"` + Email string `json:"email"` + VerifiedEmail bool `json:"verified_email"` + Name string `json:"name"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + PreferredUsername string `json:"preferred_username"` + Picture string `json:"picture"` + Groups []string `json:"groups,omitempty"` +} + +type discoveryDoc struct { + UserinfoEndpoint string `json:"userinfo_endpoint"` +} + +var ( + discoveryCache = map[string]discoveryDoc{} + discoveryMu sync.Mutex +) + +func discover(ctx context.Context, issuer string) (discoveryDoc, error) { + discoveryMu.Lock() + if d, ok := discoveryCache[issuer]; ok { + discoveryMu.Unlock() + return d, nil + } + discoveryMu.Unlock() + + url := issuer + "/.well-known/openid-configuration" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return discoveryDoc{}, err + } + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(req) + if err != nil { + return discoveryDoc{}, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return discoveryDoc{}, fmt.Errorf("discovery at %s returned %d: %s", url, resp.StatusCode, body) + } + var d discoveryDoc + if err = json.NewDecoder(resp.Body).Decode(&d); err != nil { + return discoveryDoc{}, err + } + if d.UserinfoEndpoint == "" { + return discoveryDoc{}, fmt.Errorf("discovery doc at %s has no userinfo_endpoint", url) + } + + discoveryMu.Lock() + discoveryCache[issuer] = d + discoveryMu.Unlock() + return d, nil +} + +// rawUserInfo captures the standard OIDC userinfo claims plus the common +// non-standard "groups" claim. email_verified can be a bool or a string +// depending on the IdP, so it is decoded leniently. +type rawUserInfo struct { + Sub string `json:"sub"` + Email string `json:"email"` + EmailVerified json.RawMessage `json:"email_verified"` + Name string `json:"name"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + PreferredUsername string `json:"preferred_username"` + Picture string `json:"picture"` + Groups []string `json:"groups"` +} + +// FetchOIDCProfile discovers the issuer's userinfo endpoint and fetches the +// caller's claims using the provided Authorization header value. +func FetchOIDCProfile(ctx context.Context, issuer, authorization string) (*OIDCProfile, error) { + doc, err := discover(ctx, issuer) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, doc.UserinfoEndpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", authorization) + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("userinfo at %s returned %d: %s", doc.UserinfoEndpoint, resp.StatusCode, body) + } + + var raw rawUserInfo + if err = json.NewDecoder(resp.Body).Decode(&raw); err != nil { + return nil, err + } + + return &OIDCProfile{ + ID: raw.Sub, + Sub: raw.Sub, + Email: raw.Email, + VerifiedEmail: parseVerified(raw.EmailVerified), + Name: raw.Name, + GivenName: raw.GivenName, + FamilyName: raw.FamilyName, + PreferredUsername: raw.PreferredUsername, + Picture: raw.Picture, + Groups: raw.Groups, + }, nil +} + +func parseVerified(raw json.RawMessage) bool { + if len(raw) == 0 { + return false + } + var b bool + if json.Unmarshal(raw, &b) == nil { + return b + } + var s string + if json.Unmarshal(raw, &s) == nil { + return s == "true" + } + return false +} diff --git a/generic-oidc-auth-provider/pkg/profile/profile_test.go b/generic-oidc-auth-provider/pkg/profile/profile_test.go new file mode 100644 index 00000000..9c589f6d --- /dev/null +++ b/generic-oidc-auth-provider/pkg/profile/profile_test.go @@ -0,0 +1,64 @@ +package profile + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +func TestFetchOIDCProfile(t *testing.T) { + // Mock IdP: serves discovery + userinfo. The userinfo response includes a + // string email_verified to exercise the lenient parser, plus groups. + mux := http.NewServeMux() + var base string + mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]any{ + "issuer": base, + "userinfo_endpoint": base + "/userinfo", + }) + }) + mux.HandleFunc("/userinfo", func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "Bearer tok" { + http.Error(w, "unexpected auth: "+got, http.StatusUnauthorized) + return + } + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{"sub":"abc-123","email":"jane@example.com","email_verified":"true",`+ + `"name":"Jane Doe","preferred_username":"jane","groups":["devs","admins"]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + base = server.URL + + p, err := FetchOIDCProfile(context.Background(), server.URL, "Bearer tok") + if err != nil { + t.Fatalf("FetchOIDCProfile returned error: %v", err) + } + if p.ID != "abc-123" || p.Sub != "abc-123" { + t.Errorf("unexpected id/sub: %q/%q", p.ID, p.Sub) + } + if p.Email != "jane@example.com" { + t.Errorf("unexpected email: %q", p.Email) + } + if !p.VerifiedEmail { + t.Errorf("expected verified_email true from string \"true\"") + } + if p.PreferredUsername != "jane" { + t.Errorf("unexpected preferred_username: %q", p.PreferredUsername) + } + if len(p.Groups) != 2 || p.Groups[0] != "devs" { + t.Errorf("unexpected groups: %v", p.Groups) + } +} + +func TestParseVerified(t *testing.T) { + cases := map[string]bool{`true`: true, `false`: false, `"true"`: true, `"false"`: false, ``: false} + for in, want := range cases { + if got := parseVerified(json.RawMessage(in)); got != want { + t.Errorf("parseVerified(%q) = %v, want %v", in, got, want) + } + } +} diff --git a/generic-oidc-auth-provider/tool.gpt b/generic-oidc-auth-provider/tool.gpt new file mode 100644 index 00000000..5519f3ec --- /dev/null +++ b/generic-oidc-auth-provider/tool.gpt @@ -0,0 +1,100 @@ +Name: OpenID Connect +Description: Generic OpenID Connect auth provider (works with Keycloak, Authentik, Dex, Auth0, and any OIDC-compliant IdP) +Metadata: envVars: OBOT_OIDC_AUTH_PROVIDER_CLIENT_ID,OBOT_OIDC_AUTH_PROVIDER_CLIENT_SECRET,OBOT_OIDC_AUTH_PROVIDER_ISSUER_URL,OBOT_AUTH_PROVIDER_COOKIE_SECRET,OBOT_AUTH_PROVIDER_EMAIL_DOMAINS +Metadata: noUserAuth: generic-oidc-auth-provider +Credential: ../placeholder-credential as generic-oidc-auth-provider + +#!sys.daemon ${GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool + +--- +!metadata:OpenID Connect:providerMeta +{ + "icon": "https://cdn.jsdelivr.net/npm/simple-icons@v13/icons/openid.svg", + "iconDark": "https://cdn.jsdelivr.net/npm/simple-icons@v13/icons/openid.svg", + "link": "https://openid.net/connect/", + "postgresTablePrefix": "oidc_", + "envVars": [ + { + "name": "OBOT_OIDC_AUTH_PROVIDER_ISSUER_URL", + "friendlyName": "Issuer URL", + "description": "The OIDC issuer URL. Endpoints are discovered from {issuer}/.well-known/openid-configuration. For Keycloak this is https:///realms/.", + "sensitive": false + }, + { + "name": "OBOT_OIDC_AUTH_PROVIDER_CLIENT_ID", + "friendlyName": "Client ID", + "description": "Client ID of the confidential OIDC client registered with your identity provider.", + "sensitive": false + }, + { + "name": "OBOT_OIDC_AUTH_PROVIDER_CLIENT_SECRET", + "friendlyName": "Client Secret", + "description": "Client secret for the OIDC client.", + "sensitive": true + }, + { + "name": "OBOT_AUTH_PROVIDER_COOKIE_SECRET", + "friendlyName": "Cookie Secret", + "description": "Secret used to encrypt cookies. Must be a random string of length 16, 24, or 32.", + "sensitive": true, + "hidden": true + }, + { + "name": "OBOT_AUTH_PROVIDER_EMAIL_DOMAINS", + "friendlyName": "Allowed E-Mail Domains", + "description": "A list of email domains that are allowed to authenticate with this provider. * is a special value that allows all domains.", + "sensitive": false + } + ], + "optionalEnvVars": [ + { + "name": "OBOT_OIDC_AUTH_PROVIDER_SCOPES", + "friendlyName": "Scopes", + "description": "Space-separated OAuth scopes to request. Default: openid email profile", + "sensitive": false + }, + { + "name": "OBOT_OIDC_AUTH_PROVIDER_EMAIL_CLAIM", + "friendlyName": "Email Claim", + "description": "The OIDC claim containing the user's email. Default: email", + "sensitive": false + }, + { + "name": "OBOT_OIDC_AUTH_PROVIDER_GROUPS_CLAIM", + "friendlyName": "Groups Claim", + "description": "The OIDC claim containing the user's groups. Default: groups", + "sensitive": false + }, + { + "name": "OBOT_OIDC_AUTH_PROVIDER_GROUP_ADMIN", + "friendlyName": "Group Admin (IdP) — enter: keycloak", + "description": "Enter 'keycloak' to enable IdP admin-API group features: enumerate all groups for the admin group picker, and resolve each user's real group memberships. 'keycloak' is the only supported backend today and requires the OIDC client to have a service account with realm-management roles query-groups and view-users. Leave this blank for any other IdP — groups then come only from the token claim (default). An unrecognized value is treated as blank and never blocks login.", + "sensitive": false + }, + { + "name": "OBOT_OIDC_AUTH_PROVIDER_ALLOW_UNVERIFIED_EMAIL", + "friendlyName": "Allow Unverified Email", + "description": "Allow users whose email_verified claim is not true to sign in. Many self-hosted IdPs do not set email_verified. Default: true", + "sensitive": false + }, + { + "name": "OBOT_AUTH_PROVIDER_POSTGRES_CONNECTION_DSN", + "friendlyName": "PostgreSQL connection string (DSN)", + "description": "The connection string for a PostgreSQL database to use for session storage. If unset, cookies will be used for session storage instead.", + "sensitive": true, + "hidden": true + }, + { + "name": "OBOT_AUTH_PROVIDER_TOKEN_REFRESH_DURATION", + "friendlyName": "Token Refresh Duration", + "description": "Time to wait before attempting to refresh auth tokens. Should be in a format like 1h1m1s. Default: 1h", + "sensitive": false + }, + { + "name": "OBOT_AUTH_PROVIDER_ENABLE_LOGGING", + "friendlyName": "Enable Logging", + "description": "Set to true to enable request, auth, and standard logging for the auth provider. Default: false", + "sensitive": false + } + ] +} diff --git a/index.yaml b/index.yaml index 9b439a39..9e0f1e75 100644 --- a/index.yaml +++ b/index.yaml @@ -19,6 +19,8 @@ modelProviders: reference: ./generic-openai-model-provider authProviders: + generic-oidc-auth-provider: + reference: ./generic-oidc-auth-provider github-auth-provider: reference: ./github-auth-provider google-auth-provider: