diff --git a/apiclient/types/authprovider.go b/apiclient/types/authprovider.go new file mode 100644 index 000000000..dbcb85e53 --- /dev/null +++ b/apiclient/types/authprovider.go @@ -0,0 +1,23 @@ +package types + +type AuthProvider struct { + Metadata + AuthProviderManifest + AuthProviderStatus +} + +type AuthProviderManifest struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + ToolReference string `json:"toolReference"` +} + +type AuthProviderStatus struct { + Icon string `json:"icon,omitempty"` + Configured bool `json:"configured"` + RequiredConfigurationParameters []string `json:"requiredConfigurationParameters,omitempty"` + MissingConfigurationParameters []string `json:"missingConfigurationParameters,omitempty"` + OptionalConfigurationParameters []string `json:"optionalConfigurationParameters,omitempty"` +} + +type AuthProviderList List[AuthProvider] diff --git a/apiclient/types/toolreference.go b/apiclient/types/toolreference.go index 41849c608..a6ab7ff2e 100644 --- a/apiclient/types/toolreference.go +++ b/apiclient/types/toolreference.go @@ -9,6 +9,7 @@ const ( ToolReferenceTypeKnowledgeDocumentLoader ToolReferenceType = "knowledgeDocumentLoader" ToolReferenceTypeSystem ToolReferenceType = "system" ToolReferenceTypeModelProvider ToolReferenceType = "modelProvider" + ToolReferenceTypeAuthProvider ToolReferenceType = "authProvider" ) type ToolReferenceManifest struct { diff --git a/apiclient/types/zz_generated.deepcopy.go b/apiclient/types/zz_generated.deepcopy.go index ca1ea4f17..b80793228 100644 --- a/apiclient/types/zz_generated.deepcopy.go +++ b/apiclient/types/zz_generated.deepcopy.go @@ -231,6 +231,91 @@ func (in *AssistantToolList) DeepCopy() *AssistantToolList { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthProvider) DeepCopyInto(out *AuthProvider) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + out.AuthProviderManifest = in.AuthProviderManifest + in.AuthProviderStatus.DeepCopyInto(&out.AuthProviderStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthProvider. +func (in *AuthProvider) DeepCopy() *AuthProvider { + if in == nil { + return nil + } + out := new(AuthProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthProviderList) DeepCopyInto(out *AuthProviderList) { + *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AuthProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthProviderList. +func (in *AuthProviderList) DeepCopy() *AuthProviderList { + if in == nil { + return nil + } + out := new(AuthProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthProviderManifest) DeepCopyInto(out *AuthProviderManifest) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthProviderManifest. +func (in *AuthProviderManifest) DeepCopy() *AuthProviderManifest { + if in == nil { + return nil + } + out := new(AuthProviderManifest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthProviderStatus) DeepCopyInto(out *AuthProviderStatus) { + *out = *in + if in.RequiredConfigurationParameters != nil { + in, out := &in.RequiredConfigurationParameters, &out.RequiredConfigurationParameters + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.MissingConfigurationParameters != nil { + in, out := &in.MissingConfigurationParameters, &out.MissingConfigurationParameters + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.OptionalConfigurationParameters != nil { + in, out := &in.OptionalConfigurationParameters, &out.OptionalConfigurationParameters + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthProviderStatus. +func (in *AuthProviderStatus) DeepCopy() *AuthProviderStatus { + if in == nil { + return nil + } + out := new(AuthProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Credential) DeepCopyInto(out *Credential) { *out = *in diff --git a/go.mod b/go.mod index 286259fc0..4df3121b2 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/gptscript-ai/gptscript v0.9.6-0.20241216210744-eb036809105c github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de github.com/mhale/smtpd v0.8.3 - github.com/oauth2-proxy/oauth2-proxy/v7 v7.0.0-00010101000000-000000000000 github.com/obot-platform/kinm v0.0.0-20241217210842-81947252da4e github.com/obot-platform/nah v0.0.0-20241217120500-e9169e4a999f github.com/obot-platform/namegenerator v0.0.0-20241217121223-fc58bdb7dca2 @@ -53,9 +52,6 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - cloud.google.com/go/auth v0.9.4 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect - cloud.google.com/go/compute/metadata v0.5.2 // indirect dario.cat/mergo v1.0.1 // indirect github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -64,22 +60,18 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect - github.com/a8m/envsubst v1.4.2 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bitly/go-simplejson v0.5.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.5.2 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/bombsimon/logrusr/v4 v4.1.0 // indirect - github.com/bsm/redislock v0.9.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect @@ -89,12 +81,10 @@ require ( github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.5.0 // indirect github.com/containerd/console v1.0.4 // indirect - github.com/coreos/go-oidc/v3 v3.11.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cyphar/filepath-securejoin v0.3.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/docker/cli v27.3.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect @@ -108,15 +98,12 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/getkin/kin-openapi v0.128.0 // indirect - github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/sqlite v1.11.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.3 // indirect - github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -131,13 +118,10 @@ require ( github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/s2a-go v0.1.8 // indirect + github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect - github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/gptscript-ai/broadcaster v0.0.0-20240625175512-c43682019b86 // indirect github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6 // indirect @@ -147,7 +131,6 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.3.1 // indirect @@ -161,7 +144,6 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/justinas/alice v1.2.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.11 // indirect @@ -169,17 +151,14 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mholt/archiver/v4 v4.0.0-alpha.8 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.4.0 // indirect github.com/moby/term v0.5.0 // indirect @@ -192,9 +171,9 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/nwaples/rardecode/v2 v2.0.0-beta.4 // indirect - github.com/ohler55/ojg v1.24.1 // indirect + github.com/nxadm/tail v1.4.11 // indirect github.com/olekukonko/tablewriter v0.0.6-0.20230925090304-df64c4bbad77 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/onsi/ginkgo/v2 v2.20.2 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect @@ -204,31 +183,21 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/redis/go-redis/v9 v9.6.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.6.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/go-diff-patch v0.0.0-20240223163233-798fd1e94a8e // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/stoewer/go-strcase v1.2.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/ulikunitz/xz v0.5.12 // indirect - github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -241,7 +210,6 @@ require ( go.etcd.io/etcd/api/v3 v3.5.14 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect go.etcd.io/etcd/client/v3 v3.5.14 // indirect - go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/otel v1.30.0 // indirect @@ -261,14 +229,13 @@ require ( golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.26.0 // indirect - google.golang.org/api v0.198.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 32ec296ec..27b99694b 100644 --- a/go.sum +++ b/go.sum @@ -15,14 +15,8 @@ cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTj cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go/auth v0.9.4 h1:DxF7imbEbiFu9+zdKC6cKBko1e8XeJnipNqIbWZ+kDI= -cloud.google.com/go/auth v0.9.4/go.mod h1:SHia8n6//Ya940F1rLimhJCjjx7KE17t0ctFEci3HkA= -cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= -cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= -cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= @@ -35,8 +29,6 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -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/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -61,8 +53,6 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= -github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= @@ -71,10 +61,6 @@ github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46 github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= -github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= -github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -92,16 +78,10 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= -github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= github.com/bodgit/sevenzip v1.5.2 h1:acMIYRaqoHAdeu9LhEGGjL9UzBD4RNf9z7+kWDNignI= @@ -110,12 +90,6 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bombsimon/logrusr/v4 v4.1.0 h1:uZNPbwusB0eUXlO8hIUwStE6Lr5bLN6IgYgG+75kuh4= github.com/bombsimon/logrusr/v4 v4.1.0/go.mod h1:pjfHC5e59CvjTBIU3V3sGhFWFAnsnhOR03TRc6im0l8= -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/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 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= @@ -145,12 +119,9 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= -github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= @@ -165,8 +136,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/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/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= @@ -184,9 +153,7 @@ github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtz github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -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/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -198,16 +165,13 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 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.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= -github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= -github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= @@ -226,10 +190,6 @@ github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZt github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= -github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= 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= @@ -277,7 +237,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU 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.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -296,7 +255,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw 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.1/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.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -310,27 +268,18 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= -github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= -github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gptscript-ai/broadcaster v0.0.0-20240625175512-c43682019b86 h1:m9yLtIEd0z1ia8qFjq3u0Ozb6QKwidyL856JLJp6nbA= @@ -364,8 +313,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo= github.com/hexops/autogold/v2 v2.2.1 h1:JPUXuZQGkcQMv7eeDXuNMovjfoRYaa0yVcm+F3voaGY= github.com/hexops/autogold/v2 v2.2.1/go.mod h1:IJwxtUfj1BGLm0YsR/k+dIxYi6xbeLjqGke2bzcOTMI= @@ -406,8 +353,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -443,8 +388,6 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8 github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -459,8 +402,6 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa h1:hI1uC2A3vJFjwvBn0G0a7QBRdBUp6Y048BtLAHRTKPo= -github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -472,8 +413,6 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= @@ -503,18 +442,12 @@ github.com/nwaples/rardecode/v2 v2.0.0-beta.4 h1:sdiJxQdPjECn2lh9nLFFhgLCf+0ulDU github.com/nwaples/rardecode/v2 v2.0.0-beta.4/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= -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/kinm v0.0.0-20241217210842-81947252da4e h1:Jchy17OBIk+l8Rd2Z1TNWtJx8ssw/Hv4sJd+HKoA/RM= github.com/obot-platform/kinm v0.0.0-20241217210842-81947252da4e/go.mod h1:RzrH0geIlbiTHDGZ8bpCk5k1hwdU9uu3l4zJn9n0pZU= github.com/obot-platform/nah v0.0.0-20241217120500-e9169e4a999f h1:yyexIHgaPtNrfaPLxDx+xbnibJTKKJK05jDDlIqXC04= github.com/obot-platform/nah v0.0.0-20241217120500-e9169e4a999f/go.mod h1:KG1jLO9FeYvCPGI0iDqe5oqDqOFLd3/dt/iwuMianmI= github.com/obot-platform/namegenerator v0.0.0-20241217121223-fc58bdb7dca2 h1:jiyBM/TYxU6UNVS9ff8Y8n55DOKDYohKkIZjfHpjfTY= github.com/obot-platform/namegenerator v0.0.0-20241217121223-fc58bdb7dca2/go.mod h1:isbKX6EfvvG/ojjFB2ZLyz27+2xoG3yRmpTSE+ytWEs= -github.com/obot-platform/oauth2-proxy/v7 v7.0.0-20241008204315-265dabe17f43 h1:mlwIf3/uOo0ISweKuyFHhvPzSut4oQeWWpTkzsmTPgE= -github.com/obot-platform/oauth2-proxy/v7 v7.0.0-20241008204315-265dabe17f43/go.mod h1:lxQ1wbphpjECcCoy8gfsrDHQVenNKgm+p6Oskdkl97g= -github.com/ohler55/ojg v1.24.1 h1:PaVLelrNgT5/0ppPaUtey54tOVp245z33fkhL2jljjY= -github.com/ohler55/ojg v1.24.1/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o= github.com/olekukonko/tablewriter v0.0.6-0.20230925090304-df64c4bbad77 h1:3bMMZ1f+GPXFQ1uNaYbO/uECWvSfqEA+ZEXn1rFAT88= github.com/olekukonko/tablewriter v0.0.6-0.20230925090304-df64c4bbad77/go.mod h1:8Hf+pH6thup1sPZPD+NLg7d6vbpsdilu9CPIeikvgMQ= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= @@ -523,8 +456,6 @@ github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4 github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= @@ -558,8 +489,6 @@ github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5b github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= -github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -577,10 +506,6 @@ github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= -github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= -github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -593,20 +518,12 @@ github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0 github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/go-diff-patch v0.0.0-20240223163233-798fd1e94a8e h1:H+jDTUeF+SVd4ApwnSFoew8ZwGNRfgb9EsZc7LcocAg= github.com/sourcegraph/go-diff-patch v0.0.0-20240223163233-798fd1e94a8e/go.mod h1:VsUklG6OQo7Ctunu0gS3AtEOCEc2kMB6r5rKzxAes58= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -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.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 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/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= @@ -626,8 +543,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= @@ -644,10 +559,6 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95 github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -676,8 +587,6 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.4 h1:vCwMkPZSNefSUnOW2ZKRUjBSD5Ok3W78IXhGxxAEF90= github.com/yuin/goldmark-emoji v1.0.4/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0= @@ -698,8 +607,6 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= @@ -737,7 +644,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -787,7 +693,6 @@ golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200202094626-16171245cfb2/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-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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -795,7 +700,6 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -844,13 +748,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -861,8 +764,6 @@ golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -876,7 +777,6 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -927,8 +827,6 @@ google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.198.0 h1:OOH5fZatk57iN0A7tjJQzt6aPfYQ1JiWkt1yGseazks= -google.golang.org/api v0.198.0/go.mod h1:/Lblzl3/Xqqk9hw/yS97TImKTUwnf1bv89v7+OagJzc= 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= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -950,19 +848,17 @@ google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU= google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4= -google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= -google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 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.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -971,7 +867,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ 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.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= @@ -986,8 +881,6 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/pkg/accesstoken/accesstoken.go b/pkg/accesstoken/accesstoken.go new file mode 100644 index 000000000..69cf5e182 --- /dev/null +++ b/pkg/accesstoken/accesstoken.go @@ -0,0 +1,14 @@ +package accesstoken + +import "context" + +type accessTokenKey struct{} + +func ContextWithAccessToken(ctx context.Context, accessToken string) context.Context { + return context.WithValue(ctx, accessTokenKey{}, accessToken) +} + +func GetAccessToken(ctx context.Context) string { + accessToken, _ := ctx.Value(accessTokenKey{}).(string) + return accessToken +} diff --git a/pkg/api/authz/authz.go b/pkg/api/authz/authz.go index ab957506c..87cc41332 100644 --- a/pkg/api/authz/authz.go +++ b/pkg/api/authz/authz.go @@ -36,15 +36,18 @@ var staticRules = map[string][]string{ "POST /api/token-request", "GET /api/token-request/{id}/{service}", - "GET /api/auth-providers", - "GET /api/auth-providers/{slug}", - "GET /api/oauth/start/{id}/{service}", + // The bootstrap logout just deletes a cookie in the client, and does nothing else. + "POST /api/bootstrap/logout", + "GET /api/app-oauth/authorize/{id}", "GET /api/app-oauth/refresh/{id}", "GET /api/app-oauth/callback/{id}", "GET /api/app-oauth/get-token", + + "GET /api/auth-providers", + "GET /api/auth-providers/{id}", }, AuthenticatedGroup: { "/api/oauth/redirect/{service}", diff --git a/pkg/api/handlers/authprovider.go b/pkg/api/handlers/authprovider.go new file mode 100644 index 000000000..b76b52ff8 --- /dev/null +++ b/pkg/api/handlers/authprovider.go @@ -0,0 +1,216 @@ +package handlers + +import ( + "fmt" + "strings" + + "github.com/gptscript-ai/go-gptscript" + "github.com/obot-platform/obot/apiclient/types" + "github.com/obot-platform/obot/pkg/api" + "github.com/obot-platform/obot/pkg/gateway/server/dispatcher" + v1 "github.com/obot-platform/obot/pkg/storage/apis/otto.otto8.ai/v1" + "k8s.io/apimachinery/pkg/fields" + kclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +type AuthProviderHandler struct { + gptscript *gptscript.GPTScript + dispatcher *dispatcher.Dispatcher +} + +// TODO - support deconfiguring auth providers + +func NewAuthProviderHandler(gClient *gptscript.GPTScript, dispatcher *dispatcher.Dispatcher) *AuthProviderHandler { + return &AuthProviderHandler{ + gptscript: gClient, + dispatcher: dispatcher, + } +} + +func (ap *AuthProviderHandler) ByID(req api.Context) error { + var ref v1.ToolReference + if err := req.Get(&ref, req.PathValue("id")); err != nil { + return err + } + + if ref.Spec.Type != types.ToolReferenceTypeAuthProvider { + return types.NewErrNotFound( + "auth provider %q not found", + ref.Name, + ) + } + + var credEnvVars map[string]string + if ref.Status.Tool != nil { + if envVars := ref.Status.Tool.Metadata["envVars"]; envVars != "" { + fmt.Printf("revealing creds for auth provider %q\n", ref.Name) + cred, err := ap.gptscript.RevealCredential(req.Context(), []string{string(ref.UID)}, ref.Name) + if err != nil && !strings.HasSuffix(err.Error(), "credential not found") { + return fmt.Errorf("failed to reveal credential for auth provider %q: %w", ref.Name, err) + } else if err == nil { + credEnvVars = cred.Env + } + } + } + + return req.Write(convertToolReferenceToAuthProvider(ref, credEnvVars)) +} + +func (ap *AuthProviderHandler) List(req api.Context) error { + var refList v1.ToolReferenceList + if err := req.List(&refList, &kclient.ListOptions{ + Namespace: req.Namespace(), + FieldSelector: fields.SelectorFromSet(map[string]string{ + "spec.type": string(types.ToolReferenceTypeAuthProvider), + }), + }); err != nil { + return err + } + + credCtxs := make([]string, 0, len(refList.Items)) + for _, ref := range refList.Items { + credCtxs = append(credCtxs, string(ref.UID)) + } + + creds, err := ap.gptscript.ListCredentials(req.Context(), gptscript.ListCredentialsOptions{ + CredentialContexts: credCtxs, + }) + if err != nil { + return fmt.Errorf("failed to list auth provider credentials: %w", err) + } + + credMap := make(map[string]map[string]string, len(creds)) + for _, cred := range creds { + credMap[cred.Context+cred.ToolName] = cred.Env + } + + resp := make([]types.AuthProvider, 0, len(refList.Items)) + for _, ref := range refList.Items { + resp = append(resp, convertToolReferenceToAuthProvider(ref, credMap[string(ref.UID)+ref.Name])) + } + + return req.Write(types.AuthProviderList{Items: resp}) +} + +func (ap *AuthProviderHandler) Configure(req api.Context) error { + var ref v1.ToolReference + if err := req.Get(&ref, req.PathValue("id")); err != nil { + return err + } + + if ref.Spec.Type != types.ToolReferenceTypeAuthProvider { + return types.NewErrBadRequest("%q is not an auth provider", ref.Name) + } + + var envVars map[string]string + if err := req.Read(&envVars); err != nil { + return err + } + + // Allow for updating credentials. The only way to update a credential is to delete the existing one and recreate it. + if err := ap.gptscript.DeleteCredential(req.Context(), string(ref.UID), ref.Name); err != nil && !strings.HasSuffix(err.Error(), "credential not found") { + return fmt.Errorf("failed to update credential: %w", err) + } + + for key, val := range envVars { + if val == "" { + delete(envVars, key) + } + } + + if err := ap.gptscript.CreateCredential(req.Context(), gptscript.Credential{ + Context: string(ref.UID), + ToolName: ref.Name, + Type: gptscript.CredentialTypeTool, + Env: envVars, + }); err != nil { + return fmt.Errorf("failed to create credential for auth provider %q: %w", ref.Name, err) + } + + ap.dispatcher.StopAuthProvider(ref.Namespace, ref.Name) + + if ref.Annotations[v1.AuthProviderSyncAnnotation] == "" { + if ref.Annotations == nil { + ref.Annotations = make(map[string]string, 1) + } + ref.Annotations[v1.AuthProviderSyncAnnotation] = "true" + } else { + delete(ref.Annotations, v1.AuthProviderSyncAnnotation) + } + + return req.Update(&ref) +} + +func (ap *AuthProviderHandler) Reveal(req api.Context) error { + var ref v1.ToolReference + if err := req.Get(&ref, req.PathValue("id")); err != nil { + return err + } + + if ref.Spec.Type != types.ToolReferenceTypeAuthProvider { + return types.NewErrBadRequest("%q is not an auth provider", ref.Name) + } + + fmt.Printf("revealing creds for auth provider %q\n", ref.Name) + cred, err := ap.gptscript.RevealCredential(req.Context(), []string{string(ref.UID)}, ref.Name) + if err != nil && !strings.HasSuffix(err.Error(), "credential not found") { + return fmt.Errorf("failed to reveal credential for auth provider %q: %w", ref.Name, err) + } else if err == nil { + return req.Write(cred.Env) + } + + return types.NewErrNotFound("no credential found for %q", ref.Name) +} + +func convertToolReferenceToAuthProvider(ref v1.ToolReference, credEnvVars map[string]string) types.AuthProvider { + name := ref.Name + if ref.Status.Tool != nil { + name = ref.Status.Tool.Name + } + + ap := types.AuthProvider{ + Metadata: MetadataFrom(&ref), + AuthProviderManifest: types.AuthProviderManifest{ + Name: name, + Namespace: ref.Namespace, + ToolReference: ref.Spec.Reference, + }, + AuthProviderStatus: *convertAuthProviderToolRef(ref, credEnvVars), + } + + ap.Type = "authprovider" + + return ap +} + +func convertAuthProviderToolRef(toolRef v1.ToolReference, cred map[string]string) *types.AuthProviderStatus { + var ( + requiredEnvVars, missingEnvVars, optionalEnvVars []string + icon string + ) + if toolRef.Status.Tool != nil { + if toolRef.Status.Tool.Metadata["envVars"] != "" { + requiredEnvVars = strings.Split(toolRef.Status.Tool.Metadata["envVars"], ",") + } + + for _, envVar := range requiredEnvVars { + if _, ok := cred[envVar]; !ok { + missingEnvVars = append(missingEnvVars, envVar) + } + } + + icon = toolRef.Status.Tool.Metadata["icon"] + + if optionalEnvVarMetadata := toolRef.Status.Tool.Metadata["optionalEnvVars"]; optionalEnvVarMetadata != "" { + optionalEnvVars = strings.Split(optionalEnvVarMetadata, ",") + } + } + + return &types.AuthProviderStatus{ + Icon: icon, + Configured: toolRef.Status.Tool != nil && len(missingEnvVars) == 0, + RequiredConfigurationParameters: requiredEnvVars, + MissingConfigurationParameters: missingEnvVars, + OptionalConfigurationParameters: optionalEnvVars, + } +} diff --git a/pkg/api/request.go b/pkg/api/request.go index fa25d54a6..53179a3c0 100644 --- a/pkg/api/request.go +++ b/pkg/api/request.go @@ -271,15 +271,17 @@ func (r *Context) UserID() uint { return uint(userID) } -func (r *Context) AuthProviderID() uint { - extraAuthProvider := r.User.GetExtra()["auth_provider_id"] - if len(extraAuthProvider) == 0 { - return 0 +func (r *Context) AuthProviderNameAndNamespace() (string, string) { + extraName := r.User.GetExtra()["auth_provider_name"] + extraNamespace := r.User.GetExtra()["auth_provider_namespace"] + + var name, namespace string + if len(extraName) > 0 { + name = extraName[0] } - authProviderID, err := strconv.ParseUint(extraAuthProvider[0], 10, 64) - if err != nil { - return 0 + if len(extraNamespace) > 0 { + namespace = extraNamespace[0] } - return uint(authProviderID) + return name, namespace } diff --git a/pkg/api/router/router.go b/pkg/api/router/router.go index 9c165e96a..14795ffb2 100644 --- a/pkg/api/router/router.go +++ b/pkg/api/router/router.go @@ -22,8 +22,9 @@ func Router(services *services.Services) (http.Handler, error) { webhooks := handlers.NewWebhookHandler() cronJobs := handlers.NewCronJobHandler() models := handlers.NewModelHandler() - availableModels := handlers.NewAvailableModelsHandler(services.GPTClient, services.ModelProviderDispatcher) - modelProviders := handlers.NewModelProviderHandler(services.GPTClient, services.ModelProviderDispatcher) + availableModels := handlers.NewAvailableModelsHandler(services.GPTClient, services.ProviderDispatcher) + modelProviders := handlers.NewModelProviderHandler(services.GPTClient, services.ProviderDispatcher) + authProviders := handlers.NewAuthProviderHandler(services.GPTClient, services.ProviderDispatcher) prompt := handlers.NewPromptHandler(services.GPTClient) emailreceiver := handlers.NewEmailReceiverHandler(services.EmailServerName) defaultModelAliases := handlers.NewDefaultModelAliasHandler() @@ -262,6 +263,16 @@ func Router(services *services.Services) (http.Handler, error) { mux.HandleFunc("POST /api/model-providers/{id}/reveal", modelProviders.Reveal) mux.HandleFunc("POST /api/model-providers/{id}/refresh-models", modelProviders.RefreshModels) + // Auth providers + mux.HandleFunc("GET /api/auth-providers", authProviders.List) + mux.HandleFunc("GET /api/auth-providers/{id}", authProviders.ByID) + mux.HandleFunc("POST /api/auth-providers/{id}/configure", authProviders.Configure) + mux.HandleFunc("POST /api/auth-providers/{id}/reveal", authProviders.Reveal) + + // Bootstrap + mux.HandleFunc("POST /api/bootstrap/login", services.Bootstrapper.Login) + mux.HandleFunc("POST /api/bootstrap/logout", services.Bootstrapper.Logout) + // Models mux.HandleFunc("POST /api/models", models.Create) mux.HandleFunc("PUT /api/models/{id}", models.Update) diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 6ad55a25a..f373957f5 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -3,7 +3,6 @@ package server import ( "errors" "net/http" - "slices" "strings" "github.com/gptscript-ai/go-gptscript" @@ -21,19 +20,19 @@ type Server struct { gptClient *gptscript.GPTScript authenticator *authn.Authenticator authorizer *authz.Authorizer - proxyServer *proxy.Proxy + proxyManager *proxy.Manager baseURL string mux *http.ServeMux } -func NewServer(storageClient storage.Client, gptClient *gptscript.GPTScript, authn *authn.Authenticator, authz *authz.Authorizer, proxyServer *proxy.Proxy, baseURL string) *Server { +func NewServer(storageClient storage.Client, gptClient *gptscript.GPTScript, authn *authn.Authenticator, authz *authz.Authorizer, proxyManager *proxy.Manager, baseURL string) *Server { return &Server{ storageClient: storageClient, gptClient: gptClient, authenticator: authn, authorizer: authz, - proxyServer: proxyServer, + proxyManager: proxyManager, baseURL: baseURL + "/api", mux: http.NewServeMux(), @@ -57,14 +56,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) wrap(f api.HandlerFunc) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { - // If this header is set, then the session was deemed to be invalid and the request has come back around through the proxy. - // The cookie on the request is still invalid because the new one has not been sent back to the browser. - // Therefore, respond with a redirect so that the browser will redirect back to the original request with the new cookie. - if req.Header.Get("X-Otto-Auth-Required") == "true" { - http.Redirect(rw, req, req.RequestURI, http.StatusFound) - return - } - rw.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0") rw.Header().Set("Pragma", "no-cache") rw.Header().Set("Expires", "0") @@ -75,16 +66,19 @@ func (s *Server) wrap(f api.HandlerFunc) http.HandlerFunc { return } - isOAuthPath := strings.HasPrefix(req.URL.Path, "/oauth2/") - if isOAuthPath || strings.HasPrefix(req.URL.Path, "/api/") && !s.authorizer.Authorize(req, user) { - // If this is not a request coming from browser or the proxy is not enabled, then return 403. - if !isOAuthPath && (s.proxyServer == nil || req.Method != http.MethodGet || slices.Contains(user.GetGroups(), authz.AuthenticatedGroup) || !strings.Contains(strings.ToLower(req.UserAgent()), "mozilla")) { - http.Error(rw, "forbidden", http.StatusForbidden) + if !s.authorizer.Authorize(req, user) { + if strings.HasPrefix(req.URL.Path, "/api/") { + if user.GetName() == "anonymous" { + http.Error(rw, "unauthorized", http.StatusUnauthorized) + } else { + http.Error(rw, "forbidden", http.StatusForbidden) + } return } + } - req.Header.Set("X-Otto-Auth-Required", "true") - s.proxyServer.ServeHTTP(rw, req) + if strings.HasPrefix(req.URL.Path, "/oauth2/") { + s.proxyManager.ServeHTTP(rw, req) return } diff --git a/pkg/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go new file mode 100644 index 000000000..2bfa6825c --- /dev/null +++ b/pkg/bootstrap/bootstrap.go @@ -0,0 +1,102 @@ +package bootstrap + +import ( + "crypto/rand" + "fmt" + "net/http" + "os" + "strings" + + "github.com/obot-platform/obot/pkg/api" + "github.com/obot-platform/obot/pkg/api/authz" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +const bootstrapCookie = "obot-bootstrap" + +type Bootstrap struct { + token, serverURL string +} + +func New(serverURL string) (*Bootstrap, error) { + token := os.Getenv("OBOT_BOOTSTRAP_TOKEN") + + if token == "" { + bytes := make([]byte, 32) + _, err := rand.Read(bytes) + if err != nil { + return nil, fmt.Errorf("failed to generate random token: %w", err) + } + + token = fmt.Sprintf("%x", bytes) + } + + fmt.Printf("Bootstrap token: %s\n", token) + + return &Bootstrap{ + token: token, + serverURL: serverURL, + }, nil +} + +func (b *Bootstrap) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { + authHeader := req.Header.Get("Authorization") + if authHeader == "" { + // Check for the cookie. + c, err := req.Cookie(bootstrapCookie) + if err != nil || c.Value != b.token { + return nil, false, nil + } + } else if authHeader != fmt.Sprintf("Bearer %s", b.token) { + return nil, false, nil + } + + return &authenticator.Response{ + User: &user.DefaultInfo{ + Name: "bootstrap", + UID: "bootstrap", + Groups: []string{ + authz.AdminGroup, + authz.AuthenticatedGroup, + }, + }, + }, true, nil +} + +func (b *Bootstrap) Login(req api.Context) error { + auth := req.Request.Header.Get("Authorization") + if auth == "" { + http.Error(req.ResponseWriter, "missing Authorization header", http.StatusBadRequest) + return nil + } else if auth != fmt.Sprintf("Bearer %s", b.token) { + http.Error(req.ResponseWriter, "invalid token", http.StatusUnauthorized) + return nil + } + + http.SetCookie(req.ResponseWriter, &http.Cookie{ + Name: bootstrapCookie, + Value: strings.TrimPrefix(auth, "Bearer "), + Path: "/", + MaxAge: 60 * 60 * 24 * 7, // 1 week + HttpOnly: true, + Secure: strings.HasPrefix(b.serverURL, "https://"), + }) + http.Redirect(req.ResponseWriter, req.Request, "/admin/auth-providers", http.StatusFound) + + return nil +} + +func (b *Bootstrap) Logout(req api.Context) error { + fmt.Printf("logging out bootstrap user\n") + http.SetCookie(req.ResponseWriter, &http.Cookie{ + Name: bootstrapCookie, + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + Secure: strings.HasPrefix(b.serverURL, "https://"), + }) + + return nil +} diff --git a/pkg/cli/internal/token.go b/pkg/cli/internal/token.go index 6550d0c8e..d54f1f3f7 100644 --- a/pkg/cli/internal/token.go +++ b/pkg/cli/internal/token.go @@ -45,7 +45,7 @@ func Token(ctx context.Context, baseURL string) (string, error) { return "", nil } - serviceName, err := getAuthProviderServiceName(ctx, baseURL) + serviceNamespace, serviceName, err := getAuthProviderServiceInfo(ctx, baseURL) if err != nil { return "", err } @@ -77,7 +77,7 @@ func Token(ctx context.Context, baseURL string) (string, error) { } uuid := uuid.NewString() - loginURL, err := create(ctx, baseURL, uuid, serviceName) + loginURL, err := create(ctx, baseURL, uuid, serviceName, serviceNamespace) if err != nil { return "", fmt.Errorf("failed to create login request: %w", err) } @@ -118,17 +118,22 @@ func Token(ctx context.Context, baseURL string) (string, error) { } type createRequest struct { - ServiceName string `json:"serviceName,omitempty"` - ID string `json:"id,omitempty"` + ServiceName string `json:"serviceName,omitempty"` + ServiceNamespace string `json:"serviceNamespace,omitempty"` + ID string `json:"id,omitempty"` } type createResponse struct { TokenPath string `json:"token-path,omitempty"` } -func create(ctx context.Context, baseURL, uuid, serviceName string) (string, error) { +func create(ctx context.Context, baseURL, uuid, serviceName, serviceNamespace string) (string, error) { var data bytes.Buffer - if err := json.NewEncoder(&data).Encode(createRequest{ID: uuid, ServiceName: serviceName}); err != nil { + if err := json.NewEncoder(&data).Encode(createRequest{ + ID: uuid, + ServiceName: serviceName, + ServiceNamespace: serviceNamespace, + }); err != nil { return "", err } @@ -210,27 +215,28 @@ func testToken(ctx context.Context, baseURL, token string) bool { return resp.StatusCode == 200 && user.Username != "anonymous" } -func getAuthProviderServiceName(ctx context.Context, baseURL string) (string, error) { +func getAuthProviderServiceInfo(ctx context.Context, baseURL string) (string, string, error) { req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/auth-providers", nil) if err != nil { - return "", err + return "", "", err } resp, err := http.DefaultClient.Do(req) if err != nil { - return "", err + return "", "", err } defer resp.Body.Close() - var authProviders []types.AuthProvider + var authProviders types2.AuthProviderList if err := json.NewDecoder(resp.Body).Decode(&authProviders); err != nil { - return "", err + return "", "", err } - if len(authProviders) == 0 { - return "", fmt.Errorf("no auth providers found") + if len(authProviders.Items) == 0 { + return "", "", fmt.Errorf("no auth providers found") } // Take the last auth provider. That is the one created most recently. - return authProviders[len(authProviders)-1].ServiceName, nil + lastProvider := authProviders.Items[len(authProviders.Items)-1] + return lastProvider.Namespace, lastProvider.Name, nil } diff --git a/pkg/controller/data/agent.yaml b/pkg/controller/data/agent.yaml index 22474ab8e..c7377cce8 100644 --- a/pkg/controller/data/agent.yaml +++ b/pkg/controller/data/agent.yaml @@ -12,10 +12,10 @@ spec: collapsed: /images/obot-logo-blue-black-text.svg collapsedDark: /images/obot-logo-blue-white-text.svg prompt: | - You are an AI assistance developed by Acorn Labs named Obot. You are described as follows: + You are an AI assistant developed by Acorn Labs named Obot. You are described as follows: Obot is a conversational AI assistant that can help an end user with a variety of tasks by using tools, reading/writing - files in the workspace, and querying it's knowledge database. The user interacting with Obot is doing so through a chat + files in the workspace, and querying its knowledge database. The user interacting with Obot is doing so through a chat interface and can ask questions and view/edit the files in the workspace. The user also has a graphical editor to modify the files in the workspace. Obot collaborates with the user on the files in the workspace. alias: obot diff --git a/pkg/controller/handlers/toolreference/toolreference.go b/pkg/controller/handlers/toolreference/toolreference.go index bb02e9753..8cc168ab5 100644 --- a/pkg/controller/handlers/toolreference/toolreference.go +++ b/pkg/controller/handlers/toolreference/toolreference.go @@ -43,6 +43,7 @@ type index struct { KnowledgeDocumentLoaders map[string]indexEntry `json:"knowledgeDocumentLoaders,omitempty"` System map[string]indexEntry `json:"system,omitempty"` ModelProviders map[string]indexEntry `json:"modelProviders,omitempty"` + AuthProviders map[string]indexEntry `json:"authProviders,omitempty"` } type Handler struct { @@ -173,6 +174,7 @@ func (h *Handler) readFromRegistry(ctx context.Context, c client.Client) error { toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeSystem, index.System)...) toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeModelProvider, index.ModelProviders)...) + toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeAuthProvider, index.AuthProviders)...) toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeTool, index.Tools)...) toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeStepTemplate, index.StepTemplates)...) toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeKnowledgeDataSource, index.KnowledgeDataSources)...) diff --git a/pkg/controller/routes.go b/pkg/controller/routes.go index f800ee8ed..f99113c85 100644 --- a/pkg/controller/routes.go +++ b/pkg/controller/routes.go @@ -29,7 +29,7 @@ func (c *Controller) setupRoutes() error { workflowExecution := workflowexecution.New(c.services.Invoker) workflowStep := workflowstep.New(c.services.Invoker) - toolRef := toolreference.New(c.services.GPTClient, c.services.ModelProviderDispatcher, c.services.ToolRegistryURL) + toolRef := toolreference.New(c.services.GPTClient, c.services.ProviderDispatcher, c.services.ToolRegistryURL) workspace := workspace.New(c.services.GPTClient, c.services.WorkspaceProviderType) knowledgeset := knowledgeset.New(c.services.AIHelper, c.services.Invoker) knowledgesource := knowledgesource.NewHandler(c.services.Invoker, c.services.GPTClient) diff --git a/pkg/gateway/client/auth.go b/pkg/gateway/client/auth.go index 69a03a06a..2cd142fde 100644 --- a/pkg/gateway/client/auth.go +++ b/pkg/gateway/client/auth.go @@ -33,9 +33,10 @@ func (u UserDecorator) AuthenticateRequest(req *http.Request) (*authenticator.Re } gatewayUser, err := u.client.EnsureIdentity(req.Context(), &types.Identity{ - Email: firstValue(resp.User.GetExtra(), "email"), - AuthProviderID: uint(firstValueAsInt(resp.User.GetExtra(), "auth_provider_id")), - ProviderUsername: resp.User.GetName(), + Email: firstValue(resp.User.GetExtra(), "email"), + AuthProviderName: firstValue(resp.User.GetExtra(), "auth_provider_name"), + AuthProviderNamespace: firstValue(resp.User.GetExtra(), "auth_provider_namespace"), + ProviderUsername: resp.User.GetName(), }) if err != nil { return nil, false, err diff --git a/pkg/gateway/client/client.go b/pkg/gateway/client/client.go index 143bcd1e7..e94895256 100644 --- a/pkg/gateway/client/client.go +++ b/pkg/gateway/client/client.go @@ -1,8 +1,6 @@ package client import ( - "strconv" - "github.com/obot-platform/obot/pkg/gateway/db" ) @@ -33,9 +31,3 @@ func firstValue(m map[string][]string, key string) string { } return values[0] } - -func firstValueAsInt(m map[string][]string, key string) int { - value := firstValue(m, key) - v, _ := strconv.Atoi(value) - return v -} diff --git a/pkg/gateway/client/user.go b/pkg/gateway/client/user.go index f180b79e6..0dec00765 100644 --- a/pkg/gateway/client/user.go +++ b/pkg/gateway/client/user.go @@ -7,8 +7,8 @@ import ( "net/http" "time" + "github.com/obot-platform/obot/pkg/accesstoken" "github.com/obot-platform/obot/pkg/gateway/types" - "github.com/obot-platform/obot/pkg/proxy" "gorm.io/gorm" ) @@ -22,27 +22,23 @@ func (c *Client) UserByID(ctx context.Context, id string) (*types.User, error) { return u, c.db.WithContext(ctx).Where("id = ?", id).First(u).Error } -func (c *Client) UpdateProfileIconIfNeeded(ctx context.Context, user *types.User, authProviderID uint) error { - if authProviderID == 0 { +func (c *Client) UpdateProfileIconIfNeeded(ctx context.Context, user *types.User, authProviderName, authProviderNamespace, authProviderURL string) error { + if authProviderName == "" || authProviderNamespace == "" || authProviderURL == "" { return nil } - accessToken := proxy.GetAccessToken(ctx) + accessToken := accesstoken.GetAccessToken(ctx) if accessToken == "" { return nil } var ( - authProvider types.AuthProvider - identity types.Identity + identity types.Identity ) - if err := c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - if err := tx.Where("id = ?", authProviderID).First(&authProvider).Error; err != nil { - return err - } - - return tx.Where("user_id = ?", user.ID).Where("auth_provider_id = ?", authProviderID).First(&identity).Error - }); err != nil { + if err := c.db.WithContext(ctx).Where("user_id = ?", user.ID). + Where("auth_provider_name = ?", authProviderName). + Where("auth_provider_namespace = ?", authProviderNamespace). + First(&identity).Error; err != nil { return err } @@ -51,7 +47,7 @@ func (c *Client) UpdateProfileIconIfNeeded(ctx context.Context, user *types.User return nil } - profileIconURL, err := c.fetchProfileIconURL(ctx, authProvider, user.Username, accessToken) + profileIconURL, err := c.fetchProfileIconURL(ctx, authProviderURL, accessToken) if err != nil { return err } @@ -68,71 +64,30 @@ func (c *Client) UpdateProfileIconIfNeeded(ctx context.Context, user *types.User }) } -func (c *Client) fetchProfileIconURL(ctx context.Context, authProvider types.AuthProvider, username, accessToken string) (string, error) { - switch authProvider.Type { - case types.AuthTypeGoogle: - return c.fetchGoogleProfileIconURL(ctx, accessToken) - case types.AuthTypeGitHub: - return c.fetchGitHubProfileIconURL(ctx, username) - default: - return "", fmt.Errorf("unsupported auth provider type for icon fetch: %s", authProvider.Type) +func (c *Client) fetchProfileIconURL(ctx context.Context, authProviderURL, accessToken string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, authProviderURL+"/obot-get-icon-url", nil) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) } -} -type googleProfile struct { - ID string `json:"id"` - Email string `json:"email"` - VerifiedEmail bool `json:"verified_email"` - Name string `json:"name"` - GivenName string `json:"given_name"` - FamilyName string `json:"family_name"` - Picture string `json:"picture"` - HD string `json:"hd"` -} + req.Header.Set("Authorization", "Bearer "+accessToken) -func (c *Client) fetchGoogleProfileIconURL(ctx context.Context, accessToken string) (string, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.googleapis.com/oauth2/v1/userinfo", nil) - if err != nil { - return "", err - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) resp, err := http.DefaultClient.Do(req) if err != nil { - return "", err + return "", fmt.Errorf("failed to fetch profile icon URL: %w", err) } defer resp.Body.Close() - var profile googleProfile - if err = json.NewDecoder(resp.Body).Decode(&profile); err != nil { - return "", err + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch profile icon URL: %s", resp.Status) } - return profile.Picture, nil -} - -func (c *Client) fetchGitHubProfileIconURL(ctx context.Context, username string) (string, error) { - // GitHub will automatically redirect this URL to the user's GitHub profile icon. - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://github.com/%s.png", username), nil) - if err != nil { - return "", err - } - - resp, err := (&http.Client{ - CheckRedirect: func(*http.Request, []*http.Request) error { - // Don't follow redirects, tiny optimization to only make one request. - return http.ErrUseLastResponse - }, - }).Do(req) - if err != nil { - return "", err + var body struct { + IconURL string `json:"iconURL"` } - defer resp.Body.Close() - - // Get the final URL that GitHub redirected to. - u, err := resp.Location() - if err != nil || u == nil { - return "", err + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + return "", fmt.Errorf("failed to decode response: %w", err) } - return u.String(), nil + return body.IconURL, nil } diff --git a/pkg/gateway/db/db.go b/pkg/gateway/db/db.go index 44aa3b252..ebcd41796 100644 --- a/pkg/gateway/db/db.go +++ b/pkg/gateway/db/db.go @@ -41,7 +41,6 @@ func (db *DB) AutoMigrate() (err error) { types.AuthToken{}, types.TokenRequest{}, types.LLMProxyActivity{}, - types.AuthProvider{}, types.LLMProvider{}, types.Model{}, types.OAuthTokenRequestChallenge{}, diff --git a/pkg/gateway/pkce/pkce.go b/pkg/gateway/pkce/pkce.go deleted file mode 100644 index c31da6b7e..000000000 --- a/pkg/gateway/pkce/pkce.go +++ /dev/null @@ -1,61 +0,0 @@ -package pkce - -import ( - "crypto/rand" - "crypto/sha256" - "encoding/base64" - "fmt" - "strings" -) - -type Info struct { - CodeVerifier, CodeChallenge, Method string -} - -const ( - pkceLength = 128 - pkceMethod = "S256" - codeVerifierCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -) - -// generateCodeVerifier generates a random code verifier of the specified length -func generateCodeVerifier(length int) (string, error) { - b := make([]byte, length) - if _, err := rand.Read(b); err != nil { - return "", err - } - - for i := range b { - b[i] = codeVerifierCharset[b[i]%byte(len(codeVerifierCharset))] - } - return string(b), nil -} - -// generateCodeChallengeS256 generates a S256 code challenge from the code verifier -func generateCodeChallengeS256(codeVerifier string) string { - h := sha256.New() - h.Write([]byte(codeVerifier)) - hash := h.Sum(nil) - return base64URLEncode(hash) -} - -// base64URLEncode encodes the input bytes to a URL-safe, base64-encoded string -func base64URLEncode(input []byte) string { - encoded := base64.RawURLEncoding.EncodeToString(input) - encoded = strings.TrimRight(encoded, "=") - return encoded -} - -func GetPKCE() (Info, error) { - codeVerifier, err := generateCodeVerifier(pkceLength) - if err != nil { - return Info{}, fmt.Errorf("failed to generate code verifier: %w", err) - } - - codeChallenge := generateCodeChallengeS256(codeVerifier) - return Info{ - CodeVerifier: codeVerifier, - CodeChallenge: codeChallenge, - Method: pkceMethod, - }, nil -} diff --git a/pkg/gateway/server/authprovider.go b/pkg/gateway/server/authprovider.go deleted file mode 100644 index 7edc484dd..000000000 --- a/pkg/gateway/server/authprovider.go +++ /dev/null @@ -1,241 +0,0 @@ -package server - -import ( - "errors" - "fmt" - "net/http" - - types2 "github.com/obot-platform/obot/apiclient/types" - "github.com/obot-platform/obot/pkg/api" - kcontext "github.com/obot-platform/obot/pkg/gateway/context" - ktime "github.com/obot-platform/obot/pkg/gateway/time" - "github.com/obot-platform/obot/pkg/gateway/types" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type authProviderResponse struct { - types.AuthProvider `json:",inline"` - RedirectURL string `json:"redirectURL"` -} - -func (s *Server) createAuthProvider(apiContext api.Context) error { - logger := kcontext.GetLogger(apiContext.Context()) - oauthProvider := new(types.AuthProvider) - - if err := apiContext.Read(oauthProvider); err != nil { - logger.DebugContext(apiContext.Context(), "failed to decode oauth provider", "error", err) - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, http.StatusBadRequest, fmt.Errorf("invalid auth provider request body: %v", err)) - return nil - } - - if err := oauthProvider.ValidateAndSetDefaults(); err != nil { - logger.DebugContext(apiContext.Context(), "failed to validate oauth provider", "error", err) - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, http.StatusBadRequest, fmt.Errorf("invalid auth provider: %v", err)) - return nil - } - - if err := s.db.WithContext(apiContext.Context()).Clauses(clause.Returning{}).Create(oauthProvider).Error; err != nil { - status := http.StatusInternalServerError - if errors.Is(err, gorm.ErrDuplicatedKey) || errors.Is(err, gorm.ErrCheckConstraintViolated) { - status = http.StatusBadRequest - } - - logger.DebugContext(apiContext.Context(), "failed to create auth provider", "error", err, "status", status) - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, status, fmt.Errorf("failed to create auth provider: %v", err)) - return nil - } - - oauthProvider.ClientSecret = "" - writeResponse(apiContext.Context(), logger, apiContext.ResponseWriter, authProviderResponse{AuthProvider: *oauthProvider, RedirectURL: oauthProvider.RedirectURL(s.baseURL)}) - return nil -} - -func (s *Server) updateAuthProvider(apiContext api.Context) error { - logger := kcontext.GetLogger(apiContext.Context()) - oauthProvider := new(types.AuthProvider) - - if err := apiContext.Read(oauthProvider); err != nil { - logger.DebugContext(apiContext.Context(), "failed to decode oauth provider", "error", err) - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, http.StatusBadRequest, fmt.Errorf("invalid auth provider request body: %v", err)) - return nil - } - - // If the expiration field is being changed, ensure the expiration dur field is also updated. - if oauthProvider.Expiration != "" { - var err error - oauthProvider.ExpirationDur, err = ktime.ParseDuration(oauthProvider.Expiration) - if err != nil { - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, http.StatusBadRequest, fmt.Errorf("invalid expiration duration: %v", err)) - return nil - } - } - - if err := s.db.WithContext(apiContext.Context()).Where("slug = ?", apiContext.PathValue("slug")).Updates(oauthProvider).Error; err != nil { - status := http.StatusInternalServerError - if errors.Is(err, gorm.ErrDuplicatedKey) || errors.Is(err, gorm.ErrCheckConstraintViolated) { - status = http.StatusBadRequest - } - - logger.DebugContext(apiContext.Context(), "failed to update auth provider", "error", err, "status", status) - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, status, fmt.Errorf("failed to create auth provider: %v", err)) - return nil - } - - oauthProvider.ClientSecret = "" - writeResponse(apiContext.Context(), logger, apiContext.ResponseWriter, authProviderResponse{AuthProvider: *oauthProvider, RedirectURL: oauthProvider.RedirectURL(s.baseURL)}) - return nil -} - -func (s *Server) getAuthProviders(apiContext api.Context) error { - var authProviders []types.AuthProvider - if err := s.db.WithContext(apiContext.Context()).Order("id ASC").Find(&authProviders).Error; err != nil { - return types2.NewErrHttp(http.StatusInternalServerError, err.Error()) - } - - resp := make([]authProviderResponse, len(authProviders)) - for i, authProvider := range authProviders { - authProvider.ClientSecret = "" - resp[i] = authProviderResponse{ - AuthProvider: authProvider, - RedirectURL: authProvider.RedirectURL(s.baseURL), - } - } - - return apiContext.Write(resp) -} - -func (s *Server) getAuthProvider(apiContext api.Context) error { - slug := apiContext.PathValue("slug") - if slug == "" { - return types2.NewErrHttp(http.StatusBadRequest, "id path parameter is required") - } - - oauthProvider := new(types.AuthProvider) - if err := s.db.WithContext(apiContext.Context()).Where("slug = ?", slug).Find(oauthProvider).Error; err != nil { - status := http.StatusInternalServerError - if errors.Is(err, gorm.ErrRecordNotFound) { - status = http.StatusNotFound - } - - return types2.NewErrHttp(status, fmt.Sprintf("failed to query auth provider: %v", err)) - } - - oauthProvider.ClientSecret = "" - return apiContext.Write(authProviderResponse{ - AuthProvider: *oauthProvider, - RedirectURL: oauthProvider.RedirectURL(s.baseURL), - }) -} - -func (s *Server) deleteAuthProvider(apiContext api.Context) error { - logger := kcontext.GetLogger(apiContext.Context()) - slug := apiContext.PathValue("slug") - if slug == "" { - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, http.StatusBadRequest, errors.New("slug path parameter is required")) - return nil - } - - var count int64 - if err := s.db.WithContext(apiContext.Context()).Transaction(func(tx *gorm.DB) error { - if err := tx.Model(new(types.AuthProvider)).Count(&count).Error; err != nil { - return err - } - if count == 1 { - return fmt.Errorf("cannot delete last auth provider") - } - - authProvider := new(types.AuthProvider) - if err := tx.Where("slug = ?", slug).First(authProvider).Error; err != nil { - return err - } - - if err := tx.Where("auth_provider_id = ?", authProvider.ID).Delete(new(types.Identity)).Error; err != nil { - return err - } - - if err := tx.Where("auth_provider_id = ?", authProvider.ID).Delete(new(types.AuthToken)).Error; err != nil { - return err - } - - return tx.Unscoped().Where("slug = ?", slug).Delete(new(types.AuthProvider)).Error - }); err != nil { - if count == 1 { - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, http.StatusBadRequest, fmt.Errorf("cannot delete last auth provider")) - return nil - } - - status := http.StatusInternalServerError - if errors.Is(err, gorm.ErrRecordNotFound) { - status = http.StatusNotFound - } - - logger.DebugContext(apiContext.Context(), "failed to delete auth provider by slug", "slug", slug, "err", err) - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, status, fmt.Errorf("failed to delete auth providers: %v", err)) - return nil - } - - writeResponse(apiContext.Context(), logger, apiContext.ResponseWriter, map[string]any{"deleted": true}) - return nil -} - -func (s *Server) disableAuthProvider(apiContext api.Context) error { - logger := kcontext.GetLogger(apiContext.Context()) - slug := apiContext.PathValue("slug") - if slug == "" { - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, http.StatusBadRequest, errors.New("slug path parameter is required")) - return nil - } - - var count int64 - if err := s.db.WithContext(apiContext.Context()).Transaction(func(tx *gorm.DB) error { - if err := tx.Model(new(types.AuthProvider)).Where("disabled IS NULL OR disabled = false").Count(&count).Error; err != nil { - return err - } - if count == 1 { - return fmt.Errorf("cannot disable last auth provider") - } - - return tx.Model(new(types.AuthProvider)).Where("slug = ?", slug).Update("disabled", true).Error - }); err != nil { - if count == 1 { - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, http.StatusBadRequest, fmt.Errorf("cannot disable last auth provider")) - return nil - } - - status := http.StatusInternalServerError - if errors.Is(err, gorm.ErrRecordNotFound) { - status = http.StatusNotFound - } - - logger.DebugContext(apiContext.Context(), "failed to disable auth provider by slug", "slug", slug, "err", err) - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, status, fmt.Errorf("failed to disable auth providers: %v", err)) - return nil - } - - writeResponse(apiContext.Context(), logger, apiContext.ResponseWriter, map[string]any{"disabled": true}) - return nil -} - -func (s *Server) enableAuthProvider(apiContext api.Context) error { - logger := kcontext.GetLogger(apiContext.Context()) - slug := apiContext.PathValue("slug") - if slug == "" { - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, http.StatusBadRequest, errors.New("slug path parameter is required")) - return nil - } - - if err := s.db.WithContext(apiContext.Context()).Model(new(types.AuthProvider)).Where("slug = ?", slug).Update("disabled", false).Error; err != nil { - status := http.StatusInternalServerError - if errors.Is(err, gorm.ErrRecordNotFound) { - status = http.StatusNotFound - } - - logger.DebugContext(apiContext.Context(), "failed to enable auth provider by slug", "slug", slug, "err", err) - writeError(apiContext.Context(), logger, apiContext.ResponseWriter, status, fmt.Errorf("failed to enable auth providers: %v", err)) - return nil - } - - writeResponse(apiContext.Context(), logger, apiContext.ResponseWriter, map[string]any{"enabled": true}) - return nil -} diff --git a/pkg/gateway/server/dispatcher/dispatcher.go b/pkg/gateway/server/dispatcher/dispatcher.go index d5c6cc592..879a15f6c 100644 --- a/pkg/gateway/server/dispatcher/dispatcher.go +++ b/pkg/gateway/server/dispatcher/dispatcher.go @@ -21,6 +21,7 @@ import ( "github.com/obot-platform/obot/pkg/system" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" kclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -28,8 +29,10 @@ type Dispatcher struct { invoker *invoke.Invoker gptscript *gptscript.GPTScript client kclient.Client - lock *sync.RWMutex - urls map[string]*url.URL + modelLock *sync.RWMutex + modelUrls map[string]*url.URL + authLock *sync.RWMutex + authUrls map[string]*url.URL openAICred string } @@ -38,17 +41,49 @@ func New(invoker *invoke.Invoker, c kclient.Client, gClient *gptscript.GPTScript invoker: invoker, gptscript: gClient, client: c, - lock: new(sync.RWMutex), - urls: make(map[string]*url.URL), + modelLock: new(sync.RWMutex), + modelUrls: make(map[string]*url.URL), + authLock: new(sync.RWMutex), + authUrls: make(map[string]*url.URL), } } +func (d *Dispatcher) URLForAuthProvider(ctx context.Context, namespace, authProviderName string) (*url.URL, error) { + key := namespace + "/" + authProviderName + // Check the map with the read lock. + d.authLock.RLock() + u, ok := d.authUrls[key] + d.authLock.RUnlock() + if ok && engine.IsDaemonRunning(u.String()) { + return u, nil + } + + d.authLock.Lock() + defer d.authLock.Unlock() + + // If we didn't find anything with the read lock, check with the write lock. + // It could be that another thread beat us to the write lock and added the auth provider we desire. + u, ok = d.authUrls[key] + if ok && engine.IsDaemonRunning(u.String()) { + return u, nil + } + + // We didn't find the auth provider (or the daemon stopped for some reason), so start it and add it to the map. + u, err := d.startAuthProvider(ctx, namespace, authProviderName) + if err != nil { + return nil, err + } + + d.authUrls[key] = u + return u, nil +} + func (d *Dispatcher) URLForModelProvider(ctx context.Context, namespace, modelProviderName string) (*url.URL, string, error) { key := namespace + "/" + modelProviderName // Check the map with the read lock. - d.lock.RLock() - u, ok := d.urls[key] - d.lock.RUnlock() + d.modelLock.RLock() + u, ok := d.modelUrls[key] + d.modelLock.RUnlock() if ok && (u.Hostname() != "127.0.0.1" || engine.IsDaemonRunning(u.String())) { if u.Host == "api.openai.com" { return u, d.openAICred, nil @@ -56,12 +91,12 @@ func (d *Dispatcher) URLForModelProvider(ctx context.Context, namespace, modelPr return u, "", nil } - d.lock.Lock() - defer d.lock.Unlock() + d.modelLock.Lock() + defer d.modelLock.Unlock() // If we didn't find anything with the read lock, check with the write lock. // It could be that another thread beat us to the write lock and added the model provider we desire. - u, ok = d.urls[key] + u, ok = d.modelUrls[key] if ok && (u.Hostname() != "127.0.0.1" || engine.IsDaemonRunning(u.String())) { if u.Host == "api.openai.com" { return u, d.openAICred, nil @@ -75,7 +110,7 @@ func (d *Dispatcher) URLForModelProvider(ctx context.Context, namespace, modelPr return nil, "", err } - d.urls[key] = u + d.modelUrls[key] = u if u.Host == "api.openai.com" { return u, d.openAICred, nil } @@ -85,15 +120,28 @@ func (d *Dispatcher) URLForModelProvider(ctx context.Context, namespace, modelPr func (d *Dispatcher) StopModelProvider(namespace, modelProviderName string) { key := namespace + "/" + modelProviderName - d.lock.Lock() - defer d.lock.Unlock() + d.modelLock.Lock() + defer d.modelLock.Unlock() - u := d.urls[key] + u := d.modelUrls[key] if u != nil && u.Hostname() == "127.0.0.1" && engine.IsDaemonRunning(u.String()) { engine.StopDaemon(u.String()) } - delete(d.urls, key) + delete(d.modelUrls, key) +} + +func (d *Dispatcher) StopAuthProvider(namespace, authProviderName string) { + key := namespace + "/" + authProviderName + d.authLock.Lock() + defer d.authLock.Unlock() + + u := d.authUrls[key] + if u != nil && u.Hostname() == "127.0.0.1" && engine.IsDaemonRunning(u.String()) { + engine.StopDaemon(u.String()) + } + + delete(d.authUrls, key) } func (d *Dispatcher) TransformRequest(req *http.Request, namespace string) error { @@ -251,3 +299,111 @@ func readBody(r *http.Request) (map[string]any, error) { return m, nil } + +func (d *Dispatcher) startAuthProvider(ctx context.Context, namespace, authProviderName string) (*url.URL, error) { + thread := &v1.Thread{ + ObjectMeta: metav1.ObjectMeta{ + Name: system.ThreadPrefix + authProviderName, + Namespace: namespace, + }, + Spec: v1.ThreadSpec{ + SystemTask: true, + }, + } + + if err := d.client.Get(ctx, kclient.ObjectKey{Namespace: thread.Namespace, Name: thread.Name}, thread); apierrors.IsNotFound(err) { + if err = d.client.Create(ctx, thread); err != nil { + return nil, fmt.Errorf("failed to create thread: %w", err) + } + } else if err != nil { + return nil, fmt.Errorf("failed to get thread: %w", err) + } + + var authProvider v1.ToolReference + if err := d.client.Get(ctx, kclient.ObjectKey{Namespace: namespace, Name: authProviderName}, &authProvider); err != nil || authProvider.Spec.Type != types.ToolReferenceTypeAuthProvider { + return nil, fmt.Errorf("failed to get auth provider: %w", err) + } + + credCtx := []string{string(authProvider.UID)} + if authProvider.Status.Tool == nil { + return nil, fmt.Errorf("auth provider %q has not been resolved", authProviderName) + } + + // Ensure that the auth provider has been configured so that we don't get stuck waiting on a prompt. + if authProvider.Status.Tool.Metadata["envVars"] != "" { + cred, err := d.gptscript.RevealCredential(ctx, credCtx, authProviderName) + if err != nil { + return nil, fmt.Errorf("auth provider is not configured: %w", err) + } + + var missingEnvVars []string + for _, envVar := range strings.Split(authProvider.Status.Tool.Metadata["envVars"], ",") { + if cred.Env[envVar] == "" { + missingEnvVars = append(missingEnvVars, envVar) + } + } + + if len(missingEnvVars) > 0 { + return nil, fmt.Errorf("auth provider is not configured: missing configuration parameters %s", strings.Join(missingEnvVars, ", ")) + } + } + + task, err := d.invoker.SystemTask(ctx, thread, authProviderName, "", invoke.SystemTaskOptions{ + CredentialContextIDs: credCtx, + }) + if err != nil { + return nil, err + } + + result, err := task.Result(ctx) + if err != nil { + return nil, err + } + + return url.Parse(strings.TrimSpace(result.Output)) +} + +func (d *Dispatcher) ListConfiguredAuthProviders(ctx context.Context, namespace string) ([]string, error) { + var authProviders v1.ToolReferenceList + if err := d.client.List(ctx, &authProviders, &kclient.ListOptions{ + Namespace: namespace, + FieldSelector: fields.SelectorFromSet(map[string]string{ + "spec.type": string(types.ToolReferenceTypeAuthProvider), + }), + }); err != nil { + return nil, err + } + + var result []string + for _, authProvider := range authProviders.Items { + if d.isAuthProviderConfigured(ctx, []string{string(authProvider.UID)}, authProvider) { + result = append(result, authProvider.Name) + } + } + + return result, nil +} + +func (d *Dispatcher) isAuthProviderConfigured(ctx context.Context, credCtx []string, toolRef v1.ToolReference) bool { + if toolRef.Status.Tool == nil { + return false + } + + cred, err := d.gptscript.RevealCredential(ctx, credCtx, toolRef.Name) + if err != nil { + return false + } + + var requiredEnvVars []string + if toolRef.Status.Tool.Metadata["envVars"] != "" { + requiredEnvVars = strings.Split(toolRef.Status.Tool.Metadata["envVars"], ",") + } + + for _, envVar := range requiredEnvVars { + if cred.Env[envVar] == "" { + return false + } + } + + return true +} diff --git a/pkg/gateway/server/llmproxy.go b/pkg/gateway/server/llmproxy.go index 53d2ae216..a4a0ed590 100644 --- a/pkg/gateway/server/llmproxy.go +++ b/pkg/gateway/server/llmproxy.go @@ -45,6 +45,6 @@ func (s *Server) llmProxy(req api.Context) error { func (s *Server) newDirector(namespace string, errChan chan<- error) func(req *http.Request) { return func(req *http.Request) { - errChan <- s.modelDispatcher.TransformRequest(req, namespace) + errChan <- s.dispatcher.TransformRequest(req, namespace) } } diff --git a/pkg/gateway/server/oauth.go b/pkg/gateway/server/oauth.go index 6b6ebcc52..f2e03e593 100644 --- a/pkg/gateway/server/oauth.go +++ b/pkg/gateway/server/oauth.go @@ -3,10 +3,10 @@ package server import ( "crypto/rand" "crypto/sha256" - "errors" "fmt" "net/http" "net/url" + "slices" "time" types2 "github.com/obot-platform/obot/apiclient/types" @@ -15,20 +15,26 @@ import ( "gorm.io/gorm" ) +const expirationDur = 7 * 24 * time.Hour + // oauth handles the initial oauth request, redirecting based on the "service" path parameter. func (s *Server) oauth(apiContext api.Context) error { - service := apiContext.PathValue("service") - if service == "" { - return types2.NewErrHttp(http.StatusBadRequest, "no service path parameter provided") + namespace := apiContext.PathValue("namespace") + if namespace == "" { + return types2.NewErrHttp(http.StatusBadRequest, "no namespace path parameter provided") } - oauthProvider := new(types.AuthProvider) - if err := s.db.WithContext(apiContext.Context()).Where("slug = ?", service).Where("disabled IS NULL OR disabled != ?", true).First(oauthProvider).Error; err != nil { - status := http.StatusInternalServerError - if errors.Is(err, gorm.ErrRecordNotFound) { - status = http.StatusNotFound - } - return types2.NewErrHttp(status, fmt.Sprintf("failed to find oauth provider: %v", err)) + name := apiContext.PathValue("name") + if name == "" { + return types2.NewErrHttp(http.StatusBadRequest, "no name path parameter provided") + } + + // Check to make sure this auth provider exists. + list, err := s.dispatcher.ListConfiguredAuthProviders(apiContext.Context(), namespace) + if err != nil { + return fmt.Errorf("could not list configured auth providers: %w", err) + } else if !slices.Contains(list, name) { + return types2.NewErrHttp(http.StatusNotFound, "auth provider not found") } state, err := s.createState(apiContext.Context(), apiContext.PathValue("id")) @@ -38,24 +44,28 @@ func (s *Server) oauth(apiContext api.Context) error { // Redirect the user through the oauth proxy flow so that everything is consistent. // The rd query parameter is used to redirect the user back through this oauth flow so a token can be generated. - http.Redirect(apiContext.ResponseWriter, apiContext.Request, fmt.Sprintf("%s/oauth2/start?rd=%s", s.baseURL, url.QueryEscape(fmt.Sprintf("/api/oauth/redirect/%s?state=%s", oauthProvider.Slug, state))), http.StatusFound) + http.Redirect(apiContext.ResponseWriter, apiContext.Request, fmt.Sprintf("%s/oauth2/start?rd=%s", s.baseURL, url.QueryEscape(fmt.Sprintf("/api/oauth/redirect/%s/%s?state=%s", namespace, name, state))), http.StatusFound) return nil } // redirect handles the OAuth redirect for each service. func (s *Server) redirect(apiContext api.Context) error { - service := apiContext.PathValue("service") - if service == "" { - return types2.NewErrHttp(http.StatusBadRequest, "no service path parameter provided") + namespace := apiContext.PathValue("namespace") + if namespace == "" { + return types2.NewErrHttp(http.StatusBadRequest, "no namespace path parameter provided") } - oauthProvider := new(types.AuthProvider) - if err := s.db.WithContext(apiContext.Context()).Where("slug = ?", service).Where("disabled IS NULL OR disabled != ?", true).First(oauthProvider).Error; err != nil { - status := http.StatusInternalServerError - if errors.Is(err, gorm.ErrRecordNotFound) { - status = http.StatusNotFound - } - return types2.NewErrHttp(status, fmt.Sprintf("failed to find oauth provider: %v", err)) + name := apiContext.PathValue("name") + if name == "" { + return types2.NewErrHttp(http.StatusBadRequest, "no name path parameter provided") + } + + // Check to make sure this auth provider exists. + list, err := s.dispatcher.ListConfiguredAuthProviders(apiContext.Context(), namespace) + if err != nil { + return fmt.Errorf("could not list configured auth providers: %w", err) + } else if !slices.Contains(list, name) { + return types2.NewErrHttp(http.StatusNotFound, "auth provider not found") } tr, err := s.verifyState(apiContext.Context(), apiContext.FormValue("state")) @@ -71,14 +81,15 @@ func (s *Server) redirect(apiContext api.Context) error { id := randBytes[:tokenIDLength] token := randBytes[tokenIDLength:] tr.Token = publicToken(id, token[:]) - tr.ExpiresAt = time.Now().Add(oauthProvider.ExpirationDur) + tr.ExpiresAt = time.Now().Add(expirationDur) // TODO: make this configurable? tkn := &types.AuthToken{ ID: fmt.Sprintf("%x", id), // Hash the token again for long-term storage - HashedToken: hashToken(fmt.Sprintf("%x", token)), - ExpiresAt: tr.ExpiresAt, - AuthProviderID: oauthProvider.ID, + HashedToken: hashToken(fmt.Sprintf("%x", token)), + ExpiresAt: tr.ExpiresAt, + AuthProviderNamespace: namespace, + AuthProviderName: name, } if err = s.db.WithContext(apiContext.Context()).Transaction(func(tx *gorm.DB) error { if err := tx.Updates(tr).Error; err != nil { diff --git a/pkg/gateway/server/response.go b/pkg/gateway/server/response.go index c64fadec4..3bf66663d 100644 --- a/pkg/gateway/server/response.go +++ b/pkg/gateway/server/response.go @@ -1,46 +1,9 @@ package server import ( - "context" - "encoding/json" "fmt" - "log/slog" - "net/http" ) -func writeResponse(ctx context.Context, logger *slog.Logger, w http.ResponseWriter, v any) { - b, err := json.Marshal(v) - if err != nil { - writeError(ctx, logger, w, http.StatusInternalServerError, fmt.Errorf("failed to marshal response: %w", err)) - return - } - - _, _ = w.Write(b) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } -} - -func writeError(ctx context.Context, logger *slog.Logger, w http.ResponseWriter, code int, err error) { - logger.DebugContext(ctx, "Writing error response", "code", code, "error", err) - - w.WriteHeader(code) - resp := map[string]any{ - "error": err.Error(), - } - - b, err := json.Marshal(resp) - if err != nil { - _, _ = w.Write([]byte(fmt.Sprintf(`{"error": "%s"}`, err.Error()))) - return - } - - _, _ = w.Write(b) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } -} - func (s *Server) authCompleteURL() string { return fmt.Sprintf("%s/login_complete", s.uiURL) } diff --git a/pkg/gateway/server/router.go b/pkg/gateway/server/router.go index b8aad9841..84e779aaa 100644 --- a/pkg/gateway/server/router.go +++ b/pkg/gateway/server/router.go @@ -18,22 +18,14 @@ func (s *Server) AddRoutes(mux *server.Server) { mux.HandleFunc("POST /api/token-request", s.tokenRequest) mux.HandleFunc("GET /api/token-request/{id}", s.checkForToken) - mux.HandleFunc("GET /api/token-request/{id}/{service}", s.redirectForTokenRequest) + mux.HandleFunc("GET /api/token-request/{id}/{namespace}/{name}", s.redirectForTokenRequest) mux.HandleFunc("GET /api/tokens", wrap(s.getTokens)) mux.HandleFunc("DELETE /api/tokens/{id}", wrap(s.deleteToken)) mux.HandleFunc("POST /api/tokens", wrap(s.newToken)) - mux.HandleFunc("POST /api/auth-providers", wrap(s.createAuthProvider)) - mux.HandleFunc("PATCH /api/auth-providers/{slug}", wrap(s.updateAuthProvider)) - mux.HandleFunc("DELETE /api/auth-providers/{slug}", wrap(s.deleteAuthProvider)) - mux.HandleFunc("GET /api/auth-providers", s.getAuthProviders) - mux.HandleFunc("GET /api/auth-providers/{slug}", s.getAuthProvider) - mux.HandleFunc("POST /api/auth-providers/{slug}/disable", wrap(s.disableAuthProvider)) - mux.HandleFunc("POST /api/auth-providers/{slug}/enable", wrap(s.enableAuthProvider)) - - mux.HandleFunc("GET /api/oauth/start/{id}/{service}", wrap(s.oauth)) - mux.HandleFunc("/api/oauth/redirect/{service}", wrap(s.redirect)) + mux.HandleFunc("GET /api/oauth/start/{id}/{namespace}/{name}", wrap(s.oauth)) + mux.HandleFunc("/api/oauth/redirect/{namespace}/{name}", wrap(s.redirect)) // CRUD routes for OAuth Apps (integrations with other services such as Microsoft 365) mux.HandleFunc("GET /api/oauth-apps", wrap(s.listOAuthApps)) diff --git a/pkg/gateway/server/server.go b/pkg/gateway/server/server.go index 8f799f48a..19df8debb 100644 --- a/pkg/gateway/server/server.go +++ b/pkg/gateway/server/server.go @@ -2,18 +2,13 @@ package server import ( "context" - "errors" "fmt" "net/http" - "strings" - "time" "github.com/obot-platform/obot/pkg/gateway/client" "github.com/obot-platform/obot/pkg/gateway/db" "github.com/obot-platform/obot/pkg/gateway/server/dispatcher" - "github.com/obot-platform/obot/pkg/gateway/types" "github.com/obot-platform/obot/pkg/jwt" - "gorm.io/gorm" ) type Options struct { @@ -23,13 +18,13 @@ type Options struct { } type Server struct { - adminEmails map[string]struct{} - db *db.DB - baseURL, uiURL string - httpClient *http.Client - client *client.Client - tokenService *jwt.TokenService - modelDispatcher *dispatcher.Dispatcher + adminEmails map[string]struct{} + db *db.DB + baseURL, uiURL string + httpClient *http.Client + client *client.Client + tokenService *jwt.TokenService + dispatcher *dispatcher.Dispatcher } func New(ctx context.Context, db *db.DB, tokenService *jwt.TokenService, modelProviderDispatcher *dispatcher.Dispatcher, adminEmails []string, opts Options) (*Server, error) { @@ -43,14 +38,14 @@ func New(ctx context.Context, db *db.DB, tokenService *jwt.TokenService, modelPr } s := &Server{ - adminEmails: adminEmailsSet, - db: db, - baseURL: opts.Hostname, - uiURL: opts.UIHostname, - httpClient: &http.Client{}, - client: client.New(db, adminEmails), - tokenService: tokenService, - modelDispatcher: modelProviderDispatcher, + adminEmails: adminEmailsSet, + db: db, + baseURL: opts.Hostname, + uiURL: opts.UIHostname, + httpClient: &http.Client{}, + client: client.New(db, adminEmails), + tokenService: tokenService, + dispatcher: modelProviderDispatcher, } go s.autoCleanupTokens(ctx) @@ -58,44 +53,3 @@ func New(ctx context.Context, db *db.DB, tokenService *jwt.TokenService, modelPr return s, nil } - -func (s *Server) UpsertAuthProvider(ctx context.Context, configType, clientID, clientSecret string) (uint, error) { - if clientID == "" || clientSecret == "" { - return 0, nil - } - - authProvider := &types.AuthProvider{ - Type: configType, - ClientID: clientID, - ClientSecret: clientSecret, - OAuthURL: types.OAuthURLByType(configType), - JWKSURL: types.JWKSURLByType(configType), - TokenURL: types.TokenURLByType(configType), - ServiceName: strings.ToTitle(string(configType[0])) + configType[1:], - Scopes: types.ScopesByType(configType), - UsernameClaim: types.UsernameClaimByType(configType), - EmailClaim: types.EmailClaimByType(configType), - Slug: strings.ToLower(configType), - Expiration: "7d", - ExpirationDur: 7 * 24 * time.Hour, - } - - if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - existing := new(types.AuthProvider) - if err := tx.WithContext(ctx).Where("slug = ?", authProvider.Slug).First(existing).Error; err != nil { - if !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - } - if existing.ID == 0 { - return tx.WithContext(ctx).Create(authProvider).Error - } - - authProvider.Model = existing.Model - return tx.WithContext(ctx).Model(authProvider).Updates(authProvider).Error - }); err != nil { - return 0, err - } - - return authProvider.ID, nil -} diff --git a/pkg/gateway/server/token.go b/pkg/gateway/server/token.go index 8a7ac1d4d..50c2c17a3 100644 --- a/pkg/gateway/server/token.go +++ b/pkg/gateway/server/token.go @@ -8,6 +8,7 @@ import ( "fmt" "log/slog" "net/http" + "slices" "strings" "time" @@ -29,6 +30,7 @@ const ( type tokenRequestRequest struct { ID string `json:"id"` ServiceName string `json:"serviceName"` + ServiceNamespace string `json:"serviceNamespace"` CompletionRedirectURL string `json:"completionRedirectURL"` } @@ -69,9 +71,9 @@ type createTokenRequest struct { } func (s *Server) newToken(apiContext api.Context) error { - authProviderID := apiContext.AuthProviderID() + namespace, name := apiContext.AuthProviderNameAndNamespace() userID := apiContext.UserID() - if authProviderID <= 0 || userID <= 0 { + if namespace == "" || name == "" || userID <= 0 { return types2.NewErrHttp(http.StatusForbidden, "forbidden") } @@ -100,25 +102,20 @@ func (s *Server) newToken(apiContext api.Context) error { token := randBytes[tokenIDLength:] tkn := &types.AuthToken{ - ID: fmt.Sprintf("%x", id), - UserID: userID, - HashedToken: hashToken(fmt.Sprintf("%x", token)), - ExpiresAt: time.Now().Add(customExpiration), + ID: fmt.Sprintf("%x", id), + UserID: userID, + HashedToken: hashToken(fmt.Sprintf("%x", token)), + ExpiresAt: time.Now().Add(customExpiration), + AuthProviderNamespace: namespace, + AuthProviderName: name, } - if err := s.db.WithContext(apiContext.Context()).Transaction(func(tx *gorm.DB) error { - provider := new(types.AuthProvider) - if err := tx.Where("id = ?", authProviderID).First(provider).Error; err != nil { - return fmt.Errorf("error refreshing token: %v", err) - } - - if customExpiration == 0 { - tkn.ExpiresAt = time.Now().Add(provider.ExpirationDur) - } - tkn.AuthProviderID = provider.ID - return tx.Create(tkn).Error - }); err != nil { - return types2.NewErrHttp(http.StatusInternalServerError, fmt.Sprintf("error refreshing token: %v", err)) + // Make sure the auth provider exists. + list, err := s.dispatcher.ListConfiguredAuthProviders(apiContext.Context(), namespace) + if err != nil { + return types2.NewErrHttp(http.StatusInternalServerError, fmt.Sprintf("error listing configured auth providers: %v", err)) + } else if !slices.Contains(list, name) { + return types2.NewErrHttp(http.StatusNotFound, "auth provider not found") } return apiContext.Write(refreshTokenResponse{ @@ -133,22 +130,23 @@ func (s *Server) tokenRequest(apiContext api.Context) error { return types2.NewErrHttp(http.StatusBadRequest, fmt.Sprintf("invalid token request body: %v", err)) } + if reqObj.ServiceName != "" { + list, err := s.dispatcher.ListConfiguredAuthProviders(apiContext.Context(), reqObj.ServiceNamespace) + if err != nil { + return types2.NewErrHttp(http.StatusInternalServerError, err.Error()) + } + + if !slices.Contains(list, reqObj.ServiceName) { + return types2.NewErrHttp(http.StatusBadRequest, fmt.Sprintf("service %q not found", reqObj.ServiceName)) + } + } + tokenReq := &types.TokenRequest{ ID: reqObj.ID, CompletionRedirectURL: reqObj.CompletionRedirectURL, } - oauthProvider := new(types.AuthProvider) - if err := s.db.WithContext(apiContext.Context()).Transaction(func(tx *gorm.DB) error { - if reqObj.ServiceName != "" { - // Ensure the OAuth provider exists, if one was provided. - if err := tx.Where("service_name = ?", reqObj.ServiceName).Where("disabled IS NULL OR disabled != ?", true).First(oauthProvider).Error; err != nil { - return fmt.Errorf("failed to find oauth provider %q: %v", reqObj.ServiceName, err) - } - } - - return tx.Create(tokenReq).Error - }); err != nil { + if err := s.db.WithContext(apiContext.Context()).Create(tokenReq).Error; err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return types2.NewErrHttp(http.StatusConflict, "token request already exists") } @@ -156,32 +154,36 @@ func (s *Server) tokenRequest(apiContext api.Context) error { } if reqObj.ServiceName != "" { - return apiContext.Write(map[string]any{"token-path": fmt.Sprintf("%s/api/oauth/start/%s/%s", s.baseURL, reqObj.ID, oauthProvider.Slug)}) + return apiContext.Write(map[string]any{"token-path": fmt.Sprintf("%s/api/oauth/start/%s/%s/%s", s.baseURL, reqObj.ID, reqObj.ServiceNamespace, reqObj.ServiceName)}) } return apiContext.Write(map[string]any{"token-path": fmt.Sprintf("%s/login?id=%s", s.uiURL, reqObj.ID)}) } func (s *Server) redirectForTokenRequest(apiContext api.Context) error { id := apiContext.PathValue("id") - service := apiContext.PathValue("service") + namespace := apiContext.PathValue("namespace") + name := apiContext.PathValue("name") - oauthProvider := new(types.AuthProvider) - tokenReq := new(types.TokenRequest) - if err := s.db.WithContext(apiContext.Context()).Transaction(func(tx *gorm.DB) error { - // Ensure the OAuth provider exists, if one was provided. - if err := tx.Where("slug = ?", service).Where("disabled IS NULL OR disabled != ?", true).First(oauthProvider).Error; err != nil { - return fmt.Errorf("failed to find oauth provider %q: %v", service, err) + if namespace != "" && name != "" { + list, err := s.dispatcher.ListConfiguredAuthProviders(apiContext.Context(), namespace) + if err != nil { + return types2.NewErrHttp(http.StatusInternalServerError, err.Error()) } - return tx.Where("id = ?", id).First(tokenReq).Error - }); err != nil { + if !slices.Contains(list, name) { + return types2.NewErrHttp(http.StatusBadRequest, fmt.Sprintf("service %q not found", name)) + } + } + + tokenReq := new(types.TokenRequest) + if err := s.db.WithContext(apiContext.Context()).Where("id = ?", id).First(tokenReq).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return types2.NewErrNotFound("token or service not found") + return types2.NewErrNotFound("token not found") } return types2.NewErrHttp(http.StatusInternalServerError, err.Error()) } - return apiContext.Write(map[string]any{"token-path": fmt.Sprintf("%s/api/oauth/start/%s/%s", s.baseURL, tokenReq.ID, oauthProvider.Slug)}) + return apiContext.Write(map[string]any{"token-path": fmt.Sprintf("%s/api/oauth/start/%s/%s/%s", s.baseURL, tokenReq.ID, namespace, name)}) } func (s *Server) checkForToken(apiContext api.Context) error { diff --git a/pkg/gateway/server/tokenreview.go b/pkg/gateway/server/tokenreview.go index e28d336f3..a33482e43 100644 --- a/pkg/gateway/server/tokenreview.go +++ b/pkg/gateway/server/tokenreview.go @@ -20,14 +20,15 @@ func (s *Server) AuthenticateRequest(req *http.Request) (*authenticator.Response id, token, _ := strings.Cut(bearer, ":") u := new(types.User) - var authProviderID string + var namespace, name string if err := s.db.WithContext(req.Context()).Transaction(func(tx *gorm.DB) error { tkn := new(types.AuthToken) if err := tx.Where("id = ? AND hashed_token = ?", id, hashToken(token)).First(tkn).Error; err != nil { return err } - authProviderID = fmt.Sprint(tkn.AuthProviderID) + namespace = fmt.Sprint(tkn.AuthProviderNamespace) + name = fmt.Sprint(tkn.AuthProviderName) return tx.Where("id = ?", tkn.UserID).First(u).Error }); err != nil { return nil, false, err @@ -38,8 +39,9 @@ func (s *Server) AuthenticateRequest(req *http.Request) (*authenticator.Response Name: u.Username, UID: strconv.FormatUint(uint64(u.ID), 10), Extra: map[string][]string{ - "email": {u.Email}, - "auth_provider_id": {authProviderID}, + "email": {u.Email}, + "auth_provider_namespace": {namespace}, + "auth_provider_name": {name}, }, }, }, true, nil diff --git a/pkg/gateway/server/user.go b/pkg/gateway/server/user.go index b14023869..44553fe53 100644 --- a/pkg/gateway/server/user.go +++ b/pkg/gateway/server/user.go @@ -30,7 +30,13 @@ func (s *Server) getCurrentUser(apiContext api.Context) error { return err } - if err = s.client.UpdateProfileIconIfNeeded(apiContext.Context(), user, apiContext.AuthProviderID()); err != nil { + name, namespace := apiContext.AuthProviderNameAndNamespace() + providerURL, err := s.dispatcher.URLForAuthProvider(apiContext.Context(), name, namespace) + if err != nil { + return fmt.Errorf("failed to get auth provider URL: %v", err) + } + + if err = s.client.UpdateProfileIconIfNeeded(apiContext.Context(), user, name, namespace, providerURL.String()); err != nil { pkgLog.Warnf("failed to update profile icon for user %s: %v", user.Username, err) } diff --git a/pkg/gateway/types/identity.go b/pkg/gateway/types/identity.go index 6755e45c1..d6ac32896 100644 --- a/pkg/gateway/types/identity.go +++ b/pkg/gateway/types/identity.go @@ -3,10 +3,11 @@ package types import "time" type Identity struct { - AuthProviderID uint `json:"authProviderID" gorm:"primaryKey;index:idx_user_auth_id"` - ProviderUsername string `json:"providerUsername" gorm:"primaryKey"` - Email string `json:"email"` - UserID uint `json:"userID" gorm:"index:idx_user_auth_id"` - IconURL string `json:"iconURL"` - IconLastChecked time.Time `json:"iconLastChecked"` + AuthProviderName string `json:"authProviderName" gorm:"primaryKey;index:idx_user_auth_id"` + AuthProviderNamespace string `json:"authProviderNamespace" gorm:"primaryKey;index:idx_user_auth_id"` + ProviderUsername string `json:"providerUsername" gorm:"primaryKey"` + Email string `json:"email"` + UserID uint `json:"userID" gorm:"index:idx_user_auth_id"` + IconURL string `json:"iconURL"` + IconLastChecked time.Time `json:"iconLastChecked"` } diff --git a/pkg/gateway/types/oauth_apps.go b/pkg/gateway/types/oauth_apps.go index 90cda837c..d443e05de 100644 --- a/pkg/gateway/types/oauth_apps.go +++ b/pkg/gateway/types/oauth_apps.go @@ -27,6 +27,7 @@ const ( GoogleTokenURL = "https://oauth2.googleapis.com/token" GitHubAuthorizeURL = "https://github.com/login/oauth/authorize" + GitHubTokenURL = "https://github.com/login/oauth/access_token" ) var ( diff --git a/pkg/gateway/types/providers.go b/pkg/gateway/types/providers.go index bfb439996..c959d9e8b 100644 --- a/pkg/gateway/types/providers.go +++ b/pkg/gateway/types/providers.go @@ -6,221 +6,8 @@ import ( "net/url" "strings" "time" - - ktime "github.com/obot-platform/obot/pkg/gateway/time" - "gorm.io/gorm" ) -const ( - GitHubOAuthURL = "https://github.com/login/oauth/authorize" - GitHubTokenURL = "https://github.com/login/oauth/access_token" - - GoogleOAuthURL = "https://accounts.google.com/o/oauth2/auth" - GoogleJWKSURL = "https://www.googleapis.com/oauth2/v3/certs" - - AzureOauthURL = "https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/authorize" - AzureJWKSURL = "https://login.microsoftonline.com/{tenantID}/discovery/v2.0/keys" - - AuthTypeGitHub = "github" - AuthTypeAzureAD = "azuread" - AuthTypeGoogle = "google" - AuthTypeGenericOIDC = "genericOIDC" -) - -var tokenURLByType = map[string]string{ - AuthTypeGitHub: GitHubTokenURL, - AuthTypeGoogle: GoogleTokenURL, -} - -var oauthURLByType = map[string]string{ - AuthTypeGitHub: GitHubOAuthURL, - AuthTypeGoogle: GoogleOAuthURL, - AuthTypeAzureAD: AzureOauthURL, -} - -var jwksURLByType = map[string]string{ - AuthTypeAzureAD: AzureJWKSURL, - AuthTypeGoogle: GoogleJWKSURL, -} - -var defaultScopesByType = map[string]string{ - AuthTypeGitHub: "user:email", - AuthTypeAzureAD: "openid+profile+email", - AuthTypeGoogle: "openid profile email", -} - -var defaultUsernameClaimByType = map[string]string{ - AuthTypeAzureAD: "preferred_username", - AuthTypeGoogle: "name", -} - -var defaultEmailClaimByType = map[string]string{ - AuthTypeAzureAD: "email", - AuthTypeGoogle: "email", -} - -func OAuthURLByType(t string) string { - return oauthURLByType[t] -} - -func JWKSURLByType(t string) string { - return jwksURLByType[t] -} - -func TokenURLByType(t string) string { - return tokenURLByType[t] -} - -func ScopesByType(t string) string { - return defaultScopesByType[t] -} - -func UsernameClaimByType(t string) string { - return defaultUsernameClaimByType[t] -} - -func EmailClaimByType(t string) string { - return defaultEmailClaimByType[t] -} - -type AuthTypeConfig struct { - DisplayName string `json:"displayName"` - Required map[string]string `json:"required"` - Advanced map[string]string `json:"advanced"` -} - -type AuthProvider struct { - // These fields are set for every auth provider - gorm.Model `json:",inline"` - Type string `json:"type"` - ServiceName string `json:"serviceName"` - Slug string `json:"slug" gorm:"unique"` - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - OAuthURL string `json:"oauthURL"` - Scopes string `json:"scopes,omitempty"` - Expiration string `json:"expiration,omitempty"` - ExpirationDur time.Duration `json:"-"` - Disabled bool `json:"disabled"` - // Not needed for OIDC type flows - TokenURL string `json:"tokenURL"` - // These fields are only set for AzureAD - TenantID string `json:"tenantID,omitempty"` - // These fields are only set for OIDC providers, including AzureAD - JWKSURL string `json:"jwksURL,omitempty"` - UsernameClaim string `json:"usernameClaim,omitempty"` - EmailClaim string `json:"emailClaim,omitempty"` -} - -func (ap *AuthProvider) ValidateAndSetDefaults() error { - var ( - errs []error - err error - ) - ap.Type = strings.ToLower(ap.Type) - if ap.Type == "" { - errs = append(errs, fmt.Errorf("auth provider type is required")) - } - if ap.ServiceName == "" { - errs = append(errs, fmt.Errorf("auth provider service name is required")) - } - if ap.ClientID == "" { - errs = append(errs, fmt.Errorf("auth provider client id is required")) - } - if ap.ClientSecret == "" { - errs = append(errs, fmt.Errorf("auth provider client secret is required")) - } - if ap.Type == AuthTypeAzureAD && ap.TenantID == "" { - ap.TenantID = "common" - } - if ap.OAuthURL == "" { - ap.OAuthURL = oauthURLByType[strings.ToLower(ap.Type)] - if ap.OAuthURL == "" { - errs = append(errs, fmt.Errorf("cannot determine OAuth URL for type: %s", ap.Type)) - } else if ap.Type == AuthTypeAzureAD { - ap.OAuthURL = strings.ReplaceAll(ap.OAuthURL, "{tenantID}", ap.TenantID) - } - } - if ap.TokenURL == "" { - ap.TokenURL = tokenURLByType[strings.ToLower(ap.Type)] - if ap.Type == AuthTypeGitHub && ap.TokenURL == "" { - errs = append(errs, fmt.Errorf("cannot determine token URL for type: %s", ap.Type)) - } - } - - if ap.JWKSURL == "" { - ap.JWKSURL = jwksURLByType[strings.ToLower(ap.Type)] - if ap.Type == AuthTypeAzureAD { - ap.JWKSURL = strings.ReplaceAll(ap.JWKSURL, "{tenantID}", ap.TenantID) - } - if (ap.Type == AuthTypeGenericOIDC || ap.Type == AuthTypeAzureAD || ap.Type == AuthTypeGoogle) && ap.JWKSURL == "" { - errs = append(errs, fmt.Errorf("cannot determine JWKS URL for type: %s", ap.Type)) - } - } - - if ap.Slug == "" { - ap.Slug = url.PathEscape(strings.ReplaceAll(strings.ToLower(ap.ServiceName), " ", "-")) - } else { - ap.Slug = url.PathEscape(ap.Slug) - } - - if ap.Expiration == "" { - ap.Expiration = "1d" - } - - if ap.ExpirationDur, err = ktime.ParseDuration(ap.Expiration); err != nil { - errs = append(errs, fmt.Errorf("invalid expiration: %w", err)) - } - - if ap.Scopes == "" { - ap.Scopes = defaultScopesByType[ap.Type] - } - - if ap.UsernameClaim == "" { - ap.UsernameClaim = defaultUsernameClaimByType[ap.Type] - } - if ap.EmailClaim == "" { - ap.EmailClaim = defaultEmailClaimByType[ap.Type] - } - - if ap.Type == AuthTypeGenericOIDC && ap.UsernameClaim == "" { - errs = append(errs, fmt.Errorf("username claim is required for type: %s", ap.Type)) - } - if ap.Type == AuthTypeGenericOIDC && ap.EmailClaim == "" { - errs = append(errs, fmt.Errorf("email claim is required for type: %s", ap.Type)) - } - - return errors.Join(errs...) -} - -func (ap *AuthProvider) RedirectURL(baseURL string) string { - return fmt.Sprintf("%s/api/oauth/redirect/%s", baseURL, ap.Slug) -} - -func (ap *AuthProvider) AuthURL(baseURL string, state, nonce string) string { - switch ap.Type { - case AuthTypeGitHub: - return fmt.Sprintf("%s?client_id=%s&redirect_uri=%s&scope=%s&state=%s", - ap.OAuthURL, - ap.ClientID, - ap.RedirectURL(baseURL), - ap.Scopes, - state, - ) - case AuthTypeAzureAD, AuthTypeGoogle, AuthTypeGenericOIDC: - return fmt.Sprintf("%s?client_id=%s&redirect_uri=%s&scope=%s&state=%s&nonce=%s&response_type=id_token&response_mode=form_post", - ap.OAuthURL, - ap.ClientID, - ap.RedirectURL(baseURL), - ap.Scopes, - state, - nonce, - ) - default: - return "" - } -} - type LLMProvider struct { ID uint `json:"id" gorm:"primaryKey"` CreatedAt time.Time `json:"createdAt"` diff --git a/pkg/gateway/types/tokens.go b/pkg/gateway/types/tokens.go index 21c3a539b..f238c6b32 100644 --- a/pkg/gateway/types/tokens.go +++ b/pkg/gateway/types/tokens.go @@ -3,12 +3,13 @@ package types import "time" type AuthToken struct { - ID string `json:"id" gorm:"index:idx_id_hashed_token"` - UserID uint `json:"-" gorm:"index"` - AuthProviderID uint `json:"-" gorm:"index"` - HashedToken string `json:"-" gorm:"index:idx_id_hashed_token"` - CreatedAt time.Time `json:"createdAt"` - ExpiresAt time.Time `json:"expiresAt"` + ID string `json:"id" gorm:"index:idx_id_hashed_token"` + UserID uint `json:"-" gorm:"index"` + AuthProviderNamespace string `json:"-" gorm:"index"` + AuthProviderName string `json:"-" gorm:"index"` + HashedToken string `json:"-" gorm:"index:idx_id_hashed_token"` + CreatedAt time.Time `json:"createdAt"` + ExpiresAt time.Time `json:"expiresAt"` } type TokenRequest struct { diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index a5fdcaea4..a1d2210b9 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -2,106 +2,154 @@ package proxy import ( "context" + "encoding/json" "fmt" "net/http" + "net/http/httputil" + "net/url" + "sort" "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/obot/pkg/mvl" + "github.com/obot-platform/obot/logger" + "github.com/obot-platform/obot/pkg/accesstoken" + "github.com/obot-platform/obot/pkg/gateway/server/dispatcher" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" ) -var log = mvl.Package() +var log = logger.Package() -type Config struct { - AuthCookieSecret string `usage:"Secret used to encrypt cookie"` - AuthEmailDomains string `usage:"Email domains allowed for authentication" default:"*"` - AuthAdminEmails []string `usage:"Emails admin users"` - AuthConfigType string `usage:"Type of OAuth configuration" default:"google"` - AuthClientID string `usage:"Client ID for OAuth"` - AuthClientSecret string `usage:"Client secret for OAuth"` +const AuthProviderCookie = "obot-auth-provider" - // Type-specific config - GithubConfig +type Manager struct { + dispatcher *dispatcher.Dispatcher } -type GithubConfig struct { - AuthGithubOrg string `usage:"Restrict logins to members of this organization"` - AuthGithubTeams []string `usage:"Restrict logins to members of any of these teams (slug)"` - AuthGithubRepo string `usage:"Restrict logins to collaborators of this repository formatted as org/repo"` - AuthGithubToken string `usage:"The token to use when verifying repository collaborators (must have push access to the repository)"` - AuthGithubAllowUsers []string `usage:"Users allowed to login even if they don't belong to the organization or team(s)"` +func NewProxyManager(dispatcher *dispatcher.Dispatcher) *Manager { + return &Manager{ + dispatcher: dispatcher, + } } -type Proxy struct { - proxy *oauth2proxy.OAuthProxy - authProviderID string -} +func (pm *Manager) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { + c, err := req.Cookie(AuthProviderCookie) + if err != nil { + return nil, false, nil + } -func New(serverURL string, authProviderID uint, cfg Config) (*Proxy, error) { - legacyOpts := options.NewLegacyOptions() - legacyOpts.LegacyProvider.ProviderType = cfg.AuthConfigType - legacyOpts.LegacyProvider.ProviderName = cfg.AuthConfigType - legacyOpts.LegacyProvider.ClientID = cfg.AuthClientID - legacyOpts.LegacyProvider.ClientSecret = cfg.AuthClientSecret - legacyOpts.LegacyProvider.GitHubTeam = strings.Join(cfg.AuthGithubTeams, ",") - legacyOpts.LegacyProvider.GitHubOrg = cfg.AuthGithubOrg - legacyOpts.LegacyProvider.GitHubRepo = cfg.AuthGithubRepo - legacyOpts.LegacyProvider.GitHubToken = cfg.AuthGithubToken - legacyOpts.LegacyProvider.GitHubUsers = cfg.AuthGithubAllowUsers - - oauthProxyOpts, err := legacyOpts.ToOptions() + proxy, err := pm.createProxy(req.Context(), c.Value) if err != nil { - return nil, err + return nil, false, err } - // Don't need to bind to a port - oauthProxyOpts.Server.BindAddress = "" - oauthProxyOpts.MetricsServer.BindAddress = "" - oauthProxyOpts.Cookie.Refresh = time.Hour - oauthProxyOpts.Cookie.Name = "obot_access_token" - oauthProxyOpts.Cookie.Secret = cfg.AuthCookieSecret - oauthProxyOpts.Cookie.Secure = strings.HasPrefix(serverURL, "https://") - oauthProxyOpts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: "default", - URI: "http://localhost:8080/", - Path: "(.*)", - RewriteTarget: "$1", - }, - }, + return proxy.authenticateRequest(req) +} + +func (pm *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var provider string + + if provider = r.URL.Query().Get(AuthProviderCookie); provider != "" { + // Set it as a cookie for the future. + http.SetCookie(w, &http.Cookie{ + Name: AuthProviderCookie, + Value: provider, + Path: "/", + }) + } else if c, err := r.Cookie(AuthProviderCookie); err == nil { + provider = c.Value + } + + // If no provider is set, just use the alphabetically first provider. + if provider == "" { + providers, err := pm.dispatcher.ListConfiguredAuthProviders(r.Context(), "default") + if err != nil { + http.Error(w, fmt.Sprintf("failed to list configured auth providers: %v", err), http.StatusInternalServerError) + return + } + if len(providers) == 0 { + // There aren't any auth providers configured. Return an error, unless the user is signing out, in which case, just redirect. + if r.URL.Path == "/oauth2/sign_out" { + rdParam := r.URL.Query().Get("rd") + if rdParam == "" { + rdParam = "/" + } + + http.Redirect(w, r, rdParam, http.StatusFound) + return + } + + http.Error(w, "no auth providers configured", http.StatusInternalServerError) + return + } + sort.Slice(providers, func(i, j int) bool { + return providers[i] < providers[j] + }) + provider = "default/" + providers[0] } - oauthProxyOpts.RawRedirectURL = serverURL + "/oauth2/callback" - oauthProxyOpts.ReverseProxy = true - if cfg.AuthEmailDomains != "" { - oauthProxyOpts.EmailDomains = strings.Split(cfg.AuthEmailDomains, ",") + log.Infof("forwarding request for %s to provider %s", r.URL.Path, provider) + + // If signing out, delete the auth provider cookie. + if r.URL.Path == "/oauth2/sign_out" { + http.SetCookie(w, &http.Cookie{ + Name: AuthProviderCookie, + Value: "", + Path: "/", + MaxAge: -1, + }) } - if err = validation.Validate(oauthProxyOpts); err != nil { - log.Fatalf("%s", err) + proxy, err := pm.createProxy(r.Context(), provider) + if err != nil { + http.Error(w, fmt.Sprintf("failed to create proxy: %v", err), http.StatusInternalServerError) + return + } + + proxy.serveHTTP(w, r) +} + +func (pm *Manager) createProxy(ctx context.Context, provider string) (*Proxy, error) { + parts := strings.Split(provider, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid provider: %s", provider) } - oauthProxy, err := oauth2proxy.NewOAuthProxy(oauthProxyOpts, oauth2proxy.NewValidator(oauthProxyOpts.EmailDomains, oauthProxyOpts.AuthenticatedEmailsFile)) + providerURL, err := pm.dispatcher.URLForAuthProvider(ctx, parts[0], parts[1]) if err != nil { - return nil, fmt.Errorf("failed to create oauth2 proxy: %w", err) + return nil, err + } + + return newProxy(parts[0], parts[1], providerURL.String()) +} + +type Proxy struct { + proxy *httputil.ReverseProxy + url, name, namespace string +} + +func newProxy(providerName, providerNamespace, providerURL string) (*Proxy, error) { + u, err := url.Parse(providerURL) + if err != nil { + return nil, fmt.Errorf("failed to parse provider URL: %w", err) } return &Proxy{ - proxy: oauthProxy, - authProviderID: fmt.Sprint(authProviderID), + proxy: httputil.NewSingleHostReverseProxy(u), + url: providerURL, + name: providerName, + namespace: providerNamespace, }, nil } -func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if p == nil { - // If the proxy server is not setup, and we are getting here, then a request has come in for /oauth2/... - // Since these paths are not setup when auth is disabled, then return a not found error. +func (p *Proxy) serveHTTP(w http.ResponseWriter, r *http.Request) { + // Make sure the path is something that we expect. + switch r.URL.Path { + case "/oauth2/start": + case "/oauth2/redirect": + case "/oauth2/sign_out": + case "/oauth2/callback": + default: http.Error(w, "not found", http.StatusNotFound) return } @@ -109,44 +157,72 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.proxy.ServeHTTP(w, r) } -func (p *Proxy) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { - state, err := p.proxy.LoadCookiedSession(req) - if err != nil || state == nil || state.IsExpired() { +type SerializableRequest struct { + Method string `json:"method"` + URL string `json:"url"` + Header map[string][]string `json:"header"` +} + +type SerializableState struct { + ExpiresOn *time.Time `json:"expiresOn"` + AccessToken string `json:"accessToken"` + PreferredUsername string `json:"preferredUsername"` + User string `json:"user"` + Email string `json:"email"` +} + +func (p *Proxy) authenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { + sr := SerializableRequest{ + Method: req.Method, + URL: req.URL.String(), + Header: make(map[string][]string), + } + for k, v := range req.Header { + sr.Header[k] = v + } + + srJSON, err := json.Marshal(sr) + if err != nil { return nil, false, err } - userName := state.PreferredUsername + stateRequest, err := http.NewRequest(http.MethodPost, p.url+"/obot-get-state", strings.NewReader(string(srJSON))) + if err != nil { + return nil, false, err + } + + stateResponse, err := http.DefaultClient.Do(stateRequest) + if err != nil { + return nil, false, err + } + + var ss SerializableState + if err := json.NewDecoder(stateResponse.Body).Decode(&ss); err != nil { + return nil, false, err + } + + userName := ss.PreferredUsername if userName == "" { - userName = state.User + userName = ss.User if userName == "" { - userName = state.Email + userName = ss.Email } } if req.URL.Path == "/api/me" { // Put the access token on the context so that the profile icon can be fetched. - *req = *req.WithContext(contextWithAccessToken(req.Context(), state.AccessToken)) + *req = *req.WithContext(accesstoken.ContextWithAccessToken(req.Context(), ss.AccessToken)) } return &authenticator.Response{ User: &user.DefaultInfo{ - UID: state.User, + UID: ss.User, Name: userName, Extra: map[string][]string{ - "email": {state.Email}, - "auth_provider_id": {p.authProviderID}, + "email": {ss.Email}, + "auth_provider_name": {p.name}, + "auth_provider_namespace": {p.namespace}, }, }, }, true, nil } - -type accessTokenKey struct{} - -func contextWithAccessToken(ctx context.Context, accessToken string) context.Context { - return context.WithValue(ctx, accessTokenKey{}, accessToken) -} - -func GetAccessToken(ctx context.Context) string { - accessToken, _ := ctx.Value(accessTokenKey{}).(string) - return accessToken -} diff --git a/pkg/services/config.go b/pkg/services/config.go index 942a1db68..c5d878cc5 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -2,7 +2,6 @@ package services import ( "context" - "fmt" "log/slog" "os" "path/filepath" @@ -21,6 +20,7 @@ import ( "github.com/obot-platform/obot/pkg/api/authn" "github.com/obot-platform/obot/pkg/api/authz" "github.com/obot-platform/obot/pkg/api/server" + bootstrap2 "github.com/obot-platform/obot/pkg/bootstrap" "github.com/obot-platform/obot/pkg/credstores" "github.com/obot-platform/obot/pkg/events" "github.com/obot-platform/obot/pkg/gateway/client" @@ -44,28 +44,26 @@ import ( _ "github.com/obot-platform/nah/pkg/logrus" ) -type ( - AuthConfig proxy.Config - GatewayConfig gserver.Options -) +type GatewayConfig gserver.Options type Config struct { - HTTPListenPort int `usage:"HTTP port to listen on" default:"8080" name:"http-listen-port"` - DevMode bool `usage:"Enable development mode" default:"false" name:"dev-mode" env:"OBOT_DEV_MODE"` - DevUIPort int `usage:"The port on localhost running the dev instance of the UI" default:"5173"` - AllowedOrigin string `usage:"Allowed origin for CORS"` - ToolRegistry string `usage:"The tool reference for the tool registry" default:"github.com/obot-platform/tools"` - WorkspaceProviderType string `usage:"The type of workspace provider to use for non-knowledge workspaces" default:"directory" env:"OBOT_WORKSPACE_PROVIDER_TYPE"` - WorkspaceTool string `usage:"The tool reference for the workspace provider" default:"github.com/gptscript-ai/workspace-provider"` - DatasetsTool string `usage:"The tool reference for the dataset provider" default:"github.com/gptscript-ai/datasets"` - HelperModel string `usage:"The model used to generate names and descriptions" default:"gpt-4o-mini"` - AWSKMSKeyARN string `usage:"The ARN of the AWS KMS key to use for encrypting credential storage" env:"OBOT_AWS_KMS_KEY_ARN" name:"aws-kms-key-arn"` - EncryptionConfigFile string `usage:"The path to the encryption configuration file" default:"./encryption.yaml"` - KnowledgeSetIngestionLimit int `usage:"The maximum number of files to ingest into a knowledge set" default:"1000" env:"OBOT_KNOWLEDGESET_INGESTION_LIMIT" name:"knowledge-set-ingestion-limit"` - EmailServerName string `usage:"The name of the email server to display for email receivers (default: ui-hostname value)"` - NoReplyEmailAddress string `usage:"The email to use for no-reply emails from obot"` - - AuthConfig + HTTPListenPort int `usage:"HTTP port to listen on" default:"8080" name:"http-listen-port"` + DevMode bool `usage:"Enable development mode" default:"false" name:"dev-mode" env:"OBOT_DEV_MODE"` + DevUIPort int `usage:"The port on localhost running the dev instance of the UI" default:"5173"` + AllowedOrigin string `usage:"Allowed origin for CORS"` + ToolRegistry string `usage:"The tool reference for the tool registry" default:"github.com/obot-platform/tools"` + WorkspaceProviderType string `usage:"The type of workspace provider to use for non-knowledge workspaces" default:"directory" env:"OBOT_WORKSPACE_PROVIDER_TYPE"` + WorkspaceTool string `usage:"The tool reference for the workspace provider" default:"github.com/gptscript-ai/workspace-provider"` + DatasetsTool string `usage:"The tool reference for the dataset provider" default:"github.com/gptscript-ai/datasets"` + HelperModel string `usage:"The model used to generate names and descriptions" default:"gpt-4o-mini"` + AWSKMSKeyARN string `usage:"The ARN of the AWS KMS key to use for encrypting credential storage" env:"OBOT_AWS_KMS_KEY_ARN" name:"aws-kms-key-arn"` + EncryptionConfigFile string `usage:"The path to the encryption configuration file" default:"./encryption.yaml"` + KnowledgeSetIngestionLimit int `usage:"The maximum number of files to ingest into a knowledge set" default:"1000" env:"OBOT_KNOWLEDGESET_INGESTION_LIMIT" name:"knowledge-set-ingestion-limit"` + EmailServerName string `usage:"The name of the email server to display for email receivers (default: ui-hostname value)"` + NoReplyEmailAddress string `usage:"The email to use for no-reply emails from obot"` + DisableAuthentication bool `usage:"Disable authentication" default:"false" env:"OBOT_DISABLE_AUTHENTICATION"` + AuthAdminEmails []string `usage:"Emails of admin users"` + GatewayConfig services.Config } @@ -85,9 +83,10 @@ type Services struct { APIServer *server.Server AIHelper *aihelper.AIHelper Started chan struct{} - ProxyServer *proxy.Proxy GatewayServer *gserver.Server - ModelProviderDispatcher *dispatcher.Dispatcher + ProxyManager *proxy.Manager + ProviderDispatcher *dispatcher.Dispatcher + Bootstrapper *bootstrap2.Bootstrap KnowledgeSetIngestionLimit int } @@ -215,39 +214,37 @@ func New(ctx context.Context, config Config) (*Services, error) { } var ( - tokenServer = &jwt.TokenService{} - events = events.NewEmitter(storageClient) - gatewayClient = client.New(gatewayDB, config.AuthAdminEmails) - invoker = invoke.NewInvoker(storageClient, c, client.New(gatewayDB, config.AuthAdminEmails), config.NoReplyEmailAddress, config.Hostname, config.HTTPListenPort, tokenServer, events) - modelProviderDispatcher = dispatcher.New(invoker, storageClient, c) - - proxyServer *proxy.Proxy + tokenServer = &jwt.TokenService{} + events = events.NewEmitter(storageClient) + gatewayClient = client.New(gatewayDB, config.AuthAdminEmails) + invoker = invoke.NewInvoker(storageClient, c, client.New(gatewayDB, config.AuthAdminEmails), config.NoReplyEmailAddress, config.Hostname, config.HTTPListenPort, tokenServer, events) + providerDispatcher = dispatcher.New(invoker, storageClient, c) + proxyManager *proxy.Manager ) - gatewayServer, err := gserver.New(ctx, gatewayDB, tokenServer, modelProviderDispatcher, config.AuthAdminEmails, gserver.Options(config.GatewayConfig)) + bootstrapper, err := bootstrap2.New(config.Hostname) if err != nil { return nil, err } - authProviderID, err := gatewayServer.UpsertAuthProvider(ctx, config.AuthConfigType, config.AuthClientID, config.AuthClientSecret) + gatewayServer, err := gserver.New(ctx, gatewayDB, tokenServer, providerDispatcher, config.AuthAdminEmails, gserver.Options(config.GatewayConfig)) if err != nil { return nil, err } var authenticators authenticator.Request = gatewayServer - if config.AuthClientID != "" && config.AuthClientSecret != "" { + if !config.DisableAuthentication { // "Authentication Enabled" flow - proxyServer, err = proxy.New(config.Hostname, authProviderID, proxy.Config(config.AuthConfig)) - if err != nil { - return nil, fmt.Errorf("failed to start auth server: %w", err) - } + proxyManager = proxy.NewProxyManager(providerDispatcher) // Token Auth + OAuth auth - authenticators = union.New(authenticators, proxyServer) + authenticators = union.New(authenticators, proxyManager) // Add gateway user info authenticators = client.NewUserDecorator(authenticators, gatewayClient) // Add token auth authenticators = union.New(authenticators, tokenServer) + // Add bootstrap auth + authenticators = union.New(authenticators, bootstrapper) // Add anonymous user authenticator authenticators = union.New(authenticators, authn.Anonymous{}) } else { @@ -275,15 +272,16 @@ func New(ctx context.Context, config Config) (*Services, error) { Router: r, GPTClient: c, APIServer: server.NewServer(storageClient, c, authn.NewAuthenticator(authenticators), - authz.NewAuthorizer(storageClient), proxyServer, config.Hostname), + authz.NewAuthorizer(storageClient), proxyManager, config.Hostname), TokenServer: tokenServer, Invoker: invoker, AIHelper: aihelper.New(c, config.HelperModel), GatewayServer: gatewayServer, - ProxyServer: proxyServer, KnowledgeSetIngestionLimit: config.KnowledgeSetIngestionLimit, EmailServerName: config.EmailServerName, - ModelProviderDispatcher: modelProviderDispatcher, + ProxyManager: proxyManager, + ProviderDispatcher: providerDispatcher, + Bootstrapper: bootstrapper, }, nil } diff --git a/pkg/storage/apis/otto.otto8.ai/v1/run.go b/pkg/storage/apis/otto.otto8.ai/v1/run.go index c9c1c2f25..dd30d4acd 100644 --- a/pkg/storage/apis/otto.otto8.ai/v1/run.go +++ b/pkg/storage/apis/otto.otto8.ai/v1/run.go @@ -17,6 +17,7 @@ const ( ModelProviderSyncAnnotation = "otto8.ai/model-provider-sync" WorkflowSyncAnnotation = "otto8.ai/workflow-sync" AgentSyncAnnotation = "otto8.ai/agent-sync" + AuthProviderSyncAnnotation = "otto8.ai/auth-provider-sync" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/storage/openapi/generated/openapi_generated.go b/pkg/storage/openapi/generated/openapi_generated.go index 467b7b8ba..62e5f91ec 100644 --- a/pkg/storage/openapi/generated/openapi_generated.go +++ b/pkg/storage/openapi/generated/openapi_generated.go @@ -24,6 +24,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/obot-platform/obot/apiclient/types.AssistantList": schema_obot_platform_obot_apiclient_types_AssistantList(ref), "github.com/obot-platform/obot/apiclient/types.AssistantTool": schema_obot_platform_obot_apiclient_types_AssistantTool(ref), "github.com/obot-platform/obot/apiclient/types.AssistantToolList": schema_obot_platform_obot_apiclient_types_AssistantToolList(ref), + "github.com/obot-platform/obot/apiclient/types.AuthProvider": schema_obot_platform_obot_apiclient_types_AuthProvider(ref), + "github.com/obot-platform/obot/apiclient/types.AuthProviderList": schema_obot_platform_obot_apiclient_types_AuthProviderList(ref), + "github.com/obot-platform/obot/apiclient/types.AuthProviderManifest": schema_obot_platform_obot_apiclient_types_AuthProviderManifest(ref), + "github.com/obot-platform/obot/apiclient/types.AuthProviderStatus": schema_obot_platform_obot_apiclient_types_AuthProviderStatus(ref), "github.com/obot-platform/obot/apiclient/types.Credential": schema_obot_platform_obot_apiclient_types_Credential(ref), "github.com/obot-platform/obot/apiclient/types.CredentialList": schema_obot_platform_obot_apiclient_types_CredentialList(ref), "github.com/obot-platform/obot/apiclient/types.CronJob": schema_obot_platform_obot_apiclient_types_CronJob(ref), @@ -760,6 +764,169 @@ func schema_obot_platform_obot_apiclient_types_AssistantToolList(ref common.Refe } } +func schema_obot_platform_obot_apiclient_types_AuthProvider(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "Metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/obot-platform/obot/apiclient/types.Metadata"), + }, + }, + "AuthProviderManifest": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/obot-platform/obot/apiclient/types.AuthProviderManifest"), + }, + }, + "AuthProviderStatus": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/obot-platform/obot/apiclient/types.AuthProviderStatus"), + }, + }, + }, + Required: []string{"Metadata", "AuthProviderManifest", "AuthProviderStatus"}, + }, + }, + Dependencies: []string{ + "github.com/obot-platform/obot/apiclient/types.AuthProviderManifest", "github.com/obot-platform/obot/apiclient/types.AuthProviderStatus", "github.com/obot-platform/obot/apiclient/types.Metadata"}, + } +} + +func schema_obot_platform_obot_apiclient_types_AuthProviderList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/obot-platform/obot/apiclient/types.AuthProvider"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/obot-platform/obot/apiclient/types.AuthProvider"}, + } +} + +func schema_obot_platform_obot_apiclient_types_AuthProviderManifest(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "toolReference": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "namespace", "toolReference"}, + }, + }, + } +} + +func schema_obot_platform_obot_apiclient_types_AuthProviderStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "icon": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "configured": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "requiredConfigurationParameters": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "missingConfigurationParameters": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "optionalConfigurationParameters": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"configured"}, + }, + }, + } +} + func schema_obot_platform_obot_apiclient_types_Credential(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/ui/admin/app/components/auth-and-model-providers/AuthProviderLists.tsx b/ui/admin/app/components/auth-and-model-providers/AuthProviderLists.tsx new file mode 100644 index 000000000..0d93fa5fe --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/AuthProviderLists.tsx @@ -0,0 +1,72 @@ +import { CircleCheckIcon, CircleSlashIcon } from "lucide-react"; +import { Link } from "react-router"; + +import { AuthProvider } from "~/lib/model/providers"; + +import { ProviderConfigure } from "~/components/auth-and-model-providers/ProviderConfigure"; +import { ProviderIcon } from "~/components/auth-and-model-providers/ProviderIcon"; +import { ProviderMenu } from "~/components/auth-and-model-providers/ProviderMenu"; +import { AuthProviderLinks } from "~/components/auth-and-model-providers/constants"; +import { Badge } from "~/components/ui/badge"; +import { Card, CardContent, CardHeader } from "~/components/ui/card"; + +export function AuthProviderList({ + authProviders, +}: { + authProviders: AuthProvider[]; +}) { + return ( +