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 ( +
+
+ {authProviders.map((authProvider) => ( + + + {authProvider.configured ? ( +
+ +
+ ) : ( +
+ )} + + + + + +
+ {authProvider.name} +
+ + + {authProvider.configured ? ( + + {" "} + Configured + + ) : ( + + + Not Configured + + )} + + +
+ + ))} +
+
+ ); +} diff --git a/ui/admin/app/components/auth-and-model-providers/Bootstrap.tsx b/ui/admin/app/components/auth-and-model-providers/Bootstrap.tsx new file mode 100644 index 000000000..9393945e6 --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/Bootstrap.tsx @@ -0,0 +1,73 @@ +import React, { useState } from "react"; + +import { ApiRoutes } from "~/lib/routers/apiRoutes"; +import { cn } from "~/lib/utils"; + +import { + TypographyH4, + TypographyP, + TypographySmall, +} from "~/components/Typography"; +import { Button } from "~/components/ui/button"; + +interface BootstrapProps { + className?: string; +} + +export function Bootstrap({ className }: BootstrapProps) { + const [token, setToken] = useState(""); + const [error, setError] = useState(""); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const result = await fetch(ApiRoutes.bootstrap.login().url, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (result.status === 401) { + setError("Invalid token"); + return; + } else if (result.status !== 200) { + setError("Failed to login: " + result.statusText); + return; + } + + setError(""); + window.location.href = "/admin/auth-providers"; + } catch (e) { + setError("Failed to login: " + e); + } + }; + + return ( +
+ Enter Bootstrap Token + + The token can be found in the server logs. + + setToken(e.target.value)} + placeholder="token" + className="p-2 border rounded" + required + /> + + {error && ( + + {error} + + )} +
+ ); +} diff --git a/ui/admin/app/components/model-providers/ModelProviderLists.tsx b/ui/admin/app/components/auth-and-model-providers/ModelProviderLists.tsx similarity index 78% rename from ui/admin/app/components/model-providers/ModelProviderLists.tsx rename to ui/admin/app/components/auth-and-model-providers/ModelProviderLists.tsx index 864a2a9f0..a66bd7130 100644 --- a/ui/admin/app/components/model-providers/ModelProviderLists.tsx +++ b/ui/admin/app/components/auth-and-model-providers/ModelProviderLists.tsx @@ -1,16 +1,16 @@ import { CircleCheckIcon, CircleSlashIcon } from "lucide-react"; import { Link } from "react-router"; -import { ModelProvider } from "~/lib/model/modelProviders"; +import { ModelProvider } from "~/lib/model/providers"; -import { ModelProviderConfigure } from "~/components/model-providers/ModelProviderConfigure"; -import { ModelProviderMenu } from "~/components/model-providers/ModelProviderDropdown"; -import { ModelProviderIcon } from "~/components/model-providers/ModelProviderIcon"; -import { ModelProvidersModels } from "~/components/model-providers/ModelProviderModels"; +import { ModelProvidersModels } from "~/components/auth-and-model-providers/ModelProviderModels"; +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 { ModelProviderLinks, RecommendedModelProviders, -} from "~/components/model-providers/constants"; +} from "~/components/auth-and-model-providers/constants"; import { Badge } from "~/components/ui/badge"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; @@ -33,9 +33,7 @@ export function ModelProviderList({ - +
) : (
@@ -43,8 +41,8 @@ export function ModelProviderList({ - @@ -74,9 +72,7 @@ export function ModelProviderList({ )} - + ))} diff --git a/ui/admin/app/components/model-providers/ModelProviderModels.tsx b/ui/admin/app/components/auth-and-model-providers/ModelProviderModels.tsx similarity index 94% rename from ui/admin/app/components/model-providers/ModelProviderModels.tsx rename to ui/admin/app/components/auth-and-model-providers/ModelProviderModels.tsx index ed17bb234..9ba8262bc 100644 --- a/ui/admin/app/components/model-providers/ModelProviderModels.tsx +++ b/ui/admin/app/components/auth-and-model-providers/ModelProviderModels.tsx @@ -3,12 +3,12 @@ import { PictureInPicture2Icon } from "lucide-react"; import { useMemo } from "react"; import useSWR from "swr"; -import { ModelProvider } from "~/lib/model/modelProviders"; import { Model, ModelUsage, getModelUsageLabel } from "~/lib/model/models"; +import { ModelProvider } from "~/lib/model/providers"; import { ModelApiService } from "~/lib/service/api/modelApiService"; +import { ProviderIcon } from "~/components/auth-and-model-providers/ProviderIcon"; import { DataTable } from "~/components/composed/DataTable"; -import { ModelProviderIcon } from "~/components/model-providers/ModelProviderIcon"; import { UpdateModelActive } from "~/components/model/UpdateModelActive"; import { UpdateModelUsage } from "~/components/model/UpdateModelUsage"; import { Button } from "~/components/ui/button"; @@ -84,7 +84,7 @@ export function ModelProvidersModels({ modelProvider }: ModelsConfigureProps) { - {" "} + {" "} {modelProvider.name} Models diff --git a/ui/admin/app/components/model-providers/ModelProviderTooltip.tsx b/ui/admin/app/components/auth-and-model-providers/ModelProviderTooltip.tsx similarity index 100% rename from ui/admin/app/components/model-providers/ModelProviderTooltip.tsx rename to ui/admin/app/components/auth-and-model-providers/ModelProviderTooltip.tsx diff --git a/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderConfigure.tsx similarity index 60% rename from ui/admin/app/components/model-providers/ModelProviderConfigure.tsx rename to ui/admin/app/components/auth-and-model-providers/ProviderConfigure.tsx index 992e1de2a..ac2c5aa34 100644 --- a/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx +++ b/ui/admin/app/components/auth-and-model-providers/ProviderConfigure.tsx @@ -1,14 +1,15 @@ import { useEffect, useState } from "react"; import useSWR, { mutate } from "swr"; -import { ModelProvider } from "~/lib/model/modelProviders"; +import { AuthProvider, ModelProvider } from "~/lib/model/providers"; import { NotFoundError } from "~/lib/service/api/apiErrors"; +import { AuthProviderApiService } from "~/lib/service/api/authProviderApiService"; import { ModelApiService } from "~/lib/service/api/modelApiService"; import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService"; -import { ModelProviderForm } from "~/components/model-providers/ModelProviderForm"; -import { ModelProviderIcon } from "~/components/model-providers/ModelProviderIcon"; -import { CommonModelProviderIds } from "~/components/model-providers/constants"; +import { ProviderForm } from "~/components/auth-and-model-providers/ProviderForm"; +import { ProviderIcon } from "~/components/auth-and-model-providers/ProviderIcon"; +import { CommonModelProviderIds } from "~/components/auth-and-model-providers/constants"; import { DefaultModelAliasForm } from "~/components/model/DefaultModelAliasForm"; import { LoadingSpinner } from "~/components/ui/LoadingSpinner"; import { Button } from "~/components/ui/button"; @@ -22,25 +23,27 @@ import { } from "~/components/ui/dialog"; import { Link } from "~/components/ui/link"; -type ModelProviderConfigureProps = { - modelProvider: ModelProvider; +type ProviderConfigureProps = { + provider: ModelProvider | AuthProvider; }; -export function ModelProviderConfigure({ - modelProvider, -}: ModelProviderConfigureProps) { +// TODO: keep refactoring this function to support both types of providers + +export function ProviderConfigure({ provider }: ProviderConfigureProps) { const [dialogIsOpen, setDialogIsOpen] = useState(false); const [showDefaultModelAliasForm, setShowDefaultModelAliasForm] = useState(false); - const [loadingModelProviderId, setLoadingModelProviderId] = useState(""); + const [loadingProviderId, setLoadingProviderId] = useState(""); const getLoadingModelProviderModels = useSWR( - ModelProviderApiService.getModelProviderById.key( - loadingModelProviderId - ), - ({ modelProviderId }) => - ModelProviderApiService.getModelProviderById(modelProviderId), + provider.type === "modelprovider" + ? ModelProviderApiService.getModelProviderById.key( + loadingProviderId + ) + : null, + ({ providerId }) => + ModelProviderApiService.getModelProviderById(providerId), { revalidateOnFocus: false, refreshInterval: 2000, @@ -48,18 +51,23 @@ export function ModelProviderConfigure({ ); useEffect(() => { - if (!loadingModelProviderId) return; + if (!loadingProviderId) return; + + if (provider.type === "authprovider") { + setDialogIsOpen(false); + return; + } const { isLoading, data } = getLoadingModelProviderModels; if (isLoading) return; if (data?.modelsBackPopulated) { setShowDefaultModelAliasForm(true); - setLoadingModelProviderId(""); + setLoadingProviderId(""); // revalidate models to get back populated models mutate(ModelApiService.getModels.key()); } - }, [getLoadingModelProviderModels, loadingModelProviderId]); + }, [getLoadingModelProviderModels, loadingProviderId, provider.type]); const handleDone = () => { setDialogIsOpen(false); @@ -70,10 +78,10 @@ export function ModelProviderConfigure({ @@ -83,12 +91,11 @@ export function ModelProviderConfigure({ - {loadingModelProviderId ? ( + {loadingProviderId ? (
- Loading {modelProvider.name}{" "} - Models... + Loading {provider.name} Models...
) : showDefaultModelAliasForm ? (
@@ -108,11 +115,9 @@ export function ModelProviderConfigure({
) : ( - - setLoadingModelProviderId(modelProvider.id) - } + setLoadingProviderId(provider.id)} /> )} @@ -120,20 +125,23 @@ export function ModelProviderConfigure({ ); } -export function ModelProviderConfigureContent({ - modelProvider, +export function ProviderConfigureContent({ + provider, onSuccess, }: { - modelProvider: ModelProvider; + provider: ModelProvider | AuthProvider; onSuccess: () => void; }) { - const revealModelProvider = useSWR( - ModelProviderApiService.revealModelProviderById.key(modelProvider.id), - async ({ modelProviderId }) => { + const revealByIdFunc = + provider.type === "modelprovider" + ? ModelProviderApiService.revealModelProviderById + : AuthProviderApiService.revealAuthProviderById; + + const revealProvider = useSWR( + revealByIdFunc.key(provider.id), + async ({ providerId }) => { try { - return await ModelProviderApiService.revealModelProviderById( - modelProviderId - ); + return await revealByIdFunc(providerId); } catch (error) { // 404: no credential found = just return empty object if (error instanceof NotFoundError) { @@ -145,23 +153,23 @@ export function ModelProviderConfigureContent({ } ); - const requiredParameters = modelProvider.requiredConfigurationParameters; - const parameters = revealModelProvider.data; + const requiredParameters = provider.requiredConfigurationParameters; + const optionalParameters = provider.optionalConfigurationParameters; + const parameters = revealProvider.data; return ( <> - {" "} - {modelProvider.configured - ? `Configure ${modelProvider.name}` - : `Set Up ${modelProvider.name}`} + {" "} + {provider.configured + ? `Configure ${provider.name}` + : `Set Up ${provider.name}`} - {(modelProvider.id === CommonModelProviderIds.ANTHROPIC || - modelProvider.id == - CommonModelProviderIds.ANTHROPIC_BEDROCK) && ( + {(provider.id === CommonModelProviderIds.ANTHROPIC || + provider.id == CommonModelProviderIds.ANTHROPIC_BEDROCK) && ( Note: Anthropic does not have an embeddings model and{" "} )} - {revealModelProvider.isLoading ? ( + {revealProvider.isLoading ? ( ) : ( - )} diff --git a/ui/admin/app/components/model-providers/ModelProviderDeconfigure.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderDeconfigure.tsx similarity index 54% rename from ui/admin/app/components/model-providers/ModelProviderDeconfigure.tsx rename to ui/admin/app/components/auth-and-model-providers/ProviderDeconfigure.tsx index 428e0430d..567e7afba 100644 --- a/ui/admin/app/components/model-providers/ModelProviderDeconfigure.tsx +++ b/ui/admin/app/components/auth-and-model-providers/ProviderDeconfigure.tsx @@ -2,7 +2,8 @@ import { useState } from "react"; import { toast } from "sonner"; import { mutate } from "swr"; -import { ModelProvider } from "~/lib/model/modelProviders"; +import { AuthProvider, ModelProvider } from "~/lib/model/providers"; +import { AuthProviderApiService } from "~/lib/service/api/authProviderApiService"; import { ModelApiService } from "~/lib/service/api/modelApiService"; import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService"; @@ -21,26 +22,36 @@ import { import { DropdownMenuItem } from "~/components/ui/dropdown-menu"; import { useAsync } from "~/hooks/useAsync"; -export function ModelProviderDeconfigure({ - modelProvider, +export function ProviderDeconfigure({ + provider, }: { - modelProvider: ModelProvider; + provider: ModelProvider | AuthProvider; }) { const [open, setOpen] = useState(false); const handleDeconfigure = async () => { - deconfigure.execute(modelProvider.id); + deconfigure.execute(provider.id); }; const deconfigure = useAsync( - ModelProviderApiService.deconfigureModelProviderById, + provider.type === "modelprovider" + ? ModelProviderApiService.deconfigureModelProviderById + : AuthProviderApiService.deconfigureAuthProviderById, { onSuccess: () => { - toast.success(`${modelProvider.name} deconfigured.`); - mutate(ModelProviderApiService.getModelProviders.key()); - mutate(ModelApiService.getModels.key()); + toast.success(`${provider.name} deconfigured.`); + mutate( + provider.type === "modelprovider" + ? ModelProviderApiService.getModelProviders.key() + : AuthProviderApiService.getAuthProviders.key() + ); + mutate( + provider.type === "modelprovider" + ? ModelApiService.getModels.key() + : null + ); }, onError: () => - toast.error(`Failed to deconfigure ${modelProvider.name}`), + toast.error(`Failed to deconfigure ${provider.name}`), } ); @@ -54,30 +65,20 @@ export function ModelProviderDeconfigure({ }} className="text-destructive" > - Deconfigure Model Provider + Deconfigure Provider - + - Deconfigure {modelProvider.name} + Deconfigure {provider.name} + {warningMessage(provider.type)} - Deconfiguring this model provider will remove all models - associated with it and reset it to its unconfigured state. - You will need to set up the model provider once again to use - it. + Are you sure you want to deconfigure {provider.name}? - - - Are you sure you want to deconfigure{" "} - {modelProvider.name}? - -
@@ -100,3 +101,12 @@ export function ModelProviderDeconfigure({ ); } + +function warningMessage(t: string | undefined): string | undefined { + switch (t) { + case "modelprovider": + return "Deconfiguring this model provider will remove all models associated with it and reset it to its unconfigured state. You will need to set up the model provider once again to use it."; + case "authprovider": + return "Deconfiguring this auth provider will sign out all users who are using it and reset it to its unconfigured state. You will need to set up the auth provider once again to use it."; + } +} diff --git a/ui/admin/app/components/auth-and-model-providers/ProviderForm.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderForm.tsx new file mode 100644 index 000000000..1fdfcc6a3 --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/ProviderForm.tsx @@ -0,0 +1,360 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { CircleAlertIcon } from "lucide-react"; +import { useEffect } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { mutate } from "swr"; +import { z } from "zod"; + +import { + AuthProvider, + ModelProvider, + ProviderConfig, +} from "~/lib/model/providers"; +import { AuthProviderApiService } from "~/lib/service/api/authProviderApiService"; +import { ModelApiService } from "~/lib/service/api/modelApiService"; +import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService"; + +import { TypographyH4 } from "~/components/Typography"; +import { + AuthProviderOptionalTooltips, + AuthProviderRequiredTooltips, + AuthProviderSensitiveFields, + ModelProviderRequiredTooltips, + ModelProviderSensitiveFields, +} from "~/components/auth-and-model-providers/constants"; +import { HelperTooltipLabel } from "~/components/composed/HelperTooltip"; +import { ControlledInput } from "~/components/form/controlledInputs"; +import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; +import { Button } from "~/components/ui/button"; +import { Form } from "~/components/ui/form"; +import { ScrollArea } from "~/components/ui/scroll-area"; +import { useAsync } from "~/hooks/useAsync"; + +const formSchema = z.object({ + requiredConfigParams: z.array( + z.object({ + label: z.string(), + name: z.string().min(1, { + message: "Name is required.", + }), + value: z.string().min(1, { + message: "This field is required.", + }), + }) + ), + optionalConfigParams: z.array( + z.object({ + label: z.string(), + name: z.string().min(1, { + message: "Name is required.", + }), + value: z.string(), + }) + ), +}); + +export type ProviderFormValues = z.infer; + +const translateUserFriendlyLabel = (label: string) => { + const fieldsToStrip = [ + "OBOT_OPENAI_MODEL_PROVIDER", + "OBOT_AZURE_OPENAI_MODEL_PROVIDER", + "OBOT_ANTHROPIC_MODEL_PROVIDER", + "OBOT_OLLAMA_MODEL_PROVIDER", + "OBOT_VOYAGE_MODEL_PROVIDER", + "OBOT_GROQ_MODEL_PROVIDER", + "OBOT_VLLM_MODEL_PROVIDER", + "OBOT_ANTHROPIC_BEDROCK_MODEL_PROVIDER", + "OBOT_AUTH_PROVIDER", + "OBOT_GOOGLE_AUTH_PROVIDER", + "OBOT_GITHUB_AUTH_PROVIDER", + ]; + + return fieldsToStrip + .reduce((acc, field) => { + return acc.replace(field, ""); + }, label) + .toLowerCase() + .replace(/_/g, " ") + .replace(/\b\w/g, (char: string) => char.toUpperCase()) + .trim(); +}; + +const getInitialRequiredParams = ( + requiredParameters: string[], + parameters: ProviderConfig +): ProviderFormValues["requiredConfigParams"] => + requiredParameters.map((requiredParameterKey) => ({ + label: translateUserFriendlyLabel(requiredParameterKey), + name: requiredParameterKey, + value: parameters[requiredParameterKey] ?? "", + })); + +const getInitialOptionalParams = ( + optionalParameters: string[], + parameters: ProviderConfig +): ProviderFormValues["optionalConfigParams"] => + optionalParameters.map((optionalParameterKey) => ({ + label: translateUserFriendlyLabel(optionalParameterKey), + name: optionalParameterKey, + value: parameters[optionalParameterKey] ?? "", + })); + +export function ProviderForm({ + provider, + onSuccess, + parameters, + requiredParameters, + optionalParameters, +}: { + provider: ModelProvider | AuthProvider; + onSuccess: () => void; + parameters: ProviderConfig; + requiredParameters: string[]; + optionalParameters: string[]; +}) { + const fetchAvailableModels = useAsync( + ModelApiService.getAvailableModelsByProvider, + { + onSuccess: () => { + mutate(ModelProviderApiService.getModelProviders.key()); + onSuccess(); + }, + } + ); + + const configureAuthProvider = useAsync( + AuthProviderApiService.configureAuthProviderById, + { + onSuccess: async () => { + mutate( + AuthProviderApiService.revealAuthProviderById.key( + provider.id + ) + ); + onSuccess(); + }, + } + ); + + const configureModelProvider = useAsync( + ModelProviderApiService.configureModelProviderById, + { + onSuccess: async () => { + mutate( + ModelProviderApiService.revealModelProviderById.key( + provider.id + ) + ); + await fetchAvailableModels.execute(provider.id); + }, + } + ); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + requiredConfigParams: getInitialRequiredParams( + requiredParameters, + parameters + ), + optionalConfigParams: getInitialOptionalParams( + optionalParameters, + parameters + ), + }, + }); + + useEffect(() => { + form.reset({ + requiredConfigParams: getInitialRequiredParams( + requiredParameters, + parameters + ), + optionalConfigParams: getInitialOptionalParams( + optionalParameters, + parameters + ), + }); + }, [requiredParameters, optionalParameters, parameters, form]); + + const requiredConfigParamFields = useFieldArray({ + control: form.control, + name: "requiredConfigParams", + }); + + const optionalConfigParamFields = useFieldArray({ + control: form.control, + name: "optionalConfigParams", + }); + + const { execute: onSubmit, isLoading } = useAsync( + async (data: ProviderFormValues) => { + const allConfigParams: Record = {}; + [data.requiredConfigParams, data.optionalConfigParams].forEach( + (configParams) => { + for (const param of configParams) { + if (param.value && param.name) { + allConfigParams[param.name] = param.value; + } + } + } + ); + + switch (provider.type) { + case "modelprovider": + await configureModelProvider.execute( + provider.id, + allConfigParams + ); + break; + case "authprovider": + await configureAuthProvider.execute( + provider.id, + allConfigParams + ); + break; + } + } + ); + + const FORM_ID = "model-provider-form"; + + const loading = + fetchAvailableModels.isLoading || + configureModelProvider.isLoading || + configureAuthProvider.isLoading || + isLoading; + + const sensitiveFields = + provider.type === "modelprovider" + ? ModelProviderSensitiveFields + : AuthProviderSensitiveFields; + + return ( +
+ {provider.type === "modelprovider" && + fetchAvailableModels.error !== null && ( +
+ + + An error occurred! + + Your configuration was saved, but we were not + able to connect to the model provider. Please + check your configuration and try again. + + +
+ )} + +
+
+ + + Required Configuration + + {requiredConfigParamFields.fields.map( + (field, i) => { + const type = sensitiveFields[field.name] + ? "password" + : "text"; + + return ( +
+ +
+ ); + } + )} + {optionalParameters.length > 0 && ( + + Optional Configuration + + )} + {optionalConfigParamFields.fields.map( + (field, i) => { + const type = sensitiveFields[field.name] + ? "password" + : "text"; + + return ( +
+ +
+ ); + } + )} +
+ +
+
+ +
+ +
+
+ ); + + function renderLabelWithTooltip(type: string | undefined, label: string) { + const tooltip = + type === "modelprovider" + ? ModelProviderRequiredTooltips[provider.id]?.[label] + : AuthProviderRequiredTooltips[provider.id]?.[label]; + return ; + } + + function renderLabelWithTooltipOptional( + type: string | undefined, + label: string + ) { + const tooltip = + type === "modelprovider" + ? "" + : AuthProviderOptionalTooltips[provider.id]?.[label]; + return ; + } +} diff --git a/ui/admin/app/components/auth-and-model-providers/ProviderIcon.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderIcon.tsx new file mode 100644 index 000000000..7af9ddd10 --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/ProviderIcon.tsx @@ -0,0 +1,34 @@ +import { BoxesIcon } from "lucide-react"; + +import { AuthProvider, ModelProvider } from "~/lib/model/providers"; +import { cn } from "~/lib/utils"; + +import { + CommonAuthProviderIds, + CommonModelProviderIds, +} from "~/components/auth-and-model-providers/constants"; + +export function ProviderIcon({ + provider, + size = "md", +}: { + provider: ModelProvider | AuthProvider; + size?: "md" | "lg"; +}) { + return provider.icon ? ( + {provider.name} + ) : ( + + ); +} diff --git a/ui/admin/app/components/model-providers/ModelProviderDropdown.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderMenu.tsx similarity index 66% rename from ui/admin/app/components/model-providers/ModelProviderDropdown.tsx rename to ui/admin/app/components/auth-and-model-providers/ProviderMenu.tsx index adb6bc73f..ab2d47857 100644 --- a/ui/admin/app/components/model-providers/ModelProviderDropdown.tsx +++ b/ui/admin/app/components/auth-and-model-providers/ProviderMenu.tsx @@ -1,8 +1,8 @@ import { EllipsisVerticalIcon } from "lucide-react"; -import { ModelProvider } from "~/lib/model/modelProviders"; +import { AuthProvider, ModelProvider } from "~/lib/model/providers"; -import { ModelProviderDeconfigure } from "~/components/model-providers/ModelProviderDeconfigure"; +import { ProviderDeconfigure } from "~/components/auth-and-model-providers/ProviderDeconfigure"; import { Button } from "~/components/ui/button"; import { DropdownMenu, @@ -13,10 +13,10 @@ import { DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; -export function ModelProviderMenu({ - modelProvider, +export function ProviderMenu({ + provider, }: { - modelProvider: ModelProvider; + provider: ModelProvider | AuthProvider; }) { return ( @@ -27,9 +27,9 @@ export function ModelProviderMenu({ - {modelProvider.name} + {provider.name} - + diff --git a/ui/admin/app/components/model-providers/constants.ts b/ui/admin/app/components/auth-and-model-providers/constants.ts similarity index 58% rename from ui/admin/app/components/model-providers/constants.ts rename to ui/admin/app/components/auth-and-model-providers/constants.ts index bf9990f0e..66a3c3ae9 100644 --- a/ui/admin/app/components/model-providers/constants.ts +++ b/ui/admin/app/components/auth-and-model-providers/constants.ts @@ -22,11 +22,6 @@ export const ModelProviderLinks = { "https://aws.amazon.com/bedrock/claude/", }; -export const ModelProviderConfigurationLinks = { - [CommonModelProviderIds.AZURE_OPENAI]: - "https://docs.otto8.ai/configuration/model-providers#azure-openai", -}; - export const RecommendedModelProviders = [ CommonModelProviderIds.OPENAI, CommonModelProviderIds.AZURE_OPENAI, @@ -106,3 +101,79 @@ export const ModelProviderSensitiveFields: Record = OBOT_ANTHROPIC_BEDROCK_MODEL_PROVIDER_SESSION_TOKEN: true, OBOT_ANTHROPIC_BEDROCK_MODEL_PROVIDER_REGION: false, }; + +export const CommonAuthProviderIds = { + GOOGLE: "google-auth-provider", + GITHUB: "github-auth-provider", +}; + +export const CommonAuthProviderFriendlyNames: Record = { + "google-auth-provider": "Google", + "github-auth-provider": "GitHub", +}; + +export const AuthProviderLinks = { + [CommonAuthProviderIds.GOOGLE]: "https://google.com", + [CommonAuthProviderIds.GITHUB]: "https://github.com", +}; + +export const AuthProviderRequiredTooltips: { + [key: string]: { + [key: string]: string; + }; +} = { + [CommonAuthProviderIds.GOOGLE]: { + "Client Id": + "Unique identifier for the application when using Google's OAuth. Can typically be found in Google Cloud Console > Credentials", + "Client Secret": + "Password or key that app uses to authenticate with Google's OAuth. Can typically be found in Google Cloud Console > Credentials", + "Cookie Secret": + "Secret used to encrypt cookies. Must be a random string of length 16, 24, or 32.", + "Email Domains": + "Comma separated list of email domains that are allowed to authenticate with this provider. * is a special value that allows all domains.", + }, + [CommonAuthProviderIds.GITHUB]: { + "Client Id": + "Client ID for your GitHub OAuth app. Can be found in GitHub Developer Settings > OAuth Apps", + "Client Secret": + "Client secret for your GitHub OAuth app. Can be found in GitHub Developer Settings > OAuth Apps", + "Cookie Secret": + "Secret used to encrypt cookies. Must be a random string of length 16, 24, or 32.", + "Email Domains": + "Comma separated list of email domains that are allowed to authenticate with this provider. * is a special value that allows all domains.", + }, +}; + +export const AuthProviderOptionalTooltips: { + [key: string]: { + [key: string]: string; + }; +} = { + [CommonAuthProviderIds.GITHUB]: { + Teams: "Restrict logins to members of any of these GitHub teams (comma-separated list).", + Org: "Restrict logins to members of this GitHub organization.", + Repo: "Restrict logins to collaborators on this GitHub repository (formatted orgname/repo).", + Token: "The token to use when verifying repository collaborators (must have push access to the repository).", + "Allow Users": + "Users allowed to log in, even if they do not belong to the specified org and team or collaborators.", + }, +}; + +export const AuthProviderSensitiveFields: Record = + { + // All + OBOT_AUTH_PROVIDER_COOKIE_SECRET: true, + OBOT_AUTH_PROVIDER_EMAIL_DOMAINS: false, + + // Google + OBOT_GOOGLE_AUTH_PROVIDER_CLIENT_ID: false, + OBOT_GOOGLE_AUTH_PROVIDER_CLIENT_SECRET: true, + + // GitHub + OBOT_GITHUB_AUTH_PROVIDER_CLIENT_ID: false, + OBOT_GITHUB_AUTH_PROVIDER_CLIENT_SECRET: true, + OBOT_GITHUB_AUTH_PROVIDER_TEAMS: false, + OBOT_GITHUB_AUTH_PROVIDER_ORG: false, + OBOT_GITHUB_AUTH_PROVIDER_REPO: false, + OBOT_GITHUB_AUTH_PROVIDER_TOKEN: true, + }; diff --git a/ui/admin/app/components/chat/Chatbar.tsx b/ui/admin/app/components/chat/Chatbar.tsx index 10a7746c5..564bcbfe7 100644 --- a/ui/admin/app/components/chat/Chatbar.tsx +++ b/ui/admin/app/components/chat/Chatbar.tsx @@ -3,9 +3,9 @@ import { useState } from "react"; import { cn } from "~/lib/utils"; +import { ModelProviderTooltip } from "~/components/auth-and-model-providers/ModelProviderTooltip"; import { ChatActions } from "~/components/chat/ChatActions"; import { useChat } from "~/components/chat/ChatContext"; -import { ModelProviderTooltip } from "~/components/model-providers/ModelProviderTooltip"; import { LoadingSpinner } from "~/components/ui/LoadingSpinner"; import { Button } from "~/components/ui/button"; import { AutosizeTextarea } from "~/components/ui/textarea"; diff --git a/ui/admin/app/components/chat/RunWorkflow.tsx b/ui/admin/app/components/chat/RunWorkflow.tsx index c39de0728..bf542ea72 100644 --- a/ui/admin/app/components/chat/RunWorkflow.tsx +++ b/ui/admin/app/components/chat/RunWorkflow.tsx @@ -5,9 +5,9 @@ import useSWR from "swr"; import { WorkflowService } from "~/lib/service/api/workflowService"; import { cn } from "~/lib/utils"; +import { ModelProviderTooltip } from "~/components/auth-and-model-providers/ModelProviderTooltip"; import { useChat } from "~/components/chat/ChatContext"; import { RunWorkflowForm } from "~/components/chat/RunWorkflowForm"; -import { ModelProviderTooltip } from "~/components/model-providers/ModelProviderTooltip"; import { Button, ButtonProps } from "~/components/ui/button"; import { Popover, diff --git a/ui/admin/app/components/model-providers/ModelProviderForm.tsx b/ui/admin/app/components/model-providers/ModelProviderForm.tsx deleted file mode 100644 index 9a8ca1f5e..000000000 --- a/ui/admin/app/components/model-providers/ModelProviderForm.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { CircleAlertIcon } from "lucide-react"; -import { useEffect } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; -import { mutate } from "swr"; -import { z } from "zod"; - -import { ModelProvider, ModelProviderConfig } from "~/lib/model/modelProviders"; -import { ModelApiService } from "~/lib/service/api/modelApiService"; -import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService"; - -import { TypographyH4 } from "~/components/Typography"; -import { - HelperTooltipLabel, - HelperTooltipLink, -} from "~/components/composed/HelperTooltip"; -import { - NameDescriptionForm, - ParamFormValues, -} from "~/components/composed/NameDescriptionForm"; -import { ControlledInput } from "~/components/form/controlledInputs"; -import { - ModelProviderConfigurationLinks, - ModelProviderRequiredTooltips, - ModelProviderSensitiveFields, -} from "~/components/model-providers/constants"; -import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; -import { Button } from "~/components/ui/button"; -import { Form } from "~/components/ui/form"; -import { ScrollArea } from "~/components/ui/scroll-area"; -import { Separator } from "~/components/ui/separator"; -import { useAsync } from "~/hooks/useAsync"; - -const formSchema = z.object({ - requiredConfigParams: z.array( - z.object({ - label: z.string(), - name: z.string().min(1, { - message: "Name is required.", - }), - value: z.string().min(1, { - message: "This field is required.", - }), - }) - ), - additionalConfirmParams: z.array( - z.object({ - name: z.string(), - description: z.string(), - }) - ), -}); - -export type ModelProviderFormValues = z.infer; - -const translateUserFriendlyLabel = (label: string) => { - const fieldsToStrip = [ - "OBOT_OPENAI_MODEL_PROVIDER", - "OBOT_AZURE_OPENAI_MODEL_PROVIDER", - "OBOT_ANTHROPIC_MODEL_PROVIDER", - "OBOT_OLLAMA_MODEL_PROVIDER", - "OBOT_VOYAGE_MODEL_PROVIDER", - "OBOT_GROQ_MODEL_PROVIDER", - "OBOT_VLLM_MODEL_PROVIDER", - "OBOT_ANTHROPIC_BEDROCK_MODEL_PROVIDER", - ]; - - return fieldsToStrip - .reduce((acc, field) => { - return acc.replace(field, ""); - }, label) - .toLowerCase() - .replace(/_/g, " ") - .replace(/\b\w/g, (char: string) => char.toUpperCase()) - .trim(); -}; - -const getInitialRequiredParams = ( - requiredParameters: string[], - parameters: ModelProviderConfig -): ModelProviderFormValues["requiredConfigParams"] => - requiredParameters.map((requiredParameterKey) => ({ - label: translateUserFriendlyLabel(requiredParameterKey), - name: requiredParameterKey, - value: parameters[requiredParameterKey] ?? "", - })); - -const getInitialAdditionalParams = ( - requiredParameters: string[], - parameters: ModelProviderConfig -): ParamFormValues["params"] => { - const defaultEmptyParams = [{ name: "", description: "" }]; - - const requiredParameterSet = new Set(requiredParameters); - const additionalParams = Object.entries(parameters).filter( - ([key]) => !requiredParameterSet.has(key) - ); - return additionalParams.length === 0 - ? defaultEmptyParams - : additionalParams.map(([key, value]) => ({ - name: key, - description: value, - })); -}; - -export function ModelProviderForm({ - modelProvider, - onSuccess, - parameters, - requiredParameters, -}: { - modelProvider: ModelProvider; - onSuccess: () => void; - parameters: ModelProviderConfig; - requiredParameters: string[]; -}) { - const fetchAvailableModels = useAsync( - ModelApiService.getAvailableModelsByProvider, - { - onSuccess: () => { - mutate(ModelProviderApiService.getModelProviders.key()); - onSuccess(); - }, - } - ); - - const configureModelProvider = useAsync( - ModelProviderApiService.configureModelProviderById, - { - onSuccess: async () => { - mutate( - ModelProviderApiService.revealModelProviderById.key( - modelProvider.id - ) - ); - await fetchAvailableModels.execute(modelProvider.id); - }, - } - ); - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - requiredConfigParams: getInitialRequiredParams( - requiredParameters, - parameters - ), - additionalConfirmParams: getInitialAdditionalParams( - requiredParameters, - parameters - ), - }, - }); - - useEffect(() => { - form.reset({ - requiredConfigParams: getInitialRequiredParams( - requiredParameters, - parameters - ), - additionalConfirmParams: getInitialAdditionalParams( - requiredParameters, - parameters - ), - }); - }, [requiredParameters, parameters, form]); - - const requiredConfigParamFields = useFieldArray({ - control: form.control, - name: "requiredConfigParams", - }); - - const { execute: onSubmit, isLoading } = useAsync( - async (data: ModelProviderFormValues) => { - const allConfigParams: Record = {}; - [data.requiredConfigParams, data.additionalConfirmParams].forEach( - (configParams) => { - for (const param of configParams) { - const paramValue = - "value" in param ? param.value : param.description; - if (paramValue && param.name) { - allConfigParams[param.name] = paramValue; - } - } - } - ); - - await configureModelProvider.execute( - modelProvider.id, - allConfigParams - ); - } - ); - - const FORM_ID = "model-provider-form"; - const showCustomConfiguration = - modelProvider.id === "azure-openai-model-provider"; - - const loading = - fetchAvailableModels.isLoading || - configureModelProvider.isLoading || - isLoading; - return ( -
- {fetchAvailableModels.error !== null && ( -
- - - An error occurred! - - Your configuration was saved, but we were not able - to connect to the model provider. Please check your - configuration and try again. - - -
- )} - -
- - Required Configuration - -
- - {requiredConfigParamFields.fields.map( - (field, i) => { - const type = ModelProviderSensitiveFields[ - field.name - ] - ? "password" - : "text"; - - return ( -
- -
- ); - } - )} -
- - - {showCustomConfiguration && renderCustomConfiguration()} -
-
- -
- -
-
- ); - - function renderCustomConfiguration() { - return ( - <> - - -
- - Custom Configuration (Optional) - - {ModelProviderConfigurationLinks[modelProvider.id] - ? renderCustomConfigTooltip(modelProvider.id) - : null} -
- - form.setValue("additionalConfirmParams", values) - } - /> - - ); - } - - function renderCustomConfigTooltip(modelProviderId: string) { - const link = ModelProviderConfigurationLinks[modelProviderId]; - return ; - } - - function renderLabelWithTooltip(label: string) { - const tooltip = - ModelProviderRequiredTooltips[modelProvider.id]?.[label]; - return ; - } -} diff --git a/ui/admin/app/components/model-providers/ModelProviderIcon.tsx b/ui/admin/app/components/model-providers/ModelProviderIcon.tsx deleted file mode 100644 index 129fa12e7..000000000 --- a/ui/admin/app/components/model-providers/ModelProviderIcon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { BoxesIcon } from "lucide-react"; - -import { ModelProvider } from "~/lib/model/modelProviders"; -import { cn } from "~/lib/utils"; - -import { CommonModelProviderIds } from "~/components/model-providers/constants"; - -export function ModelProviderIcon({ - modelProvider, - size = "md", -}: { - modelProvider: ModelProvider; - size?: "md" | "lg"; -}) { - return modelProvider.icon ? ( - {modelProvider.name} - ) : ( - - ); -} diff --git a/ui/admin/app/components/sidebar/Sidebar.tsx b/ui/admin/app/components/sidebar/Sidebar.tsx index f43e299fe..5a0f17d6d 100644 --- a/ui/admin/app/components/sidebar/Sidebar.tsx +++ b/ui/admin/app/components/sidebar/Sidebar.tsx @@ -3,6 +3,7 @@ import { BoxesIcon, InfoIcon, KeyIcon, + LockIcon, MessageSquare, PuzzleIcon, User, @@ -81,6 +82,11 @@ const items = [ url: $path("/webhooks"), icon: WebhookIcon, }, + { + title: "Auth Providers", + url: $path("/auth-providers"), + icon: LockIcon, + }, ]; export function AppSidebar() { diff --git a/ui/admin/app/components/signin/SignIn.tsx b/ui/admin/app/components/signin/SignIn.tsx index 2d99f1e65..0bce6319b 100644 --- a/ui/admin/app/components/signin/SignIn.tsx +++ b/ui/admin/app/components/signin/SignIn.tsx @@ -1,7 +1,8 @@ -import { FaGoogle } from "react-icons/fa"; - import { cn } from "~/lib/utils"; +import { Bootstrap } from "~/components/auth-and-model-providers/Bootstrap"; +import { ProviderIcon } from "~/components/auth-and-model-providers/ProviderIcon"; +import { CommonAuthProviderFriendlyNames } from "~/components/auth-and-model-providers/constants"; import { ObotLogo } from "~/components/branding/ObotLogo"; import { Button } from "~/components/ui/button"; import { @@ -11,12 +12,16 @@ import { CardHeader, CardTitle, } from "~/components/ui/card"; +import { useAuthProviders } from "~/hooks/auth-providers/useAuthProviders"; interface SignInProps { className?: string; } export function SignIn({ className }: SignInProps) { + const { authProviders } = useAuthProviders(); + const configuredAuthProviders = authProviders.filter((p) => p.configured); + return (
- - Please sign in using the button below. - + {configuredAuthProviders.length > 0 && ( + + Please sign in using an option below. + + )} - - + + {configuredAuthProviders.map((provider) => ( + + ))} + {configuredAuthProviders.length === 0 && }
diff --git a/ui/admin/app/components/user/UserMenu.tsx b/ui/admin/app/components/user/UserMenu.tsx index c6c8e67e6..aa3d6116f 100644 --- a/ui/admin/app/components/user/UserMenu.tsx +++ b/ui/admin/app/components/user/UserMenu.tsx @@ -3,6 +3,7 @@ import React from "react"; import { AuthDisabledUsername } from "~/lib/model/auth"; import { roleToString } from "~/lib/model/users"; +import { ApiRoutes } from "~/lib/routers/apiRoutes"; import { cn } from "~/lib/utils"; import { useAuth } from "~/components/auth/AuthContext"; @@ -56,7 +57,11 @@ export const UserMenu: React.FC = ({ { + onClick={async () => { + await fetch(ApiRoutes.bootstrap.logout().url, { + method: "POST", + }); + window.location.href = "/oauth2/sign_out?rd=/admin/"; }} diff --git a/ui/admin/app/hooks/auth-providers/useAuthProviders.tsx b/ui/admin/app/hooks/auth-providers/useAuthProviders.tsx new file mode 100644 index 000000000..095995e17 --- /dev/null +++ b/ui/admin/app/hooks/auth-providers/useAuthProviders.tsx @@ -0,0 +1,14 @@ +import useSWR from "swr"; + +import { AuthProviderApiService } from "~/lib/service/api/authProviderApiService"; + +export function useAuthProviders() { + const { data: authProviders } = useSWR( + AuthProviderApiService.getAuthProviders.key(), + () => AuthProviderApiService.getAuthProviders() + ); + const configured = + authProviders?.some((authProvider) => authProvider.configured) ?? false; + + return { configured, authProviders: authProviders ?? [] }; +} diff --git a/ui/admin/app/lib/model/models.ts b/ui/admin/app/lib/model/models.ts index e2d6a67a0..d71e682f2 100644 --- a/ui/admin/app/lib/model/models.ts +++ b/ui/admin/app/lib/model/models.ts @@ -1,6 +1,5 @@ import { z } from "zod"; -import { ModelProviderStatus } from "~/lib/model/modelProviders"; import { EntityMeta } from "~/lib/model/primitives"; export const ModelUsage = { @@ -96,15 +95,6 @@ export const ModelManifestSchema = z.object({ usage: z.nativeEnum(ModelUsage), }); -type ModelProviderManifest = { - name: string; - toolReference: string; -}; - -export type ModelProvider = EntityMeta & - ModelProviderManifest & - ModelProviderStatus; - export function getModelUsageFromAlias(alias: string) { if (!(alias in ModelAliasToUsageMap)) return null; diff --git a/ui/admin/app/lib/model/modelProviders.ts b/ui/admin/app/lib/model/providers.ts similarity index 53% rename from ui/admin/app/lib/model/modelProviders.ts rename to ui/admin/app/lib/model/providers.ts index ec205c97a..1ecf9ca0c 100644 --- a/ui/admin/app/lib/model/modelProviders.ts +++ b/ui/admin/app/lib/model/providers.ts @@ -1,18 +1,24 @@ import { EntityMeta } from "~/lib/model/primitives"; -export type ModelProviderStatus = { +export type ProviderStatus = { configured: boolean; - modelsBackPopulated?: boolean; icon?: string; requiredConfigurationParameters?: string[]; + optionalConfigurationParameters?: string[]; missingConfigurationParameters?: string[]; }; -export type ModelProvider = EntityMeta & - ModelProviderStatus & { +export type Provider = EntityMeta & + ProviderStatus & { toolReference: string; name: string; revision: string; }; -export type ModelProviderConfig = Record; +export type ProviderConfig = Record; + +export type ModelProvider = Provider & { + modelsBackPopulated?: boolean; +}; + +export type AuthProvider = Provider; diff --git a/ui/admin/app/lib/routers/apiRoutes.ts b/ui/admin/app/lib/routers/apiRoutes.ts index 9681921c1..122428914 100644 --- a/ui/admin/app/lib/routers/apiRoutes.ts +++ b/ui/admin/app/lib/routers/apiRoutes.ts @@ -266,6 +266,23 @@ export const ApiRoutes = { deleteAlias: (aliasId: string) => buildUrl(`/default-model-aliases/${aliasId}`), }, + authProviders: { + base: () => buildUrl("/auth-providers"), + getAuthProviders: () => buildUrl("/auth-providers"), + getAuthProviderById: (authProviderId: string) => + buildUrl(`/auth-providers/${authProviderId}`), + configureAuthProviderById: (authProviderId: string) => + buildUrl(`/auth-providers/${authProviderId}/configure`), + revealAuthProviderById: (authProviderId: string) => + buildUrl(`/auth-providers/${authProviderId}/reveal`), + deconfigureAuthProviderById: (authProviderId: string) => + buildUrl(`/auth-providers/${authProviderId}/deconfigure`), // TODO - implement this in the backend + }, + bootstrap: { + base: () => buildUrl("/bootstrap"), + login: () => buildUrl("/bootstrap/login"), + logout: () => buildUrl("/bootstrap/logout"), + }, webhooks: { base: () => buildUrl("/webhooks"), getWebhooks: () => buildUrl("/webhooks"), diff --git a/ui/admin/app/lib/service/api/authProviderApiService.ts b/ui/admin/app/lib/service/api/authProviderApiService.ts new file mode 100644 index 000000000..1b329c0ce --- /dev/null +++ b/ui/admin/app/lib/service/api/authProviderApiService.ts @@ -0,0 +1,90 @@ +import { + AuthProvider, + ModelProvider, + ProviderConfig, +} from "~/lib/model/providers"; +import { ApiRoutes } from "~/lib/routers/apiRoutes"; +import { request } from "~/lib/service/api/primitives"; + +const getAuthProviders = async () => { + const res = await request<{ items: AuthProvider[] }>({ + url: ApiRoutes.authProviders.getAuthProviders().url, + errorMessage: "Failed to get supported auth providers.", + }); + + return res.data.items ?? ([] as AuthProvider[]); +}; +getAuthProviders.key = () => + ({ url: ApiRoutes.authProviders.getAuthProviders().path }) as const; + +const getAuthProviderById = async (providerKey: string) => { + const res = await request({ + url: ApiRoutes.authProviders.getAuthProviderById(providerKey).url, + method: "GET", + errorMessage: + "Failed to update configuration values on the requested auth provider.", + }); + + return res.data; +}; +getAuthProviderById.key = (providerId?: string) => { + if (!providerId) return null; + + return { + url: ApiRoutes.authProviders.getAuthProviderById(providerId).path, + providerId, + }; +}; + +const configureAuthProviderById = async ( + providerKey: string, + providerConfig: ProviderConfig +) => { + const res = await request({ + url: ApiRoutes.authProviders.configureAuthProviderById(providerKey).url, + method: "POST", + data: providerConfig, + errorMessage: + "Failed to update configuration values on the requested auth provider.", + }); + + return res.data; +}; + +const revealAuthProviderById = async (providerKey: string) => { + const res = await request({ + url: ApiRoutes.authProviders.revealAuthProviderById(providerKey).url, + method: "POST", + errorMessage: + "Failed to reveal configuration values on the requested auth provider.", + }); + + return res.data; +}; +revealAuthProviderById.key = (providerId?: string) => { + if (!providerId) return null; + + return { + url: ApiRoutes.authProviders.revealAuthProviderById(providerId).path, + providerId, + }; +}; + +const deconfigureAuthProviderById = async (providerKey: string) => { + const res = await request({ + url: ApiRoutes.authProviders.deconfigureAuthProviderById(providerKey) + .url, + method: "POST", + errorMessage: "Failed to deconfigure the requested auth provider.", + }); + + return res.data; +}; + +export const AuthProviderApiService = { + getAuthProviders, + getAuthProviderById, + configureAuthProviderById, + revealAuthProviderById, + deconfigureAuthProviderById, +}; diff --git a/ui/admin/app/lib/service/api/modelProviderApiService.ts b/ui/admin/app/lib/service/api/modelProviderApiService.ts index 7b0fab580..9616ab6fd 100644 --- a/ui/admin/app/lib/service/api/modelProviderApiService.ts +++ b/ui/admin/app/lib/service/api/modelProviderApiService.ts @@ -1,4 +1,4 @@ -import { ModelProvider, ModelProviderConfig } from "~/lib/model/modelProviders"; +import { ModelProvider, ProviderConfig } from "~/lib/model/providers"; import { ApiRoutes } from "~/lib/routers/apiRoutes"; import { request } from "~/lib/service/api/primitives"; @@ -13,72 +13,66 @@ const getModelProviders = async () => { getModelProviders.key = () => ({ url: ApiRoutes.modelProviders.getModelProviders().path }) as const; -const getModelProviderById = async (modelProviderKey: string) => { +const getModelProviderById = async (providerKey: string) => { const res = await request({ - url: ApiRoutes.modelProviders.getModelProviderById(modelProviderKey) - .url, + url: ApiRoutes.modelProviders.getModelProviderById(providerKey).url, method: "GET", errorMessage: - "Failed to update configuration values on the requested modal provider.", + "Failed to update configuration values on the requested model provider.", }); return res.data; }; -getModelProviderById.key = (modelProviderId?: string) => { - if (!modelProviderId) return null; +getModelProviderById.key = (providerId?: string) => { + if (!providerId) return null; return { - url: ApiRoutes.modelProviders.getModelProviderById(modelProviderId) - .path, - modelProviderId, + url: ApiRoutes.modelProviders.getModelProviderById(providerId).path, + providerId, }; }; const configureModelProviderById = async ( - modelProviderKey: string, - modelProviderConfig: ModelProviderConfig + providerKey: string, + providerConfig: ProviderConfig ) => { const res = await request({ - url: ApiRoutes.modelProviders.configureModelProviderById( - modelProviderKey - ).url, + url: ApiRoutes.modelProviders.configureModelProviderById(providerKey) + .url, method: "POST", - data: modelProviderConfig, + data: providerConfig, errorMessage: - "Failed to update configuration values on the requested modal provider.", + "Failed to update configuration values on the requested model provider.", }); return res.data; }; -const revealModelProviderById = async (modelProviderKey: string) => { - const res = await request({ - url: ApiRoutes.modelProviders.revealModelProviderById(modelProviderKey) - .url, +const revealModelProviderById = async (providerKey: string) => { + const res = await request({ + url: ApiRoutes.modelProviders.revealModelProviderById(providerKey).url, method: "POST", errorMessage: - "Failed to reveal configuration values on the requested modal provider.", + "Failed to reveal configuration values on the requested model provider.", }); return res.data; }; -revealModelProviderById.key = (modelProviderId?: string) => { - if (!modelProviderId) return null; +revealModelProviderById.key = (providerId?: string) => { + if (!providerId) return null; return { - url: ApiRoutes.modelProviders.revealModelProviderById(modelProviderId) - .path, - modelProviderId, + url: ApiRoutes.modelProviders.revealModelProviderById(providerId).path, + providerId, }; }; -const deconfigureModelProviderById = async (modelProviderKey: string) => { +const deconfigureModelProviderById = async (providerKey: string) => { const res = await request({ - url: ApiRoutes.modelProviders.deconfigureModelProviderById( - modelProviderKey - ).url, + url: ApiRoutes.modelProviders.deconfigureModelProviderById(providerKey) + .url, method: "POST", - errorMessage: "Failed to deconfigure the requested modal provider.", + errorMessage: "Failed to deconfigure the requested model provider.", }); return res.data; diff --git a/ui/admin/app/routes/_auth.auth-providers.tsx b/ui/admin/app/routes/_auth.auth-providers.tsx new file mode 100644 index 000000000..a8913762a --- /dev/null +++ b/ui/admin/app/routes/_auth.auth-providers.tsx @@ -0,0 +1,74 @@ +import { MetaFunction } from "react-router"; + +import { AuthProvider } from "~/lib/model/providers"; +import { RouteHandle } from "~/lib/service/routeHandles"; + +import { TypographyH2 } from "~/components/Typography"; +import { AuthProviderList } from "~/components/auth-and-model-providers/AuthProviderLists"; +import { CommonAuthProviderIds } from "~/components/auth-and-model-providers/constants"; +import { WarningAlert } from "~/components/composed/WarningAlert"; +import { useAuthProviders } from "~/hooks/auth-providers/useAuthProviders"; + +const sortAuthProviders = (authProviders: AuthProvider[]) => { + return [...authProviders].sort((a, b) => { + const preferredOrder = [ + CommonAuthProviderIds.GOOGLE, + CommonAuthProviderIds.GITHUB, + ]; + const aIndex = preferredOrder.indexOf(a.id); + const bIndex = preferredOrder.indexOf(b.id); + + // If both providers are in preferredOrder, sort by their order + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + // If only a is in preferredOrder, it comes first + if (aIndex !== -1) return -1; + // If only b is in preferredOrder, it comes first + if (bIndex !== -1) return 1; + + // For all other providers, sort alphabetically by name + return a.name.localeCompare(b.name); + }); +}; + +export default function AuthProviders() { + const { configured: authProviderConfigured, authProviders } = + useAuthProviders(); + const sortedAuthProviders = sortAuthProviders(authProviders); + return ( +
+
+
+
+ + Auth Providers + +
+ {authProviderConfigured ? ( +
+ ) : ( + + )} +
+ +
+ +
+
+
+ ); +} + +export const handle: RouteHandle = { + breadcrumb: () => [{ content: "Auth Providers" }], +}; + +export const meta: MetaFunction = () => { + return [{ title: `Obot • Auth Providers` }]; +}; diff --git a/ui/admin/app/routes/_auth.model-providers.tsx b/ui/admin/app/routes/_auth.model-providers.tsx index a7fb6d387..5a1b2ed71 100644 --- a/ui/admin/app/routes/_auth.model-providers.tsx +++ b/ui/admin/app/routes/_auth.model-providers.tsx @@ -1,15 +1,15 @@ import { MetaFunction } from "react-router"; import { preload } from "swr"; -import { ModelProvider } from "~/lib/model/modelProviders"; +import { ModelProvider } from "~/lib/model/providers"; import { DefaultModelAliasApiService } from "~/lib/service/api/defaultModelAliasApiService"; import { ModelApiService } from "~/lib/service/api/modelApiService"; import { RouteHandle } from "~/lib/service/routeHandles"; import { TypographyH2 } from "~/components/Typography"; +import { ModelProviderList } from "~/components/auth-and-model-providers/ModelProviderLists"; +import { CommonModelProviderIds } from "~/components/auth-and-model-providers/constants"; import { WarningAlert } from "~/components/composed/WarningAlert"; -import { ModelProviderList } from "~/components/model-providers/ModelProviderLists"; -import { CommonModelProviderIds } from "~/components/model-providers/constants"; import { DefaultModelAliasFormDialog } from "~/components/model/DefaultModelAliasForm"; import { useModelProviders } from "~/hooks/model-providers/useModelProviders"; diff --git a/ui/user/src/lib/auth.ts b/ui/user/src/lib/auth.ts new file mode 100644 index 000000000..7619fe61a --- /dev/null +++ b/ui/user/src/lib/auth.ts @@ -0,0 +1,13 @@ +export type AuthProvider = { + configured: boolean + icon?: string + name: string + namespace: string + id: string +} + +export async function listAuthProviders(): Promise { + const resp = await fetch('/api/auth-providers') + const data = await resp.json() + return data.items.filter((provider: AuthProvider) => provider.configured); +} diff --git a/ui/user/src/lib/stores/profile.ts b/ui/user/src/lib/stores/profile.ts index aaf593769..922db7181 100644 --- a/ui/user/src/lib/stores/profile.ts +++ b/ui/user/src/lib/stores/profile.ts @@ -12,7 +12,7 @@ async function init() { try { store.set(await getProfile()); } catch (e) { - if (e instanceof Error && e.message.startsWith('403')) { + if (e instanceof Error && (e.message.startsWith('403') || e.message.startsWith('401'))) { store.set({ email: '', iconURL: '', diff --git a/ui/user/src/routes/+page.svelte b/ui/user/src/routes/+page.svelte index 99ac99a04..1ddc81e0d 100644 --- a/ui/user/src/routes/+page.svelte +++ b/ui/user/src/routes/+page.svelte @@ -8,9 +8,13 @@ import { darkMode } from '$lib/stores'; import { Book } from '$lib/icons'; import { loadedAssistants } from '$lib/stores'; + import { listAuthProviders, type AuthProvider } from '$lib/auth'; - onMount(() => { + let authProviders: AuthProvider[] = $state([]) + + onMount(async () => { highlight.highlightAll(); + authProviders = await listAuthProviders(); }); let div: HTMLElement; @@ -69,22 +73,29 @@ Friendly. Open Source. Assistant. -
diff --git a/ui/user/src/routes/[agent]/+page.svelte b/ui/user/src/routes/[agent]/+page.svelte index 1a0d5c1ba..26fe63a68 100644 --- a/ui/user/src/routes/[agent]/+page.svelte +++ b/ui/user/src/routes/[agent]/+page.svelte @@ -15,7 +15,8 @@ $effect(() => { if ($profile.unauthorized) { - window.location.href = '/oauth2/start?rd=' + window.location.pathname; + // Redirect to the main page to log in. + window.location.href = '/'; } });