From 6d0ab66f695e6692546dad66bb06102ad9352302 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Wed, 15 Jan 2025 16:21:18 -0500 Subject: [PATCH] rebase Signed-off-by: Grant Linville --- apiclient/types/authprovider.go | 23 ++ apiclient/types/toolreference.go | 1 + apiclient/types/zz_generated.deepcopy.go | 85 +++++ go.mod | 41 +- go.sum | 113 +----- pkg/accesstoken/accesstoken.go | 14 + pkg/api/authz/authz.go | 11 +- pkg/api/handlers/authprovider.go | 216 +++++++++++ pkg/api/request.go | 18 +- pkg/api/router/router.go | 19 +- pkg/api/server/server.go | 36 +- pkg/bootstrap/bootstrap.go | 102 +++++ pkg/cli/internal/token.go | 118 +++++- pkg/controller/data/agent.yaml | 4 +- .../handlers/toolreference/toolreference.go | 2 + pkg/controller/routes.go | 2 +- pkg/gateway/client/auth.go | 15 +- pkg/gateway/client/client.go | 8 - pkg/gateway/client/user.go | 91 ++--- pkg/gateway/db/db.go | 1 - pkg/gateway/pkce/pkce.go | 61 --- pkg/gateway/server/authprovider.go | 241 ------------ pkg/gateway/server/dispatcher/dispatcher.go | 186 ++++++++- pkg/gateway/server/llmproxy.go | 2 +- pkg/gateway/server/oauth.go | 73 ++-- pkg/gateway/server/response.go | 37 -- pkg/gateway/server/router.go | 14 +- pkg/gateway/server/server.go | 76 +--- pkg/gateway/server/token.go | 90 ++--- pkg/gateway/server/tokenreview.go | 10 +- pkg/gateway/server/user.go | 8 +- pkg/gateway/types/identity.go | 13 +- pkg/gateway/types/oauth_apps.go | 1 + pkg/gateway/types/providers.go | 213 ----------- pkg/gateway/types/tokens.go | 13 +- pkg/proxy/proxy.go | 260 ++++++++----- pkg/services/config.go | 52 +-- pkg/storage/apis/obot.obot.ai/v1/run.go | 1 + .../openapi/generated/openapi_generated.go | 167 ++++++++ .../AuthProviderLists.tsx | 72 ++++ .../auth-and-model-providers/Bootstrap.tsx | 73 ++++ .../ModelProviderLists.tsx | 82 ++++ .../ModelProviderModels.tsx | 62 +-- .../ModelProviderTooltip.tsx | 0 .../ProviderConfigure.tsx | 194 ++++++++++ .../ProviderDeconfigure.tsx | 111 ++++++ .../auth-and-model-providers/ProviderForm.tsx | 360 ++++++++++++++++++ .../auth-and-model-providers/ProviderIcon.tsx | 34 ++ .../auth-and-model-providers/ProviderMenu.tsx | 37 ++ .../constants.ts | 81 +++- ui/admin/app/components/chat/Chatbar.tsx | 2 +- ui/admin/app/components/chat/RunWorkflow.tsx | 2 +- .../ModelProviderConfigure.tsx | 180 --------- .../ModelProviderDeconfigure.tsx | 96 ----- .../model-providers/ModelProviderDropdown.tsx | 37 -- .../model-providers/ModelProviderForm.tsx | 298 --------------- .../model-providers/ModelProviderIcon.tsx | 33 -- .../model-providers/ModelProviderLists.tsx | 68 ---- ui/admin/app/components/sidebar/Sidebar.tsx | 104 ++--- ui/admin/app/components/signin/SignIn.tsx | 70 ++-- ui/admin/app/components/user/UserMenu.tsx | 74 ++-- .../hooks/auth-providers/useAuthProviders.tsx | 14 + ui/admin/app/lib/model/modelProviders.ts | 18 - ui/admin/app/lib/model/models.ts | 10 - ui/admin/app/lib/model/providers.ts | 24 ++ ui/admin/app/lib/routers/apiRoutes.ts | 17 + .../lib/service/api/authProviderApiService.ts | 90 +++++ .../service/api/modelProviderApiService.ts | 90 ++--- ui/admin/app/routes/_auth.auth-providers.tsx | 74 ++++ ui/admin/app/routes/_auth.model-providers.tsx | 6 +- ui/admin/app/routes/_auth.tsx | 23 +- ui/user/src/lib/auth.ts | 13 + ui/user/src/lib/stores/profile.ts | 2 +- ui/user/src/routes/+page.svelte | 40 +- ui/user/src/routes/[agent]/+page.svelte | 3 +- 75 files changed, 2838 insertions(+), 2094 deletions(-) create mode 100644 apiclient/types/authprovider.go create mode 100644 pkg/accesstoken/accesstoken.go create mode 100644 pkg/api/handlers/authprovider.go create mode 100644 pkg/bootstrap/bootstrap.go delete mode 100644 pkg/gateway/pkce/pkce.go delete mode 100644 pkg/gateway/server/authprovider.go create mode 100644 ui/admin/app/components/auth-and-model-providers/AuthProviderLists.tsx create mode 100644 ui/admin/app/components/auth-and-model-providers/Bootstrap.tsx create mode 100644 ui/admin/app/components/auth-and-model-providers/ModelProviderLists.tsx rename ui/admin/app/components/{model-providers => auth-and-model-providers}/ModelProviderModels.tsx (71%) rename ui/admin/app/components/{model-providers => auth-and-model-providers}/ModelProviderTooltip.tsx (100%) create mode 100644 ui/admin/app/components/auth-and-model-providers/ProviderConfigure.tsx create mode 100644 ui/admin/app/components/auth-and-model-providers/ProviderDeconfigure.tsx create mode 100644 ui/admin/app/components/auth-and-model-providers/ProviderForm.tsx create mode 100644 ui/admin/app/components/auth-and-model-providers/ProviderIcon.tsx create mode 100644 ui/admin/app/components/auth-and-model-providers/ProviderMenu.tsx rename ui/admin/app/components/{model-providers => auth-and-model-providers}/constants.ts (58%) delete mode 100644 ui/admin/app/components/model-providers/ModelProviderConfigure.tsx delete mode 100644 ui/admin/app/components/model-providers/ModelProviderDeconfigure.tsx delete mode 100644 ui/admin/app/components/model-providers/ModelProviderDropdown.tsx delete mode 100644 ui/admin/app/components/model-providers/ModelProviderForm.tsx delete mode 100644 ui/admin/app/components/model-providers/ModelProviderIcon.tsx delete mode 100644 ui/admin/app/components/model-providers/ModelProviderLists.tsx create mode 100644 ui/admin/app/hooks/auth-providers/useAuthProviders.tsx delete mode 100644 ui/admin/app/lib/model/modelProviders.ts create mode 100644 ui/admin/app/lib/model/providers.ts create mode 100644 ui/admin/app/lib/service/api/authProviderApiService.ts create mode 100644 ui/admin/app/routes/_auth.auth-providers.tsx create mode 100644 ui/user/src/lib/auth.ts 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 4ba346f71..74cf02295 100644 --- a/apiclient/types/zz_generated.deepcopy.go +++ b/apiclient/types/zz_generated.deepcopy.go @@ -245,6 +245,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 *Authorization) DeepCopyInto(out *Authorization) { *out = *in diff --git a/go.mod b/go.mod index 7b3193d1d..4b99e55a9 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( github.com/gptscript-ai/gptscript v0.9.6-0.20250114054318-73a9ffeb1cc8 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-20250110042456-3848b881955b github.com/obot-platform/nah v0.0.0-20241217120500-e9169e4a999f github.com/obot-platform/namegenerator v0.0.0-20241217121223-fc58bdb7dca2 @@ -55,9 +54,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 @@ -66,22 +62,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 @@ -92,12 +84,10 @@ require ( github.com/cloudflare/circl v1.5.0 // indirect github.com/containerd/console v1.0.4 // indirect github.com/containerd/log v0.1.0 // 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/distribution/reference v0.5.0 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/docker/cli v27.3.1+incompatible // indirect @@ -114,15 +104,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 @@ -137,13 +124,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/gptscript-ai/broadcaster v0.0.0-20240625175512-c43682019b86 // indirect github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect @@ -152,7 +136,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 @@ -166,7 +149,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 @@ -175,17 +157,14 @@ require ( github.com/lib/pq v1.10.9 // 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/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.4.0 // indirect @@ -200,11 +179,11 @@ 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/onsi/ginkgo/v2 v2.20.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // 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 @@ -214,31 +193,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 @@ -251,7 +220,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/auto/sdk v1.1.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 @@ -273,14 +241,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 v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/grpc v1.68.1 // indirect google.golang.org/protobuf v1.35.2 // 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 4488568b1..4443289d2 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/adhocore/gronx v1.19.5 h1:cwIG4nT1v9DvadxtHBe6MzE+FZ1JDvAUC45U2fl4eSQ= github.com/adhocore/gronx v1.19.5/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= @@ -73,10 +63,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= @@ -94,16 +80,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= @@ -112,12 +92,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= @@ -147,14 +121,11 @@ 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/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -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= @@ -169,8 +140,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/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= @@ -196,9 +165,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= @@ -210,16 +177,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= @@ -238,10 +202,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= @@ -289,7 +249,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= @@ -308,7 +267,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= @@ -322,27 +280,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= @@ -376,8 +325,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= @@ -418,8 +365,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= @@ -455,8 +400,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= @@ -471,8 +414,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= @@ -484,8 +425,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/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -519,18 +458,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-20250110042456-3848b881955b h1:1OzOajUTN+OQcffpFmr11e2NB4sjT/u4mSGYYzUs/D8= github.com/obot-platform/kinm v0.0.0-20250110042456-3848b881955b/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= @@ -543,8 +476,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -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= @@ -578,8 +509,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= @@ -595,10 +524,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= @@ -611,20 +536,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= @@ -644,8 +561,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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.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= @@ -662,10 +577,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= @@ -694,8 +605,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= @@ -716,8 +625,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/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= @@ -759,7 +666,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= @@ -809,7 +715,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= @@ -817,7 +722,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.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -866,13 +770,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= @@ -883,8 +786,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= @@ -898,7 +799,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= @@ -939,8 +839,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -949,8 +847,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= @@ -980,11 +876,9 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi 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.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -993,7 +887,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.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= @@ -1008,8 +901,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 87ac62605..c66bcc0d8 100644 --- a/pkg/api/authz/authz.go +++ b/pkg/api/authz/authz.go @@ -36,18 +36,21 @@ 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}/{namespace}/{name}", - "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}", + "/api/oauth/redirect/{namespace}/{name}", "/api/assistants", "GET /api/me", "PATCH /api/users/{id}", diff --git a/pkg/api/handlers/authprovider.go b/pkg/api/handlers/authprovider.go new file mode 100644 index 000000000..c426ee81b --- /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/obot.obot.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 abd423ee1..f21c4f8df 100644 --- a/pkg/api/request.go +++ b/pkg/api/request.go @@ -274,17 +274,19 @@ 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 } func (r *Context) UserTimezone() string { diff --git a/pkg/api/router/router.go b/pkg/api/router/router.go index 8df2224e6..8c5dd8896 100644 --- a/pkg/api/router/router.go +++ b/pkg/api/router/router.go @@ -24,8 +24,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() @@ -33,6 +34,7 @@ func Router(services *services.Services) (http.Handler, error) { tables := handlers.NewTableHandler(services.GPTClient) // Version + mux.HandleFunc("GET /api/version", version.GetVersion) // Agents @@ -294,6 +296,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) @@ -318,6 +330,9 @@ func Router(services *services.Services) (http.Handler, error) { // Catch all 404 for API mux.HTTPHandle("/api/", http.NotFoundHandler()) + // Auth Provider tools + mux.HandleFunc("/oauth2/", services.ProxyManager.HandlerFunc) + // Gateway APIs services.GatewayServer.AddRoutes(services.APIServer) diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 3564da2e4..6676cc64a 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,17 +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-Obot-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") user, err := s.authenticator.Authenticate(req) if err != nil { @@ -75,19 +63,17 @@ 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) - return - } - - req.Header.Set("X-Obot-Auth-Required", "true") - s.proxyServer.ServeHTTP(rw, req) + if !s.authorizer.Authorize(req, user) { + http.Error(rw, "forbidden", http.StatusForbidden) return } + if strings.HasPrefix(req.URL.Path, "/api/") { + 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") + } + err = f(api.Context{ ResponseWriter: rw, Request: req, 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..01f670879 100644 --- a/pkg/cli/internal/token.go +++ b/pkg/cli/internal/token.go @@ -11,6 +11,7 @@ import ( "os" "os/signal" "path/filepath" + "sort" "time" "github.com/adrg/xdg" @@ -39,15 +40,23 @@ func enter(ctx context.Context) error { } } +type providerConfig struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + ID string `json:"id,omitempty"` +} + func Token(ctx context.Context, baseURL string) (string, error) { // Check to see if authentication is required for this baseURL if testToken(ctx, baseURL, "") { return "", nil } - serviceName, err := getAuthProviderServiceName(ctx, baseURL) + authProviders, err := getAuthProviderServiceInfo(ctx, baseURL) if err != nil { return "", err + } else if len(authProviders) == 0 { + return "", fmt.Errorf("no auth providers found") } ctx, sigCancel := signal.NotifyContext(ctx, os.Interrupt) @@ -76,8 +85,37 @@ func Token(ctx context.Context, baseURL string) (string, error) { return token, nil } + // Look for the last used auth provider, if there is one. + authProviderFile, err := xdg.ConfigFile(filepath.Join("obot", "auth-provider")) + if err != nil { + return "", err + } + + var providerCfg providerConfig + providerConfigData, err := os.ReadFile(authProviderFile) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return "", fmt.Errorf("reading %s: %w", authProviderFile, err) + } else if err == nil { + if err := json.Unmarshal(providerConfigData, &providerCfg); err != nil { + return "", err + } + } + + if providerCfg.ID == "" || providerCfg.Namespace == "" || providerCfg.Name == "" { + provider, err := userSelectAuthProvider(authProviders) + if err != nil { + return "", err + } + + providerCfg = providerConfig{ + Name: provider.Name, + Namespace: provider.Namespace, + ID: provider.ID, + } + } + uuid := uuid.NewString() - loginURL, err := create(ctx, baseURL, uuid, serviceName) + loginURL, err := create(ctx, baseURL, uuid, providerCfg.ID, providerCfg.Namespace) if err != nil { return "", fmt.Errorf("failed to create login request: %w", err) } @@ -87,7 +125,7 @@ func Token(ctx context.Context, baseURL string) (string, error) { fmt.Println(color.GreenString("Authentication is needed")) fmt.Println(color.GreenString("========================")) fmt.Println() - fmt.Println(color.CyanString(serviceName) + " is used for authentication using the browser. This can be bypassed by setting") + fmt.Println(color.CyanString(providerCfg.Name) + " is used for authentication using the browser. This can be bypassed by setting") fmt.Println("the env var " + color.CyanString("OBOT_API_KEY") + " to your API key.") fmt.Println() fmt.Println(color.GreenString("Press ENTER to continue (CTRL+C to exit)")) @@ -114,21 +152,33 @@ func Token(ctx context.Context, baseURL string) (string, error) { return "", fmt.Errorf("failed to store token: %w", err) } + // Save the provider config. We deliberately ignore errors here, because it doesn't really matter + // if we are unable to save it for some reason. The user will be asked again the next time. + providerCfgJSON, err := json.Marshal(providerCfg) + if err == nil { + _ = os.WriteFile(authProviderFile, providerCfgJSON, 0600) + } + return token, os.WriteFile(tokenFile, tokenData, 0600) } type createRequest struct { - ServiceName string `json:"serviceName,omitempty"` - ID string `json:"id,omitempty"` + ProviderName string `json:"providerName,omitempty"` + ProviderNamespace string `json:"providerNamespace,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, providerName, providerNamespace 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, + ProviderName: providerName, + ProviderNamespace: providerNamespace, + }); err != nil { return "", err } @@ -210,27 +260,63 @@ 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) ([]types2.AuthProvider, error) { req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/auth-providers", nil) if err != nil { - return "", err + return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { - return "", err + return nil, 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 nil, err } - if len(authProviders) == 0 { - return "", fmt.Errorf("no auth providers found") + if len(authProviders.Items) == 0 { + return nil, fmt.Errorf("no auth providers found") + } + + return authProviders.Items, nil +} + +func userSelectAuthProvider(authProviders []types2.AuthProvider) (types2.AuthProvider, error) { + var configuredAuthProviders []types2.AuthProvider + for _, provider := range authProviders { + if provider.Configured { + configuredAuthProviders = append(configuredAuthProviders, provider) + } + } + + if len(configuredAuthProviders) == 0 { + return types2.AuthProvider{}, fmt.Errorf("no configured auth providers found") + } else if len(configuredAuthProviders) == 1 { + return configuredAuthProviders[0], nil + } + + sort.Slice(configuredAuthProviders, func(i, j int) bool { + return configuredAuthProviders[i].Name < configuredAuthProviders[j].Name + }) + fmt.Println() + fmt.Println(color.CyanString("Select an authentication provider:")) + for i, provider := range configuredAuthProviders { + fmt.Printf(" %d. %s\n", i+1, provider.Name) + } + fmt.Println() + fmt.Println(color.GreenString("Enter the number of the provider you want to use:")) + + var choice int + if _, err := fmt.Scanln(&choice); err != nil { + return types2.AuthProvider{}, fmt.Errorf("error reading choice: %w", err) + } + + if choice < 1 || choice > len(configuredAuthProviders) { + return types2.AuthProvider{}, fmt.Errorf("invalid choice %d", choice) } - // Take the last auth provider. That is the one created most recently. - return authProviders[len(authProviders)-1].ServiceName, nil + return configuredAuthProviders[choice-1], nil } diff --git a/pkg/controller/data/agent.yaml b/pkg/controller/data/agent.yaml index 96243a4ff..37f0bbd15 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 1b4cc8635..a70ac04e5 100644 --- a/pkg/controller/handlers/toolreference/toolreference.go +++ b/pkg/controller/handlers/toolreference/toolreference.go @@ -40,6 +40,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 { @@ -182,6 +183,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 2d2fe63fa..64b1a01a0 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, + toolRef := toolreference.New(c.services.GPTClient, c.services.ProviderDispatcher, c.services.ToolRegistryURL, c.services.SupportDocker) workspace := workspace.New(c.services.GPTClient, c.services.WorkspaceProviderType) knowledgeset := knowledgeset.New(c.services.Invoker) diff --git a/pkg/gateway/client/auth.go b/pkg/gateway/client/auth.go index 8420e4378..3c73342a1 100644 --- a/pkg/gateway/client/auth.go +++ b/pkg/gateway/client/auth.go @@ -32,15 +32,12 @@ func (u UserDecorator) AuthenticateRequest(req *http.Request) (*authenticator.Re return nil, false, nil } - 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(), - }, - req.Header.Get("X-Obot-User-Timezone"), - ) + gatewayUser, err := u.client.EnsureIdentity(req.Context(), &types.Identity{ + 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(), + }, req.Header.Get("X-Obot-User-Timezone")) if err != nil { return nil, false, err } diff --git a/pkg/gateway/client/client.go b/pkg/gateway/client/client.go index 66bd11708..04ded1835 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" ) @@ -38,9 +36,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 a6b67dd0b..e953938ed 100644 --- a/pkg/gateway/client/user.go +++ b/pkg/gateway/client/user.go @@ -9,8 +9,8 @@ import ( "time" types2 "github.com/obot-platform/obot/apiclient/types" + "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" ) @@ -103,27 +103,23 @@ func (c *Client) UpdateUser(ctx context.Context, actingUserIsAdmin bool, updated }) } -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 } @@ -132,7 +128,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 } @@ -149,71 +145,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 d015dfe5f..64d4886e7 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 f6914f673..3a8c04f72 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..855235d24 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,38 @@ 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&obot-auth-provider=%s", + s.baseURL, + url.QueryEscape(fmt.Sprintf("/api/oauth/redirect/%s/%s?state=%s", namespace, name, state)), + url.QueryEscape(fmt.Sprintf("%s/%s", namespace, name)), + ), + 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 +91,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..c0ff9ba59 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" @@ -28,7 +29,8 @@ const ( type tokenRequestRequest struct { ID string `json:"id"` - ServiceName string `json:"serviceName"` + ProviderName string `json:"providerName"` + ProviderNamespace string `json:"providerNamespace"` 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,55 +130,60 @@ func (s *Server) tokenRequest(apiContext api.Context) error { return types2.NewErrHttp(http.StatusBadRequest, fmt.Sprintf("invalid token request body: %v", err)) } + if reqObj.ProviderName != "" { + list, err := s.dispatcher.ListConfiguredAuthProviders(apiContext.Context(), reqObj.ProviderNamespace) + if err != nil { + return types2.NewErrHttp(http.StatusInternalServerError, err.Error()) + } + + if !slices.Contains(list, reqObj.ProviderName) { + return types2.NewErrHttp(http.StatusBadRequest, fmt.Sprintf("auth provider %q not found", reqObj.ProviderName)) + } + } + 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") } return types2.NewErrHttp(http.StatusInternalServerError, err.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)}) + if reqObj.ProviderName != "" { + return apiContext.Write(map[string]any{"token-path": fmt.Sprintf("%s/api/oauth/start/%s/%s/%s", s.baseURL, reqObj.ID, reqObj.ProviderNamespace, reqObj.ProviderName)}) } 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("auth provider %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 3ffb627c9..f2849f62b 100644 --- a/pkg/gateway/server/user.go +++ b/pkg/gateway/server/user.go @@ -25,7 +25,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(), namespace, name) + 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 3842f7e0f..89cafbbb0 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" ZoomAuthorizeURL = "https://zoom.us/oauth/authorize" ZoomTokenURL = "https://zoom.us/oauth/token" 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..8dc75d6e9 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -2,106 +2,160 @@ 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/api" + "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) HandlerFunc(ctx api.Context) error { + pm.ServeHTTP(ctx.ResponseWriter, ctx.Request) + return nil +} + +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 } - oauthProxyOpts.RawRedirectURL = serverURL + "/oauth2/callback" - oauthProxyOpts.ReverseProxy = true - if cfg.AuthEmailDomains != "" { - oauthProxyOpts.EmailDomains = strings.Split(cfg.AuthEmailDomains, ",") + // 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] } - if err = validation.Validate(oauthProxyOpts); err != nil { - log.Fatalf("%s", err) + 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, + }) + } + + 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(providerNamespace, providerName, 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 +163,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 + } + + 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 := state.PreferredUsername + 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 b73cb791f..12a3bfce5 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -24,6 +24,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" @@ -48,10 +49,7 @@ 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"` @@ -65,12 +63,14 @@ type Config struct { 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:"3000" env:"OBOT_KNOWLEDGESET_INGESTION_LIMIT" name:"knowledge-set-ingestion-limit"` EmailServerName string `usage:"The name of the email server to display for email receivers"` Docker bool `usage:"Enable Docker support" default:"false" env:"OBOT_DOCKER"` EnvKeys []string `usage:"The environment keys to pass through to the GPTScript server" env:"OBOT_ENV_KEYS"` + KnowledgeSetIngestionLimit int `usage:"The maximum number of files to ingest into a knowledge set" default:"3000" env:"OBOT_KNOWLEDGESET_INGESTION_LIMIT" name:"knowledge-set-ingestion-limit"` + 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"` - AuthConfig GatewayConfig services.Config } @@ -89,10 +89,11 @@ type Services struct { TokenServer *jwt.TokenService APIServer *server.Server Started chan struct{} - ProxyServer *proxy.Proxy GatewayServer *gserver.Server GatewayClient *client.Client - ModelProviderDispatcher *dispatcher.Dispatcher + ProxyManager *proxy.Manager + ProviderDispatcher *dispatcher.Dispatcher + Bootstrapper *bootstrap2.Bootstrap KnowledgeSetIngestionLimit int SupportDocker bool } @@ -283,39 +284,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, gatewayClient, 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, gatewayClient, 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{}) @@ -352,16 +351,17 @@ func New(ctx context.Context, config Config) (*Services, error) { Router: r, GPTClient: c, APIServer: server.NewServer(storageClient, c, authn.NewAuthenticator(authenticators), - authz.NewAuthorizer(r.Backend()), proxyServer, config.Hostname), + authz.NewAuthorizer(r.Backend()), proxyManager, config.Hostname), TokenServer: tokenServer, Invoker: invoker, GatewayServer: gatewayServer, GatewayClient: gatewayClient, - ProxyServer: proxyServer, KnowledgeSetIngestionLimit: config.KnowledgeSetIngestionLimit, EmailServerName: config.EmailServerName, - ModelProviderDispatcher: modelProviderDispatcher, SupportDocker: config.Docker, + ProxyManager: proxyManager, + ProviderDispatcher: providerDispatcher, + Bootstrapper: bootstrapper, }, nil } diff --git a/pkg/storage/apis/obot.obot.ai/v1/run.go b/pkg/storage/apis/obot.obot.ai/v1/run.go index 6b78721da..c3bd94e22 100644 --- a/pkg/storage/apis/obot.obot.ai/v1/run.go +++ b/pkg/storage/apis/obot.obot.ai/v1/run.go @@ -17,6 +17,7 @@ const ( ModelProviderSyncAnnotation = "obot.ai/model-provider-sync" WorkflowSyncAnnotation = "obot.ai/workflow-sync" AgentSyncAnnotation = "obot.ai/agent-sync" + AuthProviderSyncAnnotation = "obot.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 2b8083f43..db543cefb 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.Authorization": schema_obot_platform_obot_apiclient_types_Authorization(ref), "github.com/obot-platform/obot/apiclient/types.AuthorizationList": schema_obot_platform_obot_apiclient_types_AuthorizationList(ref), "github.com/obot-platform/obot/apiclient/types.AuthorizationManifest": schema_obot_platform_obot_apiclient_types_AuthorizationManifest(ref), @@ -803,6 +807,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_Authorization(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/auth-and-model-providers/ModelProviderLists.tsx b/ui/admin/app/components/auth-and-model-providers/ModelProviderLists.tsx new file mode 100644 index 000000000..b80eacd56 --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/ModelProviderLists.tsx @@ -0,0 +1,82 @@ +import { CircleCheckIcon, CircleSlashIcon } from "lucide-react"; +import { Link } from "react-router"; + +import { ModelProvider } from "~/lib/model/providers"; + +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/auth-and-model-providers/constants"; +import { Badge } from "~/components/ui/badge"; +import { Card, CardContent, CardHeader } from "~/components/ui/card"; + +export function ModelProviderList({ + modelProviders, +}: { + modelProviders: ModelProvider[]; +}) { + return ( +
+
+ {modelProviders.map((modelProvider) => ( + + + {RecommendedModelProviders.includes( + modelProvider.id + ) && Recommended} + {modelProvider.configured ? ( +
+ + +
+ ) : ( +
+ )} + + + + + +
+ {modelProvider.name} +
+ + + {modelProvider.configured ? ( + + {" "} + Configured + + ) : ( + + + Not Configured + + )} + + +
+ + ))} +
+
+ ); +} diff --git a/ui/admin/app/components/model-providers/ModelProviderModels.tsx b/ui/admin/app/components/auth-and-model-providers/ModelProviderModels.tsx similarity index 71% rename from ui/admin/app/components/model-providers/ModelProviderModels.tsx rename to ui/admin/app/components/auth-and-model-providers/ModelProviderModels.tsx index 4526a751a..bc4c384ae 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"; @@ -78,36 +78,36 @@ export function ModelProvidersModels({ modelProvider }: ModelsConfigureProps) { {modelProvider.name} Models - + - - - - {" "} - {modelProvider.name} Models - - - - {!isLoading && ( -
- -
- )} -
-
- - ); + + + + {" "} + {modelProvider.name} Models + + + + {!isLoading && ( +
+ +
+ )} +
+
+ + ); function getColumns(): ColumnDef[] { return [ 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/auth-and-model-providers/ProviderConfigure.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderConfigure.tsx new file mode 100644 index 000000000..4dca6cef4 --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/ProviderConfigure.tsx @@ -0,0 +1,194 @@ +import { useEffect, useState } from "react"; +import useSWR, { mutate } from "swr"; + +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 { 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"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import { Link } from "~/components/ui/link"; + +type ProviderConfigureProps = { + provider: ModelProvider | AuthProvider; +}; + +export function ProviderConfigure({ provider }: ProviderConfigureProps) { + const [dialogIsOpen, setDialogIsOpen] = useState(false); + const [showDefaultModelAliasForm, setShowDefaultModelAliasForm] = + useState(false); + + const [loadingProviderId, setLoadingProviderId] = useState(""); + + const getLoadingModelProviderModels = useSWR( + provider.type === "modelprovider" + ? ModelProviderApiService.getModelProviderById.key( + loadingProviderId + ) + : null, + ({ providerId }) => + ModelProviderApiService.getModelProviderById(providerId), + { + revalidateOnFocus: false, + refreshInterval: 2000, + } + ); + + useEffect(() => { + if (!loadingProviderId) return; + + if (provider.type === "authprovider") { + setDialogIsOpen(false); + return; + } + + const { isLoading, data } = getLoadingModelProviderModels; + if (isLoading) return; + + if (data?.modelsBackPopulated) { + setShowDefaultModelAliasForm(true); + setLoadingProviderId(""); + // revalidate models to get back populated models + mutate(ModelApiService.getModels.key()); + } + }, [getLoadingModelProviderModels, loadingProviderId, provider.type]); + + const handleDone = () => { + setDialogIsOpen(false); + setShowDefaultModelAliasForm(false); + }; + + return ( + + + + + + + + + {loadingProviderId ? ( +
+ Loading {provider.name} Models... +
+ ) : showDefaultModelAliasForm ? ( +
+ + + Configure Default Model Aliases + + + + When no model is specified, a default model is used + for creating a new agent, workflow, or working with + some tools, etc. Select your default models for the + usage types below. + +
+ +
+
+ ) : ( + setLoadingProviderId(provider.id)} + /> + )} +
+
+ ); +} + +export function ProviderConfigureContent({ + provider, + onSuccess, +}: { + provider: ModelProvider | AuthProvider; + onSuccess: () => void; +}) { + const revealByIdFunc = + provider.type === "modelprovider" + ? ModelProviderApiService.revealModelProviderById + : AuthProviderApiService.revealAuthProviderById; + + const revealProvider = useSWR( + revealByIdFunc.key(provider.id), + async ({ providerId }) => { + try { + return await revealByIdFunc(providerId); + } catch (error) { + // 404: no credential found = just return empty object + if (error instanceof NotFoundError) { + return {}; + } + // other errors = continue throw + throw error; + } + } + ); + + const requiredParameters = provider.requiredConfigurationParameters; + const optionalParameters = provider.optionalConfigurationParameters; + const parameters = revealProvider.data; + + return ( + <> + + + {" "} + {provider.configured + ? `Configure ${provider.name}` + : `Set Up ${provider.name}`} + + + + {(provider.id === CommonModelProviderIds.ANTHROPIC || + provider.id == CommonModelProviderIds.ANTHROPIC_BEDROCK) && ( + + Note: Anthropic does not have an embeddings model and{" "} + + recommends + {" "} + Voyage AI. + + )} + {revealProvider.isLoading ? ( + + ) : ( + + )} + + ); +} diff --git a/ui/admin/app/components/auth-and-model-providers/ProviderDeconfigure.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderDeconfigure.tsx new file mode 100644 index 000000000..759253c10 --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/ProviderDeconfigure.tsx @@ -0,0 +1,111 @@ +import { useState } from "react"; +import { toast } from "sonner"; +import { mutate } from "swr"; + +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"; + +import { Button } from "~/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import { DropdownMenuItem } from "~/components/ui/dropdown-menu"; +import { useAsync } from "~/hooks/useAsync"; + +export function ProviderDeconfigure({ + provider, +}: { + provider: ModelProvider | AuthProvider; +}) { + const [open, setOpen] = useState(false); + const handleDeconfigure = async () => { + deconfigure.execute(provider.id); + }; + + const deconfigure = useAsync( + provider.type === "modelprovider" + ? ModelProviderApiService.deconfigureModelProviderById + : AuthProviderApiService.deconfigureAuthProviderById, + { + onSuccess: () => { + 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 ${provider.name}`), + } + ); + + return ( + + + { + event.preventDefault(); + setOpen(true); + }} + className="text-destructive" + > + Deconfigure Provider + + + + + + + + Deconfigure {provider.name} + +

{warningMessage(provider.type)}

+

+ Are you sure you want to deconfigure {provider.name}? +

+ +
+ + + + + + +
+
+
+
+ ); +} + +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/auth-and-model-providers/ProviderMenu.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderMenu.tsx new file mode 100644 index 000000000..d2999e273 --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/ProviderMenu.tsx @@ -0,0 +1,37 @@ +import { EllipsisVerticalIcon } from "lucide-react"; + +import { AuthProvider, ModelProvider } from "~/lib/model/providers"; + +import { ProviderDeconfigure } from "~/components/auth-and-model-providers/ProviderDeconfigure"; +import { Button } from "~/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; + +export function ProviderMenu({ + provider, +}: { + provider: ModelProvider | AuthProvider; +}) { + return ( + + + + + + + {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 abeb93f26..93b010811 100644 --- a/ui/admin/app/components/model-providers/constants.ts +++ b/ui/admin/app/components/auth-and-model-providers/constants.ts @@ -26,11 +26,6 @@ export const ModelProviderLinks = { [CommonModelProviderIds.DEEPSEEK]: "https://www.deepseek.com/", }; -export const ModelProviderConfigurationLinks = { - [CommonModelProviderIds.AZURE_OPENAI]: - "https://docs.obot.ai/configuration/model-providers#azure-openai", -}; - export const RecommendedModelProviders = [ CommonModelProviderIds.OPENAI, CommonModelProviderIds.AZURE_OPENAI, @@ -121,3 +116,79 @@ export const ModelProviderSensitiveFields: Record = // DeepSeek OBOT_DEEPSEEK_MODEL_PROVIDER_API_KEY: true, }; + +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 5068665d2..9ffc8e370 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 2fd911dde..d06b7a2f0 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/ModelProviderConfigure.tsx b/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx deleted file mode 100644 index 7f8746d52..000000000 --- a/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { useEffect, useState } from "react"; -import useSWR, { mutate } from "swr"; - -import { ModelProvider } from "~/lib/model/modelProviders"; -import { NotFoundError } from "~/lib/service/api/apiErrors"; -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 { DefaultModelAliasForm } from "~/components/model/DefaultModelAliasForm"; -import { LoadingSpinner } from "~/components/ui/LoadingSpinner"; -import { Button } from "~/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "~/components/ui/dialog"; -import { Link } from "~/components/ui/link"; - -type ModelProviderConfigureProps = { - modelProvider: ModelProvider; -}; - -export function ModelProviderConfigure({ - modelProvider, -}: ModelProviderConfigureProps) { - const [dialogIsOpen, setDialogIsOpen] = useState(false); - const [showDefaultModelAliasForm, setShowDefaultModelAliasForm] = - useState(false); - - const [loadingModelProviderId, setLoadingModelProviderId] = useState(""); - - const getLoadingModelProviderModels = useSWR( - ModelProviderApiService.getModelProviderById.key(loadingModelProviderId), - ({ modelProviderId }) => - ModelProviderApiService.getModelProviderById(modelProviderId), - { - revalidateOnFocus: false, - refreshInterval: 2000, - } - ); - - useEffect(() => { - if (!loadingModelProviderId) return; - - const { isLoading, data } = getLoadingModelProviderModels; - if (isLoading) return; - - if (data?.modelsBackPopulated) { - setShowDefaultModelAliasForm(true); - setLoadingModelProviderId(""); - // revalidate models to get back populated models - mutate(ModelApiService.getModels.key()); - } - }, [getLoadingModelProviderModels, loadingModelProviderId]); - - const handleDone = () => { - setDialogIsOpen(false); - setShowDefaultModelAliasForm(false); - }; - - return ( - - - - - - - - - {loadingModelProviderId ? ( -
- Loading {modelProvider.name} Models... -
- ) : showDefaultModelAliasForm ? ( -
- - - Configure Default Model Aliases - - - - When no model is specified, a default model is used for creating a - new agent, workflow, or working with some tools, etc. Select your - default models for the usage types below. - -
- -
-
- ) : ( - setLoadingModelProviderId(modelProvider.id)} - /> - )} -
-
- ); -} - -export function ModelProviderConfigureContent({ - modelProvider, - onSuccess, -}: { - modelProvider: ModelProvider; - onSuccess: () => void; -}) { - const revealModelProvider = useSWR( - ModelProviderApiService.revealModelProviderById.key(modelProvider.id), - async ({ modelProviderId }) => { - try { - return await ModelProviderApiService.revealModelProviderById( - modelProviderId - ); - } catch (error) { - // 404: no credential found = just return empty object - if (error instanceof NotFoundError) { - return {}; - } - // other errors = continue throw - throw error; - } - } - ); - - const requiredParameters = modelProvider.requiredConfigurationParameters; - const parameters = revealModelProvider.data; - - return ( - <> - - - {" "} - {modelProvider.configured - ? `Configure ${modelProvider.name}` - : `Set Up ${modelProvider.name}`} - - - - {(modelProvider.id === CommonModelProviderIds.ANTHROPIC || - modelProvider.id == CommonModelProviderIds.ANTHROPIC_BEDROCK) && ( - - Note: Anthropic does not have an embeddings model and{" "} - - recommends - {" "} - Voyage AI. - - )} - {revealModelProvider.isLoading ? ( - - ) : ( - - )} - - ); -} diff --git a/ui/admin/app/components/model-providers/ModelProviderDeconfigure.tsx b/ui/admin/app/components/model-providers/ModelProviderDeconfigure.tsx deleted file mode 100644 index b7b6794b4..000000000 --- a/ui/admin/app/components/model-providers/ModelProviderDeconfigure.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { useState } from "react"; -import { toast } from "sonner"; -import { mutate } from "swr"; - -import { ModelProvider } from "~/lib/model/modelProviders"; -import { ModelApiService } from "~/lib/service/api/modelApiService"; -import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService"; - -import { Button } from "~/components/ui/button"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "~/components/ui/dialog"; -import { DropdownMenuItem } from "~/components/ui/dropdown-menu"; -import { useAsync } from "~/hooks/useAsync"; - -export function ModelProviderDeconfigure({ - modelProvider, -}: { - modelProvider: ModelProvider; -}) { - const [open, setOpen] = useState(false); - const handleDeconfigure = async () => { - deconfigure.execute(modelProvider.id); - }; - - const deconfigure = useAsync( - ModelProviderApiService.deconfigureModelProviderById, - { - onSuccess: () => { - toast.success(`${modelProvider.name} deconfigured.`); - mutate(ModelProviderApiService.getModelProviders.key()); - mutate(ModelApiService.getModels.key()); - }, - onError: () => toast.error(`Failed to deconfigure ${modelProvider.name}`), - } - ); - - return ( - - - { - event.preventDefault(); - setOpen(true); - }} - className="text-destructive" - > - Deconfigure Model Provider - - - - - - - - Deconfigure {modelProvider.name} - -

- 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 {modelProvider.name}? -

- - -
- - - - - - -
-
-
-
- ); -} diff --git a/ui/admin/app/components/model-providers/ModelProviderDropdown.tsx b/ui/admin/app/components/model-providers/ModelProviderDropdown.tsx deleted file mode 100644 index e7ca3d378..000000000 --- a/ui/admin/app/components/model-providers/ModelProviderDropdown.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { EllipsisVerticalIcon } from "lucide-react"; - -import { ModelProvider } from "~/lib/model/modelProviders"; - -import { ModelProviderDeconfigure } from "~/components/model-providers/ModelProviderDeconfigure"; -import { Button } from "~/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "~/components/ui/dropdown-menu"; - -export function ModelProviderMenu({ - modelProvider, -}: { - modelProvider: ModelProvider; -}) { - return ( - - - - - - - {modelProvider.name} - - - - - - ); -} 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 69e6442c8..000000000 --- a/ui/admin/app/components/model-providers/ModelProviderForm.tsx +++ /dev/null @@ -1,298 +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 { - 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", - "OBOT_XAI_MODEL_PROVIDER", - "OBOT_DEEPSEEK_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 434d4ae29..000000000 --- a/ui/admin/app/components/model-providers/ModelProviderIcon.tsx +++ /dev/null @@ -1,33 +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"; -}) { - const ignoreDarkModeSet = new Set([ - CommonModelProviderIds.AZURE_OPENAI, - CommonModelProviderIds.DEEPSEEK, - ]); - - return modelProvider.icon ? ( - {modelProvider.name} - ) : ( - - ); -} diff --git a/ui/admin/app/components/model-providers/ModelProviderLists.tsx b/ui/admin/app/components/model-providers/ModelProviderLists.tsx deleted file mode 100644 index b12c93b12..000000000 --- a/ui/admin/app/components/model-providers/ModelProviderLists.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { CircleCheckIcon, CircleSlashIcon } from "lucide-react"; -import { Link } from "react-router"; - -import { ModelProvider } from "~/lib/model/modelProviders"; - -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 { - ModelProviderLinks, - RecommendedModelProviders, -} from "~/components/model-providers/constants"; -import { Badge } from "~/components/ui/badge"; -import { Card, CardContent, CardHeader } from "~/components/ui/card"; - -export function ModelProviderList({ - modelProviders, -}: { - modelProviders: ModelProvider[]; -}) { - return ( -
-
- {modelProviders.map((modelProvider) => ( - - - {RecommendedModelProviders.includes(modelProvider.id) && ( - Recommended - )} - {modelProvider.configured ? ( -
- - -
- ) : ( -
- )} - - - - - -
- {modelProvider.name} -
- - - {modelProvider.configured ? ( - - {" "} - Configured - - ) : ( - - - Not Configured - - )} - - -
- - ))} -
-
- ); -} diff --git a/ui/admin/app/components/sidebar/Sidebar.tsx b/ui/admin/app/components/sidebar/Sidebar.tsx index 3731a23b3..d0dd5d703 100644 --- a/ui/admin/app/components/sidebar/Sidebar.tsx +++ b/ui/admin/app/components/sidebar/Sidebar.tsx @@ -1,13 +1,14 @@ import { - BotIcon, - BoxesIcon, - InfoIcon, - KeyIcon, - MessageSquare, - PuzzleIcon, - User, - WebhookIcon, - Wrench, + BotIcon, + BoxesIcon, + InfoIcon, + KeyIcon, + LockIcon, + MessageSquare, + PuzzleIcon, + User, + WebhookIcon, + Wrench, } from "lucide-react"; import { Link, useLocation } from "react-router"; import { $path } from "safe-routes"; @@ -40,46 +41,51 @@ import { // Menu items. const items = [ - { - title: "Agents", - url: $path("/agents"), - icon: BotIcon, - }, - { - title: "Threads", - url: $path("/threads"), - icon: MessageSquare, - }, - { - title: "Tools", - url: $path("/tools"), - icon: Wrench, - }, - { - title: "Users", - url: $path("/users"), - icon: User, - }, - { - title: "OAuth Apps", - url: $path("/oauth-apps"), - icon: KeyIcon, - }, - { - title: "Workflows", - url: $path("/workflows"), - icon: PuzzleIcon, - }, - { - title: "Model Providers", - url: $path("/model-providers"), - icon: BoxesIcon, - }, - { - title: "Workflow Triggers", - url: $path("/workflow-triggers"), - icon: WebhookIcon, - }, + { + title: "Agents", + url: $path("/agents"), + icon: BotIcon, + }, + { + title: "Threads", + url: $path("/threads"), + icon: MessageSquare, + }, + { + title: "Tools", + url: $path("/tools"), + icon: Wrench, + }, + { + title: "Users", + url: $path("/users"), + icon: User, + }, + { + title: "OAuth Apps", + url: $path("/oauth-apps"), + icon: KeyIcon, + }, + { + title: "Workflows", + url: $path("/workflows"), + icon: PuzzleIcon, + }, + { + title: "Model Providers", + url: $path("/model-providers"), + icon: BoxesIcon, + }, + { + title: "Workflow Triggers", + url: $path("/workflow-triggers"), + 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 ba7c5cad7..2ad0702c2 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,36 +12,49 @@ import { CardHeader, CardTitle, } from "~/components/ui/card"; +import { useAuthProviders } from "~/hooks/auth-providers/useAuthProviders"; interface SignInProps { className?: string; } export function SignIn({ className }: SignInProps) { - return ( -
- - - - - - - Please sign in using the button below. - - - - - - -
- ); + const { authProviders } = useAuthProviders(); + const configuredAuthProviders = authProviders.filter((p) => p.configured); + + return ( +
+ + + + + + {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 30cf82168..31debeccc 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 { roleLabel } from "~/lib/model/users"; +import { ApiRoutes } from "~/lib/routers/apiRoutes"; import { cn } from "~/lib/utils"; import { useAuth } from "~/components/auth/AuthContext"; @@ -31,37 +32,44 @@ export const UserMenu: React.FC = ({ return null; } - return ( - - - - - - - - - - {!avatarOnly && ( -
-

{me?.email}

-

- {roleLabel(me?.role)} -

-
- )} -
-
- - - { - window.location.href = "/oauth2/sign_out?rd=/admin/"; - }} - > - Sign Out - - - -
- ); + return ( + + + + + + + + + + {!avatarOnly && ( +
+

+ {me?.email} +

+

+ {roleLabel(me?.role)} +

+
+ )} +
+
+ + + { + await fetch(ApiRoutes.bootstrap.logout().url, { + method: "POST", + }); + + window.location.href = + "/oauth2/sign_out?rd=/admin/"; + }} + > + Sign Out + + + +
+ ); }; 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/modelProviders.ts b/ui/admin/app/lib/model/modelProviders.ts deleted file mode 100644 index 1c7263e91..000000000 --- a/ui/admin/app/lib/model/modelProviders.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { EntityMeta } from "~/lib/model/primitives"; - -export type ModelProviderStatus = { - configured: boolean; - modelsBackPopulated?: boolean; - icon?: string; - requiredConfigurationParameters?: string[]; - missingConfigurationParameters?: string[]; -}; - -export type ModelProvider = EntityMeta & - ModelProviderStatus & { - toolReference: string; - name: string; - revision: string; - }; - -export type ModelProviderConfig = Record; diff --git a/ui/admin/app/lib/model/models.ts b/ui/admin/app/lib/model/models.ts index f27c05617..ea9a4ffc2 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/providers.ts b/ui/admin/app/lib/model/providers.ts new file mode 100644 index 000000000..1ecf9ca0c --- /dev/null +++ b/ui/admin/app/lib/model/providers.ts @@ -0,0 +1,24 @@ +import { EntityMeta } from "~/lib/model/primitives"; + +export type ProviderStatus = { + configured: boolean; + icon?: string; + requiredConfigurationParameters?: string[]; + optionalConfigurationParameters?: string[]; + missingConfigurationParameters?: string[]; +}; + +export type Provider = EntityMeta & + ProviderStatus & { + toolReference: string; + name: string; + revision: string; + }; + +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 6b57b3251..feef58e9b 100644 --- a/ui/admin/app/lib/routers/apiRoutes.ts +++ b/ui/admin/app/lib/routers/apiRoutes.ts @@ -306,6 +306,23 @@ export const ApiRoutes = { deleteEmailReceiver: (id: string) => buildUrl(`/email-receivers/${id}`), }, version: () => buildUrl("/version"), + 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"), + }, }; /** revalidates the cache for all routes that match the filter callback 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 e7a24a508..c8bd70fc0 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,67 +13,67 @@ const getModelProviders = async () => { getModelProviders.key = () => ({ url: ApiRoutes.modelProviders.getModelProviders().path }) as const; -const getModelProviderById = async (modelProviderKey: string) => { - const res = await request({ - url: ApiRoutes.modelProviders.getModelProviderById(modelProviderKey).url, - method: "GET", - errorMessage: - "Failed to update configuration values on the requested modal provider.", - }); +const getModelProviderById = async (providerKey: string) => { + const res = await request({ + url: ApiRoutes.modelProviders.getModelProviderById(providerKey).url, + method: "GET", + errorMessage: + "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, - }; + return { + 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, - method: "POST", - data: modelProviderConfig, - errorMessage: - "Failed to update configuration values on the requested modal provider.", - }); + const res = await request({ + url: ApiRoutes.modelProviders.configureModelProviderById(providerKey) + .url, + method: "POST", + data: providerConfig, + errorMessage: + "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, - method: "POST", - errorMessage: - "Failed to reveal configuration values on the requested modal provider.", - }); +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 model provider.", + }); - return res.data; + 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, - }; + return { + url: ApiRoutes.modelProviders.revealModelProviderById(providerId).path, + providerId, + }; }; -const deconfigureModelProviderById = async (modelProviderKey: string) => { - const res = await request({ - url: ApiRoutes.modelProviders.deconfigureModelProviderById(modelProviderKey) - .url, - method: "POST", - errorMessage: "Failed to deconfigure the requested modal provider.", - }); +const deconfigureModelProviderById = async (providerKey: string) => { + const res = await request({ + url: ApiRoutes.modelProviders.deconfigureModelProviderById(providerKey) + .url, + method: "POST", + 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 fe56a9674..353abd836 100644 --- a/ui/admin/app/routes/_auth.model-providers.tsx +++ b/ui/admin/app/routes/_auth.model-providers.tsx @@ -1,14 +1,14 @@ 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 { 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/admin/app/routes/_auth.tsx b/ui/admin/app/routes/_auth.tsx index 3012fbfc9..b870bbe52 100644 --- a/ui/admin/app/routes/_auth.tsx +++ b/ui/admin/app/routes/_auth.tsx @@ -1,3 +1,4 @@ +import { AxiosError } from "axios"; import { Outlet, isRouteErrorResponse, useRouteError } from "react-router"; import { preload } from "swr"; @@ -44,14 +45,16 @@ export function ErrorBoundary() { const error = useRouteError(); const { isSignedIn } = useAuth(); - switch (true) { - case error instanceof UnauthorizedError: - case error instanceof ForbiddenError: - if (isSignedIn) return ; - else return ; - case isRouteErrorResponse(error): - return ; - default: - return ; - } + switch (true) { + case error instanceof UnauthorizedError: + case error instanceof ForbiddenError: + case error instanceof AxiosError && + [401, 403].includes(error.response?.status ?? 0): + if (isSignedIn) return ; + else return ; + case isRouteErrorResponse(error): + return ; + default: + return ; + } } 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 3c206d4ab..e16697f21 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; @@ -63,17 +67,29 @@ {/if}
-
diff --git a/ui/user/src/routes/[agent]/+page.svelte b/ui/user/src/routes/[agent]/+page.svelte index 45c4919f6..baefaef61 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 = '/'; } });