From 815fff608c4ceb1dce902e27a08643b5b4d3452d Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Wed, 15 Jan 2025 16:21:18 -0500 Subject: [PATCH 1/6] feat: auth providers Signed-off-by: Grant Linville --- apiclient/types/authprovider.go | 23 ++ apiclient/types/toolreference.go | 1 + apiclient/types/zz_generated.deepcopy.go | 85 ++++++ go.mod | 40 +-- go.sum | 111 +------ pkg/accesstoken/accesstoken.go | 14 + pkg/api/authz/authz.go | 11 +- pkg/api/handlers/authprovider.go | 243 ++++++++++++++++ pkg/api/request.go | 18 +- pkg/api/router/router.go | 19 +- pkg/api/server/server.go | 32 +-- pkg/bootstrap/bootstrap.go | 117 ++++++++ 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 | 12 +- 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 | 51 ++-- pkg/storage/apis/obot.obot.ai/v1/run.go | 1 + .../openapi/generated/openapi_generated.go | 167 +++++++++++ .../AuthProviderLists.tsx | 60 ++++ .../auth-and-model-providers/Bootstrap.tsx | 61 ++++ .../ModelProviderLists.tsx | 18 +- .../ModelProviderModels.tsx | 10 +- .../ModelProviderTooltip.tsx | 0 .../ProviderConfigure.tsx} | 115 +++++--- .../ProviderDeconfigure.tsx | 110 +++++++ .../ProviderForm.tsx} | 272 ++++++++++-------- .../auth-and-model-providers/ProviderIcon.tsx | 35 +++ .../ProviderMenu.tsx} | 14 +- .../constants.ts | 82 +++++- ui/admin/app/components/chat/Chatbar.tsx | 2 +- ui/admin/app/components/chat/RunWorkflow.tsx | 2 +- .../ModelProviderDeconfigure.tsx | 96 ------- .../model-providers/ModelProviderIcon.tsx | 33 --- ui/admin/app/components/sidebar/Sidebar.tsx | 6 + ui/admin/app/components/signin/SignIn.tsx | 43 +-- ui/admin/app/components/user/UserMenu.tsx | 7 +- .../hooks/auth-providers/useAuthProviders.tsx | 14 + ui/admin/app/lib/model/models.ts | 10 - .../model/{modelProviders.ts => providers.ts} | 16 +- ui/admin/app/lib/routers/apiRoutes.ts | 17 ++ .../lib/service/api/authProviderApiService.ts | 89 ++++++ .../service/api/modelProviderApiService.ts | 50 ++-- ui/admin/app/lib/service/routeService.ts | 5 + ui/admin/app/routes/_auth.auth-providers.tsx | 71 +++++ ui/admin/app/routes/_auth.model-providers.tsx | 6 +- ui/admin/app/routes/_auth.tsx | 3 + ui/user/src/lib/auth.ts | 13 + ui/user/src/lib/stores/profile.ts | 2 +- ui/user/src/routes/+page.svelte | 41 ++- ui/user/src/routes/[agent]/+page.svelte | 3 +- 71 files changed, 2257 insertions(+), 1525 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 rename ui/admin/app/components/{model-providers => auth-and-model-providers}/ModelProviderLists.tsx (73%) rename ui/admin/app/components/{model-providers => auth-and-model-providers}/ModelProviderModels.tsx (92%) rename ui/admin/app/components/{model-providers => auth-and-model-providers}/ModelProviderTooltip.tsx (100%) rename ui/admin/app/components/{model-providers/ModelProviderConfigure.tsx => auth-and-model-providers/ProviderConfigure.tsx} (52%) create mode 100644 ui/admin/app/components/auth-and-model-providers/ProviderDeconfigure.tsx rename ui/admin/app/components/{model-providers/ModelProviderForm.tsx => auth-and-model-providers/ProviderForm.tsx} (52%) create mode 100644 ui/admin/app/components/auth-and-model-providers/ProviderIcon.tsx rename ui/admin/app/components/{model-providers/ModelProviderDropdown.tsx => auth-and-model-providers/ProviderMenu.tsx} (64%) rename ui/admin/app/components/{model-providers => auth-and-model-providers}/constants.ts (63%) delete mode 100644 ui/admin/app/components/model-providers/ModelProviderDeconfigure.tsx delete mode 100644 ui/admin/app/components/model-providers/ModelProviderIcon.tsx create mode 100644 ui/admin/app/hooks/auth-providers/useAuthProviders.tsx rename ui/admin/app/lib/model/{modelProviders.ts => providers.ts} (51%) 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 126c027dd..7c4510ecc 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 60ae9ec8d..e6a03b870 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( github.com/gptscript-ai/gptscript v0.9.6-0.20250120172457-3f876b2ef42b 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-20250116162656-270198b40c6d github.com/obot-platform/nah v0.0.0-20250116162537-3bafada8cfb4 github.com/obot-platform/namegenerator v0.0.0-20241217121223-fc58bdb7dca2 @@ -56,9 +55,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 @@ -67,22 +63,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 @@ -93,12 +85,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 @@ -115,15 +105,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 @@ -138,13 +125,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 @@ -153,7 +137,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 @@ -167,7 +150,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 @@ -176,17 +158,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 @@ -201,11 +180,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 @@ -215,21 +194,14 @@ 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/stretchr/objx v0.5.2 // indirect @@ -239,8 +211,6 @@ require ( 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 @@ -253,7 +223,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 @@ -275,14 +244,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 4c7647a79..4aa859058 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-20250116162656-270198b40c6d h1:GzMvRkssr4jAa2YvQiv9eXhjuNpaZVab3GajE7+cQ3s= github.com/obot-platform/kinm v0.0.0-20250116162656-270198b40c6d/go.mod h1:RzrH0geIlbiTHDGZ8bpCk5k1hwdU9uu3l4zJn9n0pZU= github.com/obot-platform/nah v0.0.0-20250116162537-3bafada8cfb4 h1:T5m+KSIXx0wTL1HdWngu1I6sCWI3tdfcJ+4Ud/vkHu4= github.com/obot-platform/nah v0.0.0-20250116162537-3bafada8cfb4/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/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw= @@ -613,20 +538,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= @@ -647,8 +564,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= @@ -665,10 +580,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= @@ -697,8 +608,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= @@ -719,8 +628,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= @@ -762,7 +669,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= @@ -812,7 +718,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= @@ -820,7 +725,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= @@ -869,13 +773,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= @@ -886,8 +789,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= @@ -901,7 +802,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= @@ -952,8 +852,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= @@ -983,11 +881,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= @@ -996,7 +892,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= @@ -1011,8 +906,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 4691a648c..c55c92874 100644 --- a/pkg/api/authz/authz.go +++ b/pkg/api/authz/authz.go @@ -36,10 +36,10 @@ 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}", @@ -49,9 +49,12 @@ var staticRules = map[string][]string{ "POST /api/sendgrid", "GET /api/healthz", + + "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..292564d06 --- /dev/null +++ b/pkg/api/handlers/authprovider.go @@ -0,0 +1,243 @@ +package handlers + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "slices" + "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" +) + +const cookieSecretEnvVar = "OBOT_AUTH_PROVIDER_COOKIE_SECRET" + +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 != "" { + 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 + } + + cookieSecret, err := generateCookieSecret() + if err != nil { + return err + } + envVars[cookieSecretEnvVar] = cookieSecret + + // 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) + } + + 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"], ",") + + // Remove the cookie secret environment variable if it's there. + idx := slices.Index(requiredEnvVars, cookieSecretEnvVar) + if idx != -1 { + requiredEnvVars = append(requiredEnvVars[:idx], requiredEnvVars[idx+1:]...) + } + } + + 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, + } +} + +func generateCookieSecret() (string, error) { + const length = 32 + + var bytes = make([]byte, length) + _, err := rand.Read(bytes) + if err != nil { + return "", fmt.Errorf("failed to generate random token: %w", err) + } + + return base64.StdEncoding.EncodeToString(bytes), nil +} 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 810b6f1f3..cb0efda41 100644 --- a/pkg/api/router/router.go +++ b/pkg/api/router/router.go @@ -25,8 +25,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, services.Invoker) + availableModels := handlers.NewAvailableModelsHandler(services.GPTClient, services.ProviderDispatcher) + modelProviders := handlers.NewModelProviderHandler(services.GPTClient, services.ProviderDispatcher, services.Invoker) + authProviders := handlers.NewAuthProviderHandler(services.GPTClient, services.ProviderDispatcher) prompt := handlers.NewPromptHandler(services.GPTClient) emailreceiver := handlers.NewEmailReceiverHandler(services.EmailServerName) defaultModelAliases := handlers.NewDefaultModelAliasHandler() @@ -36,6 +37,7 @@ func Router(services *services.Services) (http.Handler, error) { sendgridWebhookHandler := sendgrid.NewInboundWebhookHandler(services.StorageClient, services.EmailServerName, services.SendgridWebhookUsername, services.SendgridWebhookPassword) // Version + mux.HandleFunc("GET /api/version", version.GetVersion) // Agents @@ -301,6 +303,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) @@ -325,6 +337,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..b55bfa99a 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,35 +56,20 @@ 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 { http.Error(rw, err.Error(), http.StatusUnauthorized) 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")) { + if strings.HasPrefix(req.URL.Path, "/api/") { + if !s.authorizer.Authorize(req, user) { http.Error(rw, "forbidden", http.StatusForbidden) return } - - req.Header.Set("X-Obot-Auth-Required", "true") - s.proxyServer.ServeHTTP(rw, req) - 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") } err = f(api.Context{ diff --git a/pkg/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go new file mode 100644 index 000000000..9f144ae5e --- /dev/null +++ b/pkg/bootstrap/bootstrap.go @@ -0,0 +1,117 @@ +package bootstrap + +import ( + "crypto/rand" + "fmt" + "net/http" + "os" + "strings" + + types2 "github.com/obot-platform/obot/apiclient/types" + "github.com/obot-platform/obot/pkg/api" + "github.com/obot-platform/obot/pkg/api/authz" + "github.com/obot-platform/obot/pkg/gateway/client" + "github.com/obot-platform/obot/pkg/gateway/types" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +const bootstrapCookie = "obot-bootstrap" + +type Bootstrap struct { + token, serverURL string + gatewayClient *client.Client +} + +func New(serverURL string, c *client.Client) (*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) + + // We deliberately only print the token if it was not provided by the user. + fmt.Printf("Bootstrap token: %s\nUse this token to log in to the Admin UI.\n", token) + } + + return &Bootstrap{ + token: token, + serverURL: serverURL, + gatewayClient: c, + }, 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 + } + + gatewayUser, err := b.gatewayClient.EnsureIdentityWithRole( + req.Context(), + &types.Identity{ + ProviderUsername: "bootstrap", + }, + req.Header.Get("X-Obot-User-Timezone"), + types2.RoleAdmin, + ) + if err != nil { + return nil, false, err + } + + return &authenticator.Response{ + User: &user.DefaultInfo{ + Name: "bootstrap", + UID: fmt.Sprintf("%d", gatewayUser.ID), + 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 d48d18c40..6f48a648c 100644 --- a/pkg/controller/handlers/toolreference/toolreference.go +++ b/pkg/controller/handlers/toolreference/toolreference.go @@ -44,6 +44,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 { @@ -193,6 +194,7 @@ func (h *Handler) readFromRegistry(ctx context.Context, c client.Client) error { toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeSystem, registryURL, index.System)...) toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeModelProvider, registryURL, index.ModelProviders)...) + toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeAuthProvider, registryURL, index.AuthProviders)...) toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeTool, registryURL, index.Tools)...) toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeStepTemplate, registryURL, index.StepTemplates)...) toAdd = append(toAdd, h.toolsToToolReferences(ctx, types.ToolReferenceTypeKnowledgeDataSource, registryURL, index.KnowledgeDataSources)...) diff --git a/pkg/controller/routes.go b/pkg/controller/routes.go index ef7ee5d17..075a71b1f 100644 --- a/pkg/controller/routes.go +++ b/pkg/controller/routes.go @@ -31,7 +31,7 @@ func (c *Controller) setupRoutes() error { workflowStep := workflowstep.New(c.services.Invoker) toolRef := toolreference.New( c.services.GPTClient, - c.services.ModelProviderDispatcher, + c.services.ProviderDispatcher, c.services.ToolRegistryURLs, c.services.SupportDocker, ) 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 2b896caf6..30e717506 100644 --- a/pkg/gateway/server/router.go +++ b/pkg/gateway/server/router.go @@ -23,22 +23,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..f3876acd0 100644 --- a/pkg/gateway/server/user.go +++ b/pkg/gateway/server/user.go @@ -25,8 +25,16 @@ func (s *Server) getCurrentUser(apiContext api.Context) error { return err } - if err = s.client.UpdateProfileIconIfNeeded(apiContext.Context(), user, apiContext.AuthProviderID()); err != nil { - pkgLog.Warnf("failed to update profile icon for user %s: %v", user.Username, err) + name, namespace := apiContext.AuthProviderNameAndNamespace() + + if name != "" && namespace != "" { + providerURL, err := s.dispatcher.URLForAuthProvider(apiContext.Context(), namespace, name) + if err != nil { + return fmt.Errorf("failmed 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) + } } return apiContext.Write(types.ConvertUser(user, s.client.IsExplicitAdmin(user.Email))) 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 45a69195b..869146025 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -23,6 +23,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" @@ -47,10 +48,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"` @@ -62,17 +60,19 @@ 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"` EnableSMTPServer bool `usage:"Enable SMTP server to receive emails" default:"false" env:"OBOT_ENABLE_SMTP_SERVER"` 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"` // Sendgrid webhook SendgridWebhookUsername string `usage:"The username for the sendgrid webhook to authenticate with"` SendgridWebhookPassword string `usage:"The password for the sendgrid webhook to authenticate with"` - AuthConfig GatewayConfig services.Config } @@ -91,10 +91,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 @@ -300,16 +301,21 @@ func New(ctx context.Context, config Config) (*Services, error) { tokenServer, events, ) - modelProviderDispatcher = dispatcher.New(invoker, storageClient, c) + providerDispatcher = dispatcher.New(invoker, storageClient, c) - proxyServer *proxy.Proxy + proxyManager *proxy.Manager ) + bootstrapper, err := bootstrap2.New(config.Hostname, gatewayClient) + if err != nil { + return nil, err + } + gatewayServer, err := gserver.New( ctx, gatewayDB, tokenServer, - modelProviderDispatcher, + providerDispatcher, config.AuthAdminEmails, gserver.Options(config.GatewayConfig), ) @@ -317,25 +323,19 @@ func New(ctx context.Context, config Config) (*Services, error) { return nil, err } - authProviderID, err := gatewayServer.UpsertAuthProvider(ctx, config.AuthConfigType, config.AuthClientID, config.AuthClientSecret) - 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{}) @@ -376,20 +376,21 @@ func New(ctx context.Context, config Config) (*Services, error) { c, authn.NewAuthenticator(authenticators), authz.NewAuthorizer(r.Backend()), - proxyServer, + proxyManager, config.Hostname, ), TokenServer: tokenServer, Invoker: invoker, GatewayServer: gatewayServer, GatewayClient: gatewayClient, - ProxyServer: proxyServer, KnowledgeSetIngestionLimit: config.KnowledgeSetIngestionLimit, EmailServerName: config.EmailServerName, - ModelProviderDispatcher: modelProviderDispatcher, SupportDocker: config.Docker, SendgridWebhookUsername: config.SendgridWebhookUsername, SendgridWebhookPassword: config.SendgridWebhookPassword, + 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 24327505f..7fe067692 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..6db301949 --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/AuthProviderLists.tsx @@ -0,0 +1,60 @@ +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..4d9509253 --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/Bootstrap.tsx @@ -0,0 +1,61 @@ +import React, { useState } from "react"; + +import { ApiRoutes } from "~/lib/routers/apiRoutes"; +import { cn } from "~/lib/utils"; + +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

+ setToken(e.target.value)} + placeholder="token" + className="rounded border p-2" + required + /> + + {error && {error}} +
+ ); +} diff --git a/ui/admin/app/components/model-providers/ModelProviderLists.tsx b/ui/admin/app/components/auth-and-model-providers/ModelProviderLists.tsx similarity index 73% rename from ui/admin/app/components/model-providers/ModelProviderLists.tsx rename to ui/admin/app/components/auth-and-model-providers/ModelProviderLists.tsx index 6af3bd073..e72b2b3f8 100644 --- a/ui/admin/app/components/model-providers/ModelProviderLists.tsx +++ b/ui/admin/app/components/auth-and-model-providers/ModelProviderLists.tsx @@ -1,16 +1,16 @@ import { CircleCheckIcon, CircleSlashIcon } from "lucide-react"; import { Link } from "react-router"; -import { ModelProvider } from "~/lib/model/modelProviders"; +import { ModelProvider } from "~/lib/model/providers"; -import { ModelProviderConfigure } from "~/components/model-providers/ModelProviderConfigure"; -import { ModelProviderMenu } from "~/components/model-providers/ModelProviderDropdown"; -import { ModelProviderIcon } from "~/components/model-providers/ModelProviderIcon"; -import { ModelProvidersModels } from "~/components/model-providers/ModelProviderModels"; +import { ModelProvidersModels } from "~/components/auth-and-model-providers/ModelProviderModels"; +import { ProviderConfigure } from "~/components/auth-and-model-providers/ProviderConfigure"; +import { ProviderIcon } from "~/components/auth-and-model-providers/ProviderIcon"; +import { ProviderMenu } from "~/components/auth-and-model-providers/ProviderMenu"; import { ModelProviderLinks, RecommendedModelProviders, -} from "~/components/model-providers/constants"; +} from "~/components/auth-and-model-providers/constants"; import { Badge } from "~/components/ui/badge"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; @@ -33,7 +33,7 @@ export function ModelProviderList({ {modelProvider.configured ? (
- +
) : (
@@ -41,7 +41,7 @@ export function ModelProviderList({ - +
{modelProvider.name} @@ -60,7 +60,7 @@ export function ModelProviderList({ )} - + ))} diff --git a/ui/admin/app/components/model-providers/ModelProviderModels.tsx b/ui/admin/app/components/auth-and-model-providers/ModelProviderModels.tsx similarity index 92% rename from ui/admin/app/components/model-providers/ModelProviderModels.tsx rename to ui/admin/app/components/auth-and-model-providers/ModelProviderModels.tsx index 4526a751a..d77c1e5c1 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"; @@ -79,7 +79,7 @@ export function ModelProvidersModels({ modelProvider }: ModelsConfigureProps) { - {" "} - {modelProvider.name} Models + {modelProvider.name}{" "} + Models diff --git a/ui/admin/app/components/model-providers/ModelProviderTooltip.tsx b/ui/admin/app/components/auth-and-model-providers/ModelProviderTooltip.tsx similarity index 100% rename from ui/admin/app/components/model-providers/ModelProviderTooltip.tsx rename to ui/admin/app/components/auth-and-model-providers/ModelProviderTooltip.tsx diff --git a/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderConfigure.tsx similarity index 52% rename from ui/admin/app/components/model-providers/ModelProviderConfigure.tsx rename to ui/admin/app/components/auth-and-model-providers/ProviderConfigure.tsx index 7f8746d52..b006d8dbb 100644 --- a/ui/admin/app/components/model-providers/ModelProviderConfigure.tsx +++ b/ui/admin/app/components/auth-and-model-providers/ProviderConfigure.tsx @@ -1,14 +1,16 @@ import { useEffect, useState } from "react"; import useSWR, { mutate } from "swr"; -import { ModelProvider } from "~/lib/model/modelProviders"; +import { AuthProvider, ModelProvider } from "~/lib/model/providers"; import { NotFoundError } from "~/lib/service/api/apiErrors"; +import { AuthProviderApiService } from "~/lib/service/api/authProviderApiService"; import { ModelApiService } from "~/lib/service/api/modelApiService"; import { ModelProviderApiService } from "~/lib/service/api/modelProviderApiService"; -import { ModelProviderForm } from "~/components/model-providers/ModelProviderForm"; -import { ModelProviderIcon } from "~/components/model-providers/ModelProviderIcon"; -import { CommonModelProviderIds } from "~/components/model-providers/constants"; +import { ProviderForm } from "~/components/auth-and-model-providers/ProviderForm"; +import { ProviderIcon } from "~/components/auth-and-model-providers/ProviderIcon"; +import { CommonModelProviderIds } from "~/components/auth-and-model-providers/constants"; +import { CopyText } from "~/components/composed/CopyText"; import { DefaultModelAliasForm } from "~/components/model/DefaultModelAliasForm"; import { LoadingSpinner } from "~/components/ui/LoadingSpinner"; import { Button } from "~/components/ui/button"; @@ -22,23 +24,23 @@ import { } from "~/components/ui/dialog"; import { Link } from "~/components/ui/link"; -type ModelProviderConfigureProps = { - modelProvider: ModelProvider; +type ProviderConfigureProps = { + provider: ModelProvider | AuthProvider; }; -export function ModelProviderConfigure({ - modelProvider, -}: ModelProviderConfigureProps) { +export function ProviderConfigure({ provider }: ProviderConfigureProps) { const [dialogIsOpen, setDialogIsOpen] = useState(false); const [showDefaultModelAliasForm, setShowDefaultModelAliasForm] = useState(false); - const [loadingModelProviderId, setLoadingModelProviderId] = useState(""); + const [loadingProviderId, setLoadingProviderId] = useState(""); const getLoadingModelProviderModels = useSWR( - ModelProviderApiService.getModelProviderById.key(loadingModelProviderId), - ({ modelProviderId }) => - ModelProviderApiService.getModelProviderById(modelProviderId), + provider.type === "modelprovider" + ? ModelProviderApiService.getModelProviderById.key(loadingProviderId) + : null, + ({ providerId }) => + ModelProviderApiService.getModelProviderById(providerId), { revalidateOnFocus: false, refreshInterval: 2000, @@ -46,18 +48,18 @@ export function ModelProviderConfigure({ ); useEffect(() => { - if (!loadingModelProviderId) return; + if (!loadingProviderId) return; const { isLoading, data } = getLoadingModelProviderModels; if (isLoading) return; if (data?.modelsBackPopulated) { setShowDefaultModelAliasForm(true); - setLoadingModelProviderId(""); + setLoadingProviderId(""); // revalidate models to get back populated models mutate(ModelApiService.getModels.key()); } - }, [getLoadingModelProviderModels, loadingModelProviderId]); + }, [getLoadingModelProviderModels, loadingProviderId]); const handleDone = () => { setDialogIsOpen(false); @@ -68,22 +70,22 @@ export function ModelProviderConfigure({ - + - {loadingModelProviderId ? ( + {loadingProviderId ? (
- Loading {modelProvider.name} Models... + Loading {provider.name} Models...
) : showDefaultModelAliasForm ? (
@@ -102,9 +104,13 @@ export function ModelProviderConfigure({
) : ( - setLoadingModelProviderId(modelProvider.id)} + + provider.type === "modelprovider" + ? setLoadingProviderId(provider.id) + : setDialogIsOpen(false) + } /> )} @@ -112,20 +118,23 @@ export function ModelProviderConfigure({ ); } -export function ModelProviderConfigureContent({ - modelProvider, +export function ProviderConfigureContent({ + provider, onSuccess, }: { - modelProvider: ModelProvider; + provider: ModelProvider | AuthProvider; onSuccess: () => void; }) { - const revealModelProvider = useSWR( - ModelProviderApiService.revealModelProviderById.key(modelProvider.id), - async ({ modelProviderId }) => { + const revealByIdFunc = + provider.type === "modelprovider" + ? ModelProviderApiService.revealModelProviderById + : AuthProviderApiService.revealAuthProviderById; + + const revealProvider = useSWR( + revealByIdFunc.key(provider.id), + async ({ providerId }) => { try { - return await ModelProviderApiService.revealModelProviderById( - modelProviderId - ); + return await revealByIdFunc(providerId); } catch (error) { // 404: no credential found = just return empty object if (error instanceof NotFoundError) { @@ -137,22 +146,23 @@ export function ModelProviderConfigureContent({ } ); - const requiredParameters = modelProvider.requiredConfigurationParameters; - const parameters = revealModelProvider.data; + const requiredParameters = provider.requiredConfigurationParameters; + const optionalParameters = provider.optionalConfigurationParameters; + const parameters = revealProvider.data; return ( <> - {" "} - {modelProvider.configured - ? `Configure ${modelProvider.name}` - : `Set Up ${modelProvider.name}`} + {" "} + {provider.configured + ? `Configure ${provider.name}` + : `Set Up ${provider.name}`} - {(modelProvider.id === CommonModelProviderIds.ANTHROPIC || - modelProvider.id == CommonModelProviderIds.ANTHROPIC_BEDROCK) && ( + {(provider.id === CommonModelProviderIds.ANTHROPIC || + provider.id == CommonModelProviderIds.ANTHROPIC_BEDROCK) && ( Note: Anthropic does not have an embeddings model and{" "} )} - {revealModelProvider.isLoading ? ( + {provider.type === "authprovider" && ( + + Note: the callback URL for this auth provider is + + + )} + {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..490a6c93d --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/ProviderDeconfigure.tsx @@ -0,0 +1,110 @@ +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): 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/model-providers/ModelProviderForm.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderForm.tsx similarity index 52% rename from ui/admin/app/components/model-providers/ModelProviderForm.tsx rename to ui/admin/app/components/auth-and-model-providers/ProviderForm.tsx index 30e698c99..14a432521 100644 --- a/ui/admin/app/components/model-providers/ModelProviderForm.tsx +++ b/ui/admin/app/components/auth-and-model-providers/ProviderForm.tsx @@ -5,29 +5,28 @@ import { useFieldArray, useForm } from "react-hook-form"; import { mutate } from "swr"; import { z } from "zod"; -import { ModelProvider, ModelProviderConfig } from "~/lib/model/modelProviders"; +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 { - HelperTooltipLabel, - HelperTooltipLink, -} from "~/components/composed/HelperTooltip"; -import { - NameDescriptionForm, - ParamFormValues, -} from "~/components/composed/NameDescriptionForm"; -import { ControlledInput } from "~/components/form/controlledInputs"; -import { - ModelProviderConfigurationLinks, + AuthProviderOptionalTooltips, + AuthProviderRequiredTooltips, + AuthProviderSensitiveFields, ModelProviderRequiredTooltips, ModelProviderSensitiveFields, -} from "~/components/model-providers/constants"; +} 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 { Separator } from "~/components/ui/separator"; import { useAsync } from "~/hooks/useAsync"; const formSchema = z.object({ @@ -42,15 +41,18 @@ const formSchema = z.object({ }), }) ), - additionalConfirmParams: z.array( + optionalConfigParams: z.array( z.object({ - name: z.string(), - description: z.string(), + label: z.string(), + name: z.string().min(1, { + message: "Name is required.", + }), + value: z.string(), }) ), }); -export type ModelProviderFormValues = z.infer; +export type ProviderFormValues = z.infer; const translateUserFriendlyLabel = (label: string) => { const fieldsToStrip = [ @@ -62,9 +64,9 @@ const translateUserFriendlyLabel = (label: string) => { "OBOT_GROQ_MODEL_PROVIDER", "OBOT_VLLM_MODEL_PROVIDER", "OBOT_ANTHROPIC_BEDROCK_MODEL_PROVIDER", - "OBOT_XAI_MODEL_PROVIDER", - "OBOT_DEEPSEEK_MODEL_PROVIDER", - "OBOT_GEMINI_VERTEX_MODEL_PROVIDER", + "OBOT_AUTH_PROVIDER", + "OBOT_GOOGLE_AUTH_PROVIDER", + "OBOT_GITHUB_AUTH_PROVIDER", ]; return fieldsToStrip @@ -79,42 +81,36 @@ const translateUserFriendlyLabel = (label: string) => { const getInitialRequiredParams = ( requiredParameters: string[], - parameters: ModelProviderConfig -): ModelProviderFormValues["requiredConfigParams"] => + parameters: ProviderConfig +): ProviderFormValues["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, - })); -}; +const getInitialOptionalParams = ( + optionalParameters: string[], + parameters: ProviderConfig +): ProviderFormValues["optionalConfigParams"] => + optionalParameters.map((optionalParameterKey) => ({ + label: translateUserFriendlyLabel(optionalParameterKey), + name: optionalParameterKey, + value: parameters[optionalParameterKey] ?? "", + })); -export function ModelProviderForm({ - modelProvider, +export function ProviderForm({ + provider, onSuccess, parameters, requiredParameters, + optionalParameters, }: { - modelProvider: ModelProvider; + provider: ModelProvider | AuthProvider; onSuccess: () => void; - parameters: ModelProviderConfig; + parameters: ProviderConfig; requiredParameters: string[]; + optionalParameters: string[]; }) { const fetchAvailableModels = useAsync( ModelApiService.getAvailableModelsByProvider, @@ -126,6 +122,17 @@ export function ModelProviderForm({ } ); + const configureAuthProvider = useAsync( + AuthProviderApiService.configureAuthProviderById, + { + onSuccess: async () => { + onSuccess(); + mutate(AuthProviderApiService.getAuthProviders.key()); + mutate(AuthProviderApiService.revealAuthProviderById.key(provider.id)); + }, + } + ); + const validateAndConfigureModelProvider = useAsync( ModelProviderApiService.validateModelProviderById, { @@ -146,22 +153,22 @@ export function ModelProviderForm({ { onSuccess: async () => { mutate( - ModelProviderApiService.revealModelProviderById.key(modelProvider.id) + ModelProviderApiService.revealModelProviderById.key(provider.id) ); - await fetchAvailableModels.execute(modelProvider.id); + await fetchAvailableModels.execute(provider.id); }, } ); - const form = useForm({ + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { requiredConfigParams: getInitialRequiredParams( requiredParameters, parameters ), - additionalConfirmParams: getInitialAdditionalParams( - requiredParameters, + optionalConfigParams: getInitialOptionalParams( + optionalParameters, parameters ), }, @@ -173,72 +180,89 @@ export function ModelProviderForm({ requiredParameters, parameters ), - additionalConfirmParams: getInitialAdditionalParams( - requiredParameters, + optionalConfigParams: getInitialOptionalParams( + optionalParameters, parameters ), }); - }, [requiredParameters, parameters, form]); + }, [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: ModelProviderFormValues) => { + async (data: ProviderFormValues) => { const allConfigParams: Record = {}; - [data.requiredConfigParams, data.additionalConfirmParams].forEach( + [data.requiredConfigParams, data.optionalConfigParams].forEach( (configParams) => { for (const param of configParams) { - const paramValue = - "value" in param ? param.value : param.description; - if (paramValue && param.name) { - allConfigParams[param.name] = paramValue; + if (param.value && param.name) { + allConfigParams[param.name] = param.value; } } } ); - await validateAndConfigureModelProvider.execute( - modelProvider.id, - allConfigParams - ); + switch (provider.type) { + case "modelprovider": + await validateAndConfigureModelProvider.execute( + provider.id, + allConfigParams + ); + break; + case "authprovider": + await configureAuthProvider.execute(provider.id, allConfigParams); + break; + } } ); const FORM_ID = "model-provider-form"; - const showCustomConfiguration = - modelProvider.id === "azure-openai-model-provider"; const loading = validateAndConfigureModelProvider.isLoading || fetchAvailableModels.isLoading || configureModelProvider.isLoading || + configureAuthProvider.isLoading || isLoading; + const sensitiveFields = + provider.type === "modelprovider" + ? ModelProviderSensitiveFields + : AuthProviderSensitiveFields; + return (
- {validateAndConfigureModelProvider.error !== null && ( -
- - - An error occurred! - - Your configuration could not be saved, because it failed - validation:{" "} - - {(typeof validateAndConfigureModelProvider.error === "object" && - "message" in validateAndConfigureModelProvider.error && - (validateAndConfigureModelProvider.error - .message as string)) ?? - "Unknown error"} - - - -
- )} - {validateAndConfigureModelProvider.error === null && + {provider.type === "modelprovider" && + validateAndConfigureModelProvider.error !== null && ( +
+ + + An error occurred! + + Your configuration could not be saved, because it failed + validation:{" "} + + {(typeof validateAndConfigureModelProvider.error === + "object" && + "message" in validateAndConfigureModelProvider.error && + (validateAndConfigureModelProvider.error + .message as string)) ?? + "Unknown error"} + + + +
+ )} + {provider.type === "modelprovider" && + validateAndConfigureModelProvider.error === null && fetchAvailableModels.error !== null && (
@@ -260,17 +284,15 @@ export function ModelProviderForm({ )}
-

Required Configuration

+

Required Configuration

{requiredConfigParamFields.fields.map((field, i) => { - const type = ModelProviderSensitiveFields[field.name] - ? "password" - : "text"; + 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 ( +
+ +
+ ); + })} - - {showCustomConfiguration && renderCustomConfiguration()}
@@ -310,37 +359,22 @@ export function ModelProviderForm({
); - 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(type: string | undefined, label: string) { + const tooltip = + type === "modelprovider" + ? ModelProviderRequiredTooltips[provider.id]?.[label] + : AuthProviderRequiredTooltips[provider.id]?.[label]; + return ; } - function renderLabelWithTooltip(label: string) { - const tooltip = ModelProviderRequiredTooltips[modelProvider.id]?.[label]; + 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..8ad59b91b --- /dev/null +++ b/ui/admin/app/components/auth-and-model-providers/ProviderIcon.tsx @@ -0,0 +1,35 @@ +import { BoxesIcon } from "lucide-react"; + +import { AuthProvider, ModelProvider } from "~/lib/model/providers"; +import { cn } from "~/lib/utils"; + +import { + CommonAuthProviderIds, + CommonModelProviderIds, +} from "~/components/auth-and-model-providers/constants"; + +export function ProviderIcon({ + provider, + size = "md", +}: { + provider: ModelProvider | AuthProvider; + size?: "md" | "lg"; +}) { + return provider.icon ? ( + {provider.name} + ) : ( + + ); +} diff --git a/ui/admin/app/components/model-providers/ModelProviderDropdown.tsx b/ui/admin/app/components/auth-and-model-providers/ProviderMenu.tsx similarity index 64% rename from ui/admin/app/components/model-providers/ModelProviderDropdown.tsx rename to ui/admin/app/components/auth-and-model-providers/ProviderMenu.tsx index e7ca3d378..0cc758ce4 100644 --- a/ui/admin/app/components/model-providers/ModelProviderDropdown.tsx +++ b/ui/admin/app/components/auth-and-model-providers/ProviderMenu.tsx @@ -1,8 +1,8 @@ import { EllipsisVerticalIcon } from "lucide-react"; -import { ModelProvider } from "~/lib/model/modelProviders"; +import { AuthProvider, ModelProvider } from "~/lib/model/providers"; -import { ModelProviderDeconfigure } from "~/components/model-providers/ModelProviderDeconfigure"; +import { ProviderDeconfigure } from "~/components/auth-and-model-providers/ProviderDeconfigure"; import { Button } from "~/components/ui/button"; import { DropdownMenu, @@ -13,10 +13,10 @@ import { DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; -export function ModelProviderMenu({ - modelProvider, +export function ProviderMenu({ + provider, }: { - modelProvider: ModelProvider; + provider: ModelProvider | AuthProvider; }) { return ( @@ -27,9 +27,9 @@ export function ModelProviderMenu({ - {modelProvider.name} + {provider.name} - + diff --git a/ui/admin/app/components/model-providers/constants.ts b/ui/admin/app/components/auth-and-model-providers/constants.ts similarity index 63% rename from ui/admin/app/components/model-providers/constants.ts rename to ui/admin/app/components/auth-and-model-providers/constants.ts index 50664e8ff..e4f124ab2 100644 --- a/ui/admin/app/components/model-providers/constants.ts +++ b/ui/admin/app/components/auth-and-model-providers/constants.ts @@ -28,11 +28,6 @@ export const ModelProviderLinks = { [CommonModelProviderIds.GEMINI_VERTEX]: "https://cloud.google.com/vertex-ai", }; -export const ModelProviderConfigurationLinks = { - [CommonModelProviderIds.AZURE_OPENAI]: - "https://docs.obot.ai/configuration/model-providers#azure-openai", -}; - export const RecommendedModelProviders = [ CommonModelProviderIds.OPENAI, CommonModelProviderIds.AZURE_OPENAI, @@ -132,3 +127,80 @@ export const ModelProviderSensitiveFields: Record = OBOT_GEMINI_VERTEX_MODEL_PROVIDER_GOOGLE_CREDENTIALS_JSON: true, OBOT_GEMINI_VERTEX_MODEL_PROVIDER_GOOGLE_CLOUD_PROJECT: false, }; + +export const CommonAuthProviderIds = { + GOOGLE: "google-auth-provider", + GITHUB: "github-auth-provider", +}; + +export const CommonAuthProviderFriendlyNames: Record = { + "google-auth-provider": "Google", + "github-auth-provider": "GitHub", +}; + +export const AuthProviderLinks = { + [CommonAuthProviderIds.GOOGLE]: "https://google.com", + [CommonAuthProviderIds.GITHUB]: "https://github.com", +}; + +export const AuthProviderRequiredTooltips: { + [key: string]: { + [key: string]: string; + }; +} = { + [CommonAuthProviderIds.GOOGLE]: { + "Client Id": + "Unique identifier for the application when using Google's OAuth. Can typically be found in Google Cloud Console > Credentials", + "Client Secret": + "Password or key that app uses to authenticate with Google's OAuth. Can typically be found in Google Cloud Console > Credentials", + "Cookie Secret": + "Secret used to encrypt cookies. Must be a random string of length 16, 24, or 32.", + "Email Domains": + "Comma separated list of email domains that are allowed to authenticate with this provider. * is a special value that allows all domains.", + }, + [CommonAuthProviderIds.GITHUB]: { + "Client Id": + "Client ID for your GitHub OAuth app. Can be found in GitHub Developer Settings > OAuth Apps", + "Client Secret": + "Client secret for your GitHub OAuth app. Can be found in GitHub Developer Settings > OAuth Apps", + "Cookie Secret": + "Secret used to encrypt cookies. Must be a random string of length 16, 24, or 32.", + "Email Domains": + "Comma separated list of email domains that are allowed to authenticate with this provider. * is a special value that allows all domains.", + }, +}; + +export const AuthProviderOptionalTooltips: { + [key: string]: { + [key: string]: string; + }; +} = { + [CommonAuthProviderIds.GITHUB]: { + Teams: + "Restrict logins to members of any of these GitHub teams (comma-separated list).", + Org: "Restrict logins to members of this GitHub organization.", + Repo: "Restrict logins to collaborators on this GitHub repository (formatted orgname/repo).", + Token: + "The token to use when verifying repository collaborators (must have push access to the repository).", + "Allow Users": + "Users allowed to log in, even if they do not belong to the specified org and team or collaborators.", + }, +}; + +export const AuthProviderSensitiveFields: Record = + { + // All + OBOT_AUTH_PROVIDER_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 494506a69..0bdb8c1f7 100644 --- a/ui/admin/app/components/chat/Chatbar.tsx +++ b/ui/admin/app/components/chat/Chatbar.tsx @@ -3,10 +3,10 @@ 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 { ChatRunInfo } from "~/components/chat/ChatRunInfo"; -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/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/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/sidebar/Sidebar.tsx b/ui/admin/app/components/sidebar/Sidebar.tsx index 6cc6309b6..c47daca32 100644 --- a/ui/admin/app/components/sidebar/Sidebar.tsx +++ b/ui/admin/app/components/sidebar/Sidebar.tsx @@ -2,6 +2,7 @@ import { BotIcon, BoxesIcon, InfoIcon, + LockIcon, MessageSquare, PuzzleIcon, User, @@ -74,6 +75,11 @@ const items = [ 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..06baa8520 100644 --- a/ui/admin/app/components/signin/SignIn.tsx +++ b/ui/admin/app/components/signin/SignIn.tsx @@ -1,7 +1,8 @@ -import { FaGoogle } from "react-icons/fa"; - import { cn } from "~/lib/utils"; +import { Bootstrap } from "~/components/auth-and-model-providers/Bootstrap"; +import { ProviderIcon } from "~/components/auth-and-model-providers/ProviderIcon"; +import { CommonAuthProviderFriendlyNames } from "~/components/auth-and-model-providers/constants"; import { ObotLogo } from "~/components/branding/ObotLogo"; import { Button } from "~/components/ui/button"; import { @@ -11,12 +12,16 @@ import { CardHeader, CardTitle, } from "~/components/ui/card"; +import { useAuthProviders } from "~/hooks/auth-providers/useAuthProviders"; interface SignInProps { className?: string; } export function SignIn({ className }: SignInProps) { + const { authProviders } = useAuthProviders(); + const configuredAuthProviders = authProviders.filter((p) => p.configured); + return (
@@ -24,21 +29,27 @@ export function SignIn({ className }: SignInProps) { - - Please sign in using the button below. - + {configuredAuthProviders.length > 0 && ( + + Please sign in using an option below. + + )} - - + + {configuredAuthProviders.map((provider) => ( + + ))} + {configuredAuthProviders.length === 0 && }
diff --git a/ui/admin/app/components/user/UserMenu.tsx b/ui/admin/app/components/user/UserMenu.tsx index 30cf82168..03aaadd64 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"; @@ -54,7 +55,11 @@ export const UserMenu: React.FC = ({ { + onClick={async () => { + await fetch(ApiRoutes.bootstrap.logout().url, { + method: "POST", + }); + window.location.href = "/oauth2/sign_out?rd=/admin/"; }} > diff --git a/ui/admin/app/hooks/auth-providers/useAuthProviders.tsx b/ui/admin/app/hooks/auth-providers/useAuthProviders.tsx new file mode 100644 index 000000000..10853ffc7 --- /dev/null +++ b/ui/admin/app/hooks/auth-providers/useAuthProviders.tsx @@ -0,0 +1,14 @@ +import useSWR from "swr"; + +import { AuthProviderApiService } from "~/lib/service/api/authProviderApiService"; + +export function useAuthProviders() { + const { data: authProviders } = useSWR( + AuthProviderApiService.getAuthProviders.key(), + () => AuthProviderApiService.getAuthProviders() + ); + const configured = + authProviders?.some((authProvider) => authProvider.configured) ?? false; + + return { configured, authProviders: authProviders ?? [] }; +} diff --git a/ui/admin/app/lib/model/models.ts b/ui/admin/app/lib/model/models.ts index 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/modelProviders.ts b/ui/admin/app/lib/model/providers.ts similarity index 51% rename from ui/admin/app/lib/model/modelProviders.ts rename to ui/admin/app/lib/model/providers.ts index 1c7263e91..4f5d994a3 100644 --- a/ui/admin/app/lib/model/modelProviders.ts +++ b/ui/admin/app/lib/model/providers.ts @@ -1,18 +1,24 @@ import { EntityMeta } from "~/lib/model/primitives"; -export type ModelProviderStatus = { +export type ProviderStatus = { configured: boolean; - modelsBackPopulated?: boolean; icon?: string; requiredConfigurationParameters?: string[]; + optionalConfigurationParameters?: string[]; missingConfigurationParameters?: string[]; }; -export type ModelProvider = EntityMeta & - ModelProviderStatus & { +export type Provider = EntityMeta & + ProviderStatus & { toolReference: string; name: string; revision: string; }; -export type ModelProviderConfig = Record; +export type ProviderConfig = Record; + +export type ModelProvider = Provider & { + modelsBackPopulated?: boolean; +}; + +export type AuthProvider = Provider; diff --git a/ui/admin/app/lib/routers/apiRoutes.ts b/ui/admin/app/lib/routers/apiRoutes.ts index fec11fc35..9a4bad200 100644 --- a/ui/admin/app/lib/routers/apiRoutes.ts +++ b/ui/admin/app/lib/routers/apiRoutes.ts @@ -308,6 +308,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..5d393749d --- /dev/null +++ b/ui/admin/app/lib/service/api/authProviderApiService.ts @@ -0,0 +1,89 @@ +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 0ff742875..4fd1b8a47 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,22 +13,22 @@ const getModelProviders = async () => { getModelProviders.key = () => ({ url: ApiRoutes.modelProviders.getModelProviders().path }) as const; -const getModelProviderById = async (modelProviderKey: string) => { +const getModelProviderById = async (providerKey: string) => { const res = await request({ - url: ApiRoutes.modelProviders.getModelProviderById(modelProviderKey).url, + url: ApiRoutes.modelProviders.getModelProviderById(providerKey).url, method: "GET", errorMessage: - "Failed to update configuration values on the requested modal provider.", + "Failed to update configuration values on the requested model provider.", }); return res.data; }; -getModelProviderById.key = (modelProviderId?: string) => { - if (!modelProviderId) return null; +getModelProviderById.key = (providerId?: string) => { + if (!providerId) return null; return { - url: ApiRoutes.modelProviders.getModelProviderById(modelProviderId).path, - modelProviderId, + url: ApiRoutes.modelProviders.getModelProviderById(providerId).path, + providerId, }; }; @@ -49,46 +49,44 @@ const validateModelProviderById = async ( }; const configureModelProviderById = async ( - modelProviderKey: string, - modelProviderConfig: ModelProviderConfig + providerKey: string, + providerConfig: ProviderConfig ) => { const res = await request({ - url: ApiRoutes.modelProviders.configureModelProviderById(modelProviderKey) - .url, + url: ApiRoutes.modelProviders.configureModelProviderById(providerKey).url, method: "POST", - data: modelProviderConfig, + data: providerConfig, errorMessage: - "Failed to update configuration values on the requested modal provider.", + "Failed to update configuration values on the requested model provider.", }); return res.data; }; -const revealModelProviderById = async (modelProviderKey: string) => { - const res = await request({ - url: ApiRoutes.modelProviders.revealModelProviderById(modelProviderKey).url, +const revealModelProviderById = async (providerKey: string) => { + const res = await request({ + url: ApiRoutes.modelProviders.revealModelProviderById(providerKey).url, method: "POST", errorMessage: - "Failed to reveal configuration values on the requested modal provider.", + "Failed to reveal configuration values on the requested model provider.", }); return res.data; }; -revealModelProviderById.key = (modelProviderId?: string) => { - if (!modelProviderId) return null; +revealModelProviderById.key = (providerId?: string) => { + if (!providerId) return null; return { - url: ApiRoutes.modelProviders.revealModelProviderById(modelProviderId).path, - modelProviderId, + url: ApiRoutes.modelProviders.revealModelProviderById(providerId).path, + providerId, }; }; -const deconfigureModelProviderById = async (modelProviderKey: string) => { +const deconfigureModelProviderById = async (providerKey: string) => { const res = await request({ - url: ApiRoutes.modelProviders.deconfigureModelProviderById(modelProviderKey) - .url, + url: ApiRoutes.modelProviders.deconfigureModelProviderById(providerKey).url, method: "POST", - errorMessage: "Failed to deconfigure the requested modal provider.", + errorMessage: "Failed to deconfigure the requested model provider.", }); return res.data; diff --git a/ui/admin/app/lib/service/routeService.ts b/ui/admin/app/lib/service/routeService.ts index 554397387..51c291b6a 100644 --- a/ui/admin/app/lib/service/routeService.ts +++ b/ui/admin/app/lib/service/routeService.ts @@ -66,6 +66,11 @@ export const RouteHelperMap = { path: "/agents/:agent", schema: QuerySchemas.agentSchema, }, + "/auth-providers": { + regex: exactRegex($path("/auth-providers")), + path: "/auth-providers", + schema: z.null(), + }, "/debug": { regex: exactRegex($path("/debug")), path: "/debug", 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..520587a74 --- /dev/null +++ b/ui/admin/app/routes/_auth.auth-providers.tsx @@ -0,0 +1,71 @@ +import { MetaFunction } from "react-router"; + +import { AuthProvider } from "~/lib/model/providers"; +import { RouteHandle } from "~/lib/service/routeHandles"; + +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 c4cd0595c..1e5b1d096 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 { ScrollArea } from "~/components/ui/scroll-area"; 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..e840184f4 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"; @@ -47,6 +48,8 @@ export function ErrorBoundary() { 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): 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 1d0e05164..32fa99126 100644 --- a/ui/user/src/routes/+page.svelte +++ b/ui/user/src/routes/+page.svelte @@ -6,6 +6,13 @@ import { darkMode } from '$lib/stores'; import { Book } from '$lib/icons'; import { loadedAssistants } from '$lib/stores'; + import { listAuthProviders, type AuthProvider } from '$lib/auth'; + + let authProviders: AuthProvider[] = $state([]) + + onMount(async () => { + authProviders = await listAuthProviders(); + }); let div: HTMLElement; @@ -57,17 +64,29 @@ {/if}
-
diff --git a/ui/user/src/routes/[agent]/+page.svelte b/ui/user/src/routes/[agent]/+page.svelte index 2d011ab61..ab6e65cce 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 = '/'; } }); From 59001285922bd5e46137c50c85dad0ec02e3a848 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 20 Jan 2025 09:38:23 -0500 Subject: [PATCH 2/6] fix lint error Signed-off-by: Grant Linville --- ui/admin/app/lib/service/api/modelProviderApiService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/admin/app/lib/service/api/modelProviderApiService.ts b/ui/admin/app/lib/service/api/modelProviderApiService.ts index 4fd1b8a47..ce3cdb609 100644 --- a/ui/admin/app/lib/service/api/modelProviderApiService.ts +++ b/ui/admin/app/lib/service/api/modelProviderApiService.ts @@ -34,7 +34,7 @@ getModelProviderById.key = (providerId?: string) => { const validateModelProviderById = async ( modelProviderKey: string, - modelProviderConfig: ModelProviderConfig + modelProviderConfig: ProviderConfig ) => { const res = await request({ url: ApiRoutes.modelProviders.validateModelProviderById(modelProviderKey) From b83eab21ef48152f04b3d4771979e96320a074c0 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 20 Jan 2025 13:28:39 -0500 Subject: [PATCH 3/6] PR feedback Signed-off-by: Grant Linville --- pkg/api/router/router.go | 1 - pkg/gateway/server/dispatcher/dispatcher.go | 38 +++++++++++---------- pkg/gateway/server/tokenreview.go | 5 ++- pkg/proxy/proxy.go | 7 ++-- pkg/services/config.go | 12 +++---- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/pkg/api/router/router.go b/pkg/api/router/router.go index cb0efda41..c243f2936 100644 --- a/pkg/api/router/router.go +++ b/pkg/api/router/router.go @@ -37,7 +37,6 @@ func Router(services *services.Services) (http.Handler, error) { sendgridWebhookHandler := sendgrid.NewInboundWebhookHandler(services.StorageClient, services.EmailServerName, services.SendgridWebhookUsername, services.SendgridWebhookPassword) // Version - mux.HandleFunc("GET /api/version", version.GetVersion) // Agents diff --git a/pkg/gateway/server/dispatcher/dispatcher.go b/pkg/gateway/server/dispatcher/dispatcher.go index 64d4886e7..2a9965635 100644 --- a/pkg/gateway/server/dispatcher/dispatcher.go +++ b/pkg/gateway/server/dispatcher/dispatcher.go @@ -330,21 +330,16 @@ func (d *Dispatcher) startAuthProvider(ctx context.Context, namespace, authProvi } // 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) + isConfigured, missingEnvVars, err := d.isAuthProviderConfigured(ctx, credCtx, authProvider) 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) + return nil, fmt.Errorf("failed to check auth provider configuration: %w", err) + } else if !isConfigured { + if len(missingEnvVars) > 0 { + return nil, fmt.Errorf("auth provider is not configured: missing configuration parameters %s", strings.Join(missingEnvVars, ", ")) } - } - - if len(missingEnvVars) > 0 { - return nil, fmt.Errorf("auth provider is not configured: missing configuration parameters %s", strings.Join(missingEnvVars, ", ")) + return nil, fmt.Errorf("auth provider is not configured: %w", err) } } @@ -376,7 +371,7 @@ func (d *Dispatcher) ListConfiguredAuthProviders(ctx context.Context, namespace var result []string for _, authProvider := range authProviders.Items { - if d.isAuthProviderConfigured(ctx, []string{string(authProvider.UID)}, authProvider) { + if isConfigured, _, _ := d.isAuthProviderConfigured(ctx, []string{string(authProvider.UID)}, authProvider); isConfigured { result = append(result, authProvider.Name) } } @@ -384,14 +379,16 @@ func (d *Dispatcher) ListConfiguredAuthProviders(ctx context.Context, namespace return result, nil } -func (d *Dispatcher) isAuthProviderConfigured(ctx context.Context, credCtx []string, toolRef v1.ToolReference) bool { +// isAuthProviderConfigured checks an auth provider to see if all of its required environment variables are set. +// Returns: isConfigured (bool), missingEnvVars ([]string), error +func (d *Dispatcher) isAuthProviderConfigured(ctx context.Context, credCtx []string, toolRef v1.ToolReference) (bool, []string, error) { if toolRef.Status.Tool == nil { - return false + return false, nil, nil } cred, err := d.gptscript.RevealCredential(ctx, credCtx, toolRef.Name) if err != nil { - return false + return false, nil, err } var requiredEnvVars []string @@ -399,11 +396,16 @@ func (d *Dispatcher) isAuthProviderConfigured(ctx context.Context, credCtx []str requiredEnvVars = strings.Split(toolRef.Status.Tool.Metadata["envVars"], ",") } + var missingEnvVars []string for _, envVar := range requiredEnvVars { if cred.Env[envVar] == "" { - return false + missingEnvVars = append(missingEnvVars, envVar) } } - return true + if len(missingEnvVars) > 0 { + return false, missingEnvVars, nil + } + + return true, nil, nil } diff --git a/pkg/gateway/server/tokenreview.go b/pkg/gateway/server/tokenreview.go index a33482e43..c699a6767 100644 --- a/pkg/gateway/server/tokenreview.go +++ b/pkg/gateway/server/tokenreview.go @@ -1,7 +1,6 @@ package server import ( - "fmt" "net/http" "strconv" "strings" @@ -27,8 +26,8 @@ func (s *Server) AuthenticateRequest(req *http.Request) (*authenticator.Response return err } - namespace = fmt.Sprint(tkn.AuthProviderNamespace) - name = fmt.Sprint(tkn.AuthProviderName) + namespace = tkn.AuthProviderNamespace + name = tkn.AuthProviderName return tx.Where("id = ?", tkn.UserID).First(u).Error }); err != nil { return nil, false, err diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 8dc75d6e9..524b47f7f 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -85,7 +85,7 @@ func (pm *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - http.Error(w, "no auth providers configured", http.StatusInternalServerError) + http.Error(w, "no auth providers configured", http.StatusBadRequest) return } sort.Slice(providers, func(i, j int) bool { @@ -181,10 +181,7 @@ func (p *Proxy) authenticateRequest(req *http.Request) (*authenticator.Response, sr := SerializableRequest{ Method: req.Method, URL: req.URL.String(), - Header: make(map[string][]string), - } - for k, v := range req.Header { - sr.Header[k] = v + Header: req.Header, } srJSON, err := json.Marshal(sr) diff --git a/pkg/services/config.go b/pkg/services/config.go index 869146025..b33411a04 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -23,7 +23,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/bootstrap" "github.com/obot-platform/obot/pkg/credstores" "github.com/obot-platform/obot/pkg/events" "github.com/obot-platform/obot/pkg/gateway/client" @@ -65,8 +65,7 @@ type Config struct { 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"` + EnableAuthentication bool `usage:"Enable authentication" default:"false" env:"OBOT_ENABLE_AUTHENTICATION"` AuthAdminEmails []string `usage:"Emails of admin users"` // Sendgrid webhook @@ -95,7 +94,7 @@ type Services struct { GatewayClient *client.Client ProxyManager *proxy.Manager ProviderDispatcher *dispatcher.Dispatcher - Bootstrapper *bootstrap2.Bootstrap + Bootstrapper *bootstrap.Bootstrap KnowledgeSetIngestionLimit int SupportDocker bool @@ -306,7 +305,7 @@ func New(ctx context.Context, config Config) (*Services, error) { proxyManager *proxy.Manager ) - bootstrapper, err := bootstrap2.New(config.Hostname, gatewayClient) + bootstrapper, err := bootstrap.New(config.Hostname, gatewayClient) if err != nil { return nil, err } @@ -324,8 +323,7 @@ func New(ctx context.Context, config Config) (*Services, error) { } var authenticators authenticator.Request = gatewayServer - if !config.DisableAuthentication { - // "Authentication Enabled" flow + if config.EnableAuthentication { proxyManager = proxy.NewProxyManager(providerDispatcher) // Token Auth + OAuth auth From 4c53bfe6026130a91c024deb9eae86deb5657979 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 20 Jan 2025 15:31:56 -0500 Subject: [PATCH 4/6] fix authz Signed-off-by: Grant Linville --- pkg/api/authz/authz.go | 36 ++++++++++++++++++++++++++++++++---- pkg/api/server/server.go | 9 +++++---- pkg/services/config.go | 2 +- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/pkg/api/authz/authz.go b/pkg/api/authz/authz.go index c55c92874..62731db63 100644 --- a/pkg/api/authz/authz.go +++ b/pkg/api/authz/authz.go @@ -27,7 +27,11 @@ var staticRules = map[string][]string{ // Allow access to the UI "/admin/", "/{$}", + "/{agent}", + "/images/", + "/_app/", "/static/", + // Allow access to the oauth2 endpoints "/oauth2/", @@ -38,7 +42,7 @@ var staticRules = map[string][]string{ "GET /api/oauth/start/{id}/{namespace}/{name}", - // The bootstrap logout just deletes a cookie in the client, and does nothing else. + "POST /api/bootstrap/login", "POST /api/bootstrap/logout", "GET /api/app-oauth/authorize/{id}", @@ -65,14 +69,25 @@ var staticRules = map[string][]string{ }, } +var devModeRules = map[string][]string{ + anyGroup: { + "/node_modules/", + "/@fs/", + "/.svelte-kit/", + "/@vite/", + "/@id/", + "/src/", + }, +} + type Authorizer struct { rules []rule storage kclient.Client } -func NewAuthorizer(storage kclient.Client) *Authorizer { +func NewAuthorizer(storage kclient.Client, devMode bool) *Authorizer { return &Authorizer{ - rules: defaultRules(), + rules: defaultRules(devMode), storage: storage, } } @@ -103,7 +118,7 @@ type rule struct { mux *http.ServeMux } -func defaultRules() []rule { +func defaultRules(devMode bool) []rule { var ( rules []rule f = (*fake)(nil) @@ -120,6 +135,19 @@ func defaultRules() []rule { rules = append(rules, rule) } + if devMode { + for _, group := range slices.Sorted(maps.Keys(devModeRules)) { + rule := rule{ + group: group, + mux: http.NewServeMux(), + } + for _, url := range devModeRules[group] { + rule.mux.Handle(url, f) + } + rules = append(rules, rule) + } + } + return rules } diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index b55bfa99a..7c3fa0edc 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -62,11 +62,12 @@ func (s *Server) wrap(f api.HandlerFunc) http.HandlerFunc { return } + if !s.authorizer.Authorize(req, user) { + http.Error(rw, "forbidden", http.StatusForbidden) + return + } + if strings.HasPrefix(req.URL.Path, "/api/") { - if !s.authorizer.Authorize(req, user) { - http.Error(rw, "forbidden", http.StatusForbidden) - 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") diff --git a/pkg/services/config.go b/pkg/services/config.go index b33411a04..ea355eef3 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -373,7 +373,7 @@ func New(ctx context.Context, config Config) (*Services, error) { storageClient, c, authn.NewAuthenticator(authenticators), - authz.NewAuthorizer(r.Backend()), + authz.NewAuthorizer(r.Backend(), config.DevMode), proxyManager, config.Hostname, ), From c36a669e076ce8e3df2c30655b6e2eb07d52b6f2 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Mon, 20 Jan 2025 17:38:40 -0500 Subject: [PATCH 5/6] detect when we should set a new cookie from the auth provider Signed-off-by: Donnie Adams --- go.mod | 1 - pkg/api/server/server.go | 12 ++++++++++++ pkg/proxy/proxy.go | 28 ++++++++++++++++++---------- pkg/services/config.go | 2 +- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index e6a03b870..88662a759 100644 --- a/go.mod +++ b/go.mod @@ -205,7 +205,6 @@ require ( github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // 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 diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 7c3fa0edc..c7ca5efcd 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -62,6 +62,10 @@ func (s *Server) wrap(f api.HandlerFunc) http.HandlerFunc { return } + if setCookie := firstValue(user.GetExtra(), "set-cookie"); setCookie != "" { + rw.Header().Set("Set-Cookie", setCookie) + } + if !s.authorizer.Authorize(req, user) { http.Error(rw, "forbidden", http.StatusForbidden) return @@ -90,3 +94,11 @@ func (s *Server) wrap(f api.HandlerFunc) http.HandlerFunc { } } } + +func firstValue(m map[string][]string, key string) string { + values := m[key] + if len(values) == 0 { + return "" + } + return values[0] +} diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 524b47f7f..597c6fe3f 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -175,6 +175,7 @@ type SerializableState struct { PreferredUsername string `json:"preferredUsername"` User string `json:"user"` Email string `json:"email"` + SetCookie string `json:"setCookie"` } func (p *Proxy) authenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { @@ -198,9 +199,10 @@ func (p *Proxy) authenticateRequest(req *http.Request) (*authenticator.Response, if err != nil { return nil, false, err } + defer stateResponse.Body.Close() var ss SerializableState - if err := json.NewDecoder(stateResponse.Body).Decode(&ss); err != nil { + if err = json.NewDecoder(stateResponse.Body).Decode(&ss); err != nil { return nil, false, err } @@ -212,20 +214,26 @@ func (p *Proxy) authenticateRequest(req *http.Request) (*authenticator.Response, } } + u := &user.DefaultInfo{ + UID: ss.User, + Name: userName, + Extra: map[string][]string{ + "email": {ss.Email}, + "auth_provider_name": {p.name}, + "auth_provider_namespace": {p.namespace}, + }, + } + + if ss.SetCookie != "" { + u.Extra["set-cookie"] = []string{ss.SetCookie} + } + if req.URL.Path == "/api/me" { // Put the access token on the context so that the profile icon can be fetched. *req = *req.WithContext(accesstoken.ContextWithAccessToken(req.Context(), ss.AccessToken)) } return &authenticator.Response{ - User: &user.DefaultInfo{ - UID: ss.User, - Name: userName, - Extra: map[string][]string{ - "email": {ss.Email}, - "auth_provider_name": {p.name}, - "auth_provider_namespace": {p.namespace}, - }, - }, + User: u, }, true, nil } diff --git a/pkg/services/config.go b/pkg/services/config.go index ea355eef3..494c2219c 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -65,7 +65,7 @@ type Config struct { 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"` - EnableAuthentication bool `usage:"Enable authentication" default:"false" env:"OBOT_ENABLE_AUTHENTICATION"` + EnableAuthentication bool `usage:"Enable authentication" default:"false"` AuthAdminEmails []string `usage:"Emails of admin users"` // Sendgrid webhook From ed378b01f31da7f924683da4b2655c89cb3db384 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Mon, 20 Jan 2025 17:57:51 -0500 Subject: [PATCH 6/6] add onMount import back in Signed-off-by: Donnie Adams --- ui/user/src/routes/+page.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/user/src/routes/+page.svelte b/ui/user/src/routes/+page.svelte index 32fa99126..b342ec070 100644 --- a/ui/user/src/routes/+page.svelte +++ b/ui/user/src/routes/+page.svelte @@ -7,6 +7,7 @@ import { Book } from '$lib/icons'; import { loadedAssistants } from '$lib/stores'; import { listAuthProviders, type AuthProvider } from '$lib/auth'; + import {onMount} from "svelte" let authProviders: AuthProvider[] = $state([])