Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync
This commit is contained in:
@@ -134,17 +134,17 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/term v0.25.0 // indirect
|
||||
golang.org/x/term v0.26.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.org/x/tools v0.27.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
gonum.org/v1/gonum v0.15.1 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240820151423-278611b39280 // indirect
|
||||
|
||||
+10
-10
@@ -381,8 +381,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
@@ -392,8 +392,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -403,8 +403,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
@@ -433,8 +433,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
@@ -452,8 +452,8 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
glog "github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -57,7 +58,8 @@ type httpClient interface {
|
||||
func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Route, ctx *contextmodel.ReqContext,
|
||||
proxyPath string, cfg *setting.Cfg, clientProvider httpclient.Provider,
|
||||
oAuthTokenService oauthtoken.OAuthTokenService, dsService datasources.DataSourceService,
|
||||
tracer tracing.Tracer, features featuremgmt.FeatureToggles) (*DataSourceProxy, error) {
|
||||
tracer tracing.Tracer, features featuremgmt.FeatureToggles,
|
||||
) (*DataSourceProxy, error) {
|
||||
targetURL, err := datasource.ValidateURL(ds.Type, ds.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -261,7 +263,8 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
|
||||
}
|
||||
|
||||
if proxy.oAuthTokenService.IsOAuthPassThruEnabled(proxy.ds) {
|
||||
if token := proxy.oAuthTokenService.GetCurrentOAuthToken(req.Context(), proxy.ctx.SignedInUser); token != nil {
|
||||
reqCtx := contexthandler.FromContext(req.Context())
|
||||
if token := proxy.oAuthTokenService.GetCurrentOAuthToken(req.Context(), proxy.ctx.SignedInUser, reqCtx.UserToken); token != nil {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
|
||||
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
|
||||
@@ -32,7 +32,9 @@ import (
|
||||
pluginfakes "github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
@@ -557,7 +559,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
var routes []*plugins.Route
|
||||
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/to/folder/", func(proxy *DataSourceProxy) {
|
||||
proxy.oAuthTokenService = &oauthtokentest.MockOauthTokenService{
|
||||
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester) *oauth2.Token {
|
||||
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester, _ *auth.UserToken) *oauth2.Token {
|
||||
return (&oauth2.Token{
|
||||
AccessToken: "testtoken",
|
||||
RefreshToken: "testrefreshtoken",
|
||||
@@ -573,6 +575,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
req = req.WithContext(context.WithValue(req.Context(), ctxkey.Key{}, &contextmodel.ReqContext{UserToken: nil}))
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.director(req)
|
||||
|
||||
@@ -35,8 +35,8 @@ require (
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
|
||||
@@ -77,8 +77,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -91,8 +91,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
@@ -78,13 +78,13 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/term v0.25.0 // indirect
|
||||
golang.org/x/term v0.26.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.org/x/tools v0.27.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240820151423-278611b39280 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect
|
||||
|
||||
@@ -221,8 +221,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
@@ -241,8 +241,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
@@ -259,8 +259,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
@@ -277,8 +277,8 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
+3
-3
@@ -35,9 +35,9 @@ require (
|
||||
go.opentelemetry.io/otel v1.32.0 // indirect; @grafana/grafana-backend-group
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 // indirect; @grafana/grafana-backend-group
|
||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/crypto v0.28.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/mod v0.21.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/net v0.30.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
|
||||
golang.org/x/crypto v0.29.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/mod v0.22.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/net v0.31.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
|
||||
golang.org/x/oauth2 v0.23.0 // @grafana/identity-access-team
|
||||
golang.org/x/sync v0.9.0 // indirect; @grafana/alerting-backend
|
||||
golang.org/x/text v0.20.0 // indirect; @grafana/grafana-backend-group
|
||||
|
||||
+6
-6
@@ -262,8 +262,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
@@ -272,8 +272,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -283,8 +283,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
|
||||
@@ -6,10 +6,10 @@ require (
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/subcommands v1.2.0
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
|
||||
golang.org/x/tools v0.26.0
|
||||
golang.org/x/tools v0.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
||||
|
||||
+3
-3
@@ -111,12 +111,12 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/goleak v1.3.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.org/x/tools v0.27.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
gonum.org/v1/gonum v0.15.1 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect
|
||||
|
||||
+6
-6
@@ -297,15 +297,15 @@ golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -337,8 +337,8 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
)
|
||||
@@ -48,13 +49,15 @@ type queryParser struct {
|
||||
legacy service.LegacyDataSourceLookup
|
||||
reader *expr.ExpressionQueryReader
|
||||
tracer tracing.Tracer
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func newQueryParser(reader *expr.ExpressionQueryReader, legacy service.LegacyDataSourceLookup, tracer tracing.Tracer) *queryParser {
|
||||
func newQueryParser(reader *expr.ExpressionQueryReader, legacy service.LegacyDataSourceLookup, tracer tracing.Tracer, logger log.Logger) *queryParser {
|
||||
return &queryParser{
|
||||
reader: reader,
|
||||
legacy: legacy,
|
||||
tracer: tracer,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +85,7 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe
|
||||
|
||||
ds, err := p.getValidDataSourceRef(ctx, q.Datasource, q.DatasourceID)
|
||||
if err != nil {
|
||||
p.logger.Error("Failed to get valid datasource ref", "error", err)
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
@@ -93,14 +97,17 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe
|
||||
// but this approach lets us focus on well typed behavior first
|
||||
raw, err := json.Marshal(q)
|
||||
if err != nil {
|
||||
p.logger.Error("Failed to marshal query for expression", "error", err)
|
||||
return rsp, err
|
||||
}
|
||||
iter, err := jsoniter.ParseBytes(jsoniter.ConfigDefault, raw)
|
||||
if err != nil {
|
||||
p.logger.Error("Failed to parse bytes for expression", "error", err)
|
||||
return rsp, err
|
||||
}
|
||||
exp, err := p.reader.ReadQuery(q, iter)
|
||||
if err != nil {
|
||||
p.logger.Error("Failed to read query for expression", "error", err)
|
||||
return rsp, NewErrorWithRefID(q.RefID, err)
|
||||
}
|
||||
exp.GraphID = int64(len(expressions) + 1)
|
||||
@@ -170,6 +177,7 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe
|
||||
// Add the sorted expressions
|
||||
sortedNodes, err := topo.SortStabilized(dg, nil)
|
||||
if err != nil {
|
||||
p.logger.Error("Error when sorting nodes", "error", err)
|
||||
return rsp, makeCyclicError("")
|
||||
}
|
||||
for _, v := range sortedNodes {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
@@ -29,7 +30,7 @@ type parserTestObject struct {
|
||||
func TestQuerySplitting(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
parser := newQueryParser(expr.NewExpressionQueryReader(featuremgmt.WithFeatures()),
|
||||
&legacyDataSourceRetriever{}, tracing.InitializeTracerForTest())
|
||||
&legacyDataSourceRetriever{}, tracing.InitializeTracerForTest(), log.NewNopLogger())
|
||||
|
||||
t.Run("missing datasource flavors", func(t *testing.T) {
|
||||
split, err := parser.parseRequest(ctx, &query.QueryDataRequest{
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestQueryRestConnectHandler(t *testing.T) {
|
||||
},
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
parser: newQueryParser(expr.NewExpressionQueryReader(featuremgmt.WithFeatures()),
|
||||
&legacyDataSourceRetriever{}, tracing.InitializeTracerForTest()),
|
||||
&legacyDataSourceRetriever{}, tracing.InitializeTracerForTest(), nil),
|
||||
log: log.New("test"),
|
||||
}
|
||||
qr := newQueryREST(b)
|
||||
|
||||
@@ -75,7 +75,7 @@ func NewQueryAPIBuilder(features featuremgmt.FeatureToggles,
|
||||
log: log.New("query_apiserver"),
|
||||
client: client,
|
||||
registry: registry,
|
||||
parser: newQueryParser(reader, legacy, tracer),
|
||||
parser: newQueryParser(reader, legacy, tracer, log.New("query_parser")),
|
||||
metrics: newQueryMetrics(registerer),
|
||||
tracer: tracer,
|
||||
features: features,
|
||||
|
||||
@@ -29,7 +29,8 @@ func NewScopeAPIBuilder() *ScopeAPIBuilder {
|
||||
}
|
||||
|
||||
func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, reg prometheus.Registerer) *ScopeAPIBuilder {
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
if !(features.IsEnabledGlobally(featuremgmt.FlagScopeApi) ||
|
||||
features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs)) {
|
||||
return nil // skip registration unless opting into experimental apis
|
||||
}
|
||||
builder := NewScopeAPIBuilder()
|
||||
|
||||
@@ -42,6 +42,9 @@ func GetAccessPolicies(ctx context.Context, orgID int64, sql *session.SessionDB,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = rows.Close()
|
||||
}()
|
||||
|
||||
created := time.Now()
|
||||
updated := time.Now()
|
||||
|
||||
@@ -115,7 +115,7 @@ func folderTreeCollector(store db.DB) legacyTupleCollector {
|
||||
func managedPermissionsCollector(store db.DB, kind string) legacyTupleCollector {
|
||||
return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) {
|
||||
query := `
|
||||
SELECT u.uid as user_uid, t.uid as team_uid, p.action, p.kind, p.identifier, r.org_id, br.role as basic_role_name
|
||||
SELECT u.uid as user_uid, u.is_service_account as is_service_account, t.uid as team_uid, p.action, p.kind, p.identifier, r.org_id, br.role as basic_role_name
|
||||
FROM permission p
|
||||
INNER JOIN role r ON p.role_id = r.id
|
||||
LEFT JOIN user_role ur ON r.id = ur.role_id
|
||||
@@ -128,12 +128,13 @@ func managedPermissionsCollector(store db.DB, kind string) legacyTupleCollector
|
||||
AND p.kind = ?
|
||||
`
|
||||
type Permission struct {
|
||||
Action string `xorm:"action"`
|
||||
Kind string
|
||||
Identifier string
|
||||
UserUID string `xorm:"user_uid"`
|
||||
TeamUID string `xorm:"team_uid"`
|
||||
BasicRoleName string `xorm:"basic_role_name"`
|
||||
Action string `xorm:"action"`
|
||||
Kind string
|
||||
Identifier string
|
||||
UserUID string `xorm:"user_uid"`
|
||||
IsServiceAccount bool `xorm:"is_service_account"`
|
||||
TeamUID string `xorm:"team_uid"`
|
||||
BasicRoleName string `xorm:"basic_role_name"`
|
||||
}
|
||||
|
||||
var permissions []Permission
|
||||
@@ -149,7 +150,9 @@ func managedPermissionsCollector(store db.DB, kind string) legacyTupleCollector
|
||||
|
||||
for _, p := range permissions {
|
||||
var subject string
|
||||
if len(p.UserUID) > 0 {
|
||||
if len(p.UserUID) > 0 && p.IsServiceAccount {
|
||||
subject = zanzana.NewTupleEntry(zanzana.TypeServiceAccount, p.UserUID, "")
|
||||
} else if len(p.UserUID) > 0 {
|
||||
subject = zanzana.NewTupleEntry(zanzana.TypeUser, p.UserUID, "")
|
||||
} else if len(p.TeamUID) > 0 {
|
||||
subject = zanzana.NewTupleEntry(zanzana.TypeTeam, p.TeamUID, zanzana.RelationTeamMember)
|
||||
@@ -198,16 +201,19 @@ func tupleStringWithoutCondition(tuple *openfgav1.TupleKey) string {
|
||||
func basicRoleBindingsCollector(store db.DB) legacyTupleCollector {
|
||||
return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) {
|
||||
query := `
|
||||
SELECT ou.org_id, u.uid as user_uid, ou.role as org_role
|
||||
SELECT
|
||||
ou.org_id, u.uid as user_uid,
|
||||
u.is_service_account as is_service_account,
|
||||
ou.role as org_role
|
||||
FROM org_user ou
|
||||
LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ou.user_id
|
||||
WHERE ou.org_id = ?
|
||||
AND NOT u.is_service_account
|
||||
`
|
||||
// FIXME: handle service admin role
|
||||
type Binding struct {
|
||||
UserUID string `xorm:"user_uid"`
|
||||
OrgRole string `xorm:"org_role"`
|
||||
UserUID string `xorm:"user_uid"`
|
||||
IsServiceAccount bool `xorm:"is_service_account"`
|
||||
OrgRole string `xorm:"org_role"`
|
||||
}
|
||||
|
||||
var bindings []Binding
|
||||
@@ -222,8 +228,13 @@ func basicRoleBindingsCollector(store db.DB) legacyTupleCollector {
|
||||
tuples := make(map[string]map[string]*openfgav1.TupleKey)
|
||||
|
||||
for _, b := range bindings {
|
||||
userType := zanzana.TypeUser
|
||||
if b.IsServiceAccount {
|
||||
userType = zanzana.TypeServiceAccount
|
||||
}
|
||||
|
||||
tuple := &openfgav1.TupleKey{
|
||||
User: zanzana.NewTupleEntry(zanzana.TypeUser, b.UserUID, ""),
|
||||
User: zanzana.NewTupleEntry(userType, b.UserUID, ""),
|
||||
Relation: zanzana.RelationAssignee,
|
||||
Object: zanzana.NewTupleEntry(zanzana.TypeRole, zanzana.TranslateBasicRole(b.OrgRole), ""),
|
||||
}
|
||||
@@ -286,7 +297,7 @@ func teamRoleBindingsCollector(store db.DB) legacyTupleCollector {
|
||||
func userRoleBindingsCollector(store db.DB) legacyTupleCollector {
|
||||
return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) {
|
||||
query := `
|
||||
SELECT u.uid AS user_uid, r.uid AS role_uid
|
||||
SELECT u.uid AS user_uid, u.is_service_account as is_service_account, r.uid AS role_uid
|
||||
FROM user_role ur
|
||||
INNER JOIN ` + store.GetDialect().Quote("user") + ` u ON ur.user_id = u.id
|
||||
INNER JOIN role r ON ur.role_id = r.id
|
||||
@@ -294,8 +305,9 @@ func userRoleBindingsCollector(store db.DB) legacyTupleCollector {
|
||||
AND r.name NOT LIKE 'managed:%'
|
||||
`
|
||||
type Binding struct {
|
||||
UserUID string `xorm:"user_uid"`
|
||||
RoleUID string `xorm:"role_uid"`
|
||||
UserUID string `xorm:"user_uid"`
|
||||
IsServiceAccount bool `xorm:"is_service_account"`
|
||||
RoleUID string `xorm:"role_uid"`
|
||||
}
|
||||
|
||||
var bindings []Binding
|
||||
@@ -310,8 +322,13 @@ func userRoleBindingsCollector(store db.DB) legacyTupleCollector {
|
||||
tuples := make(map[string]map[string]*openfgav1.TupleKey)
|
||||
|
||||
for _, b := range bindings {
|
||||
userType := zanzana.TypeUser
|
||||
if b.IsServiceAccount {
|
||||
userType = zanzana.TypeServiceAccount
|
||||
}
|
||||
|
||||
tuple := &openfgav1.TupleKey{
|
||||
User: zanzana.NewTupleEntry(zanzana.TypeUser, b.UserUID, ""),
|
||||
User: zanzana.NewTupleEntry(userType, b.UserUID, ""),
|
||||
Relation: zanzana.RelationAssignee,
|
||||
Object: zanzana.NewTupleEntry(zanzana.TypeRole, b.RoleUID, ""),
|
||||
}
|
||||
@@ -397,7 +414,8 @@ func zanzanaCollector(relations []string) zanzanaTupleCollector {
|
||||
|
||||
for c != "" {
|
||||
res, err := client.Read(ctx, &authzextv1.ReadRequest{
|
||||
Namespace: namespace,
|
||||
ContinuationToken: c,
|
||||
Namespace: namespace,
|
||||
TupleKey: &authzextv1.ReadRequestTupleKey{
|
||||
Object: object,
|
||||
Relation: relation,
|
||||
|
||||
@@ -74,12 +74,15 @@ type CreateTokenCommand struct {
|
||||
}
|
||||
|
||||
// UserTokenService are used for generating and validating user tokens
|
||||
//
|
||||
//go:generate mockery --name UserTokenService --structname MockUserAuthTokenService --outpkg authtest --filename auth_token_service_mock.go --output ./authtest/
|
||||
type UserTokenService interface {
|
||||
CreateToken(ctx context.Context, cmd *CreateTokenCommand) (*UserToken, error)
|
||||
LookupToken(ctx context.Context, unhashedToken string) (*UserToken, error)
|
||||
GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*UserToken, error)
|
||||
GetExternalSession(ctx context.Context, extSessionID int64) (*ExternalSession, error)
|
||||
GetExternalSession(ctx context.Context, externalSessionID int64) (*ExternalSession, error)
|
||||
FindExternalSessions(ctx context.Context, query *ListExternalSessionQuery) ([]*ExternalSession, error)
|
||||
UpdateExternalSession(ctx context.Context, externalSessionID int64, cmd *UpdateExternalSessionCommand) error
|
||||
|
||||
// RotateToken will always rotate a valid token
|
||||
RotateToken(ctx context.Context, cmd RotateCommand) (*UserToken, error)
|
||||
|
||||
@@ -248,14 +248,18 @@ func (s *UserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context,
|
||||
return &userToken, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) GetExternalSession(ctx context.Context, extSessionID int64) (*auth.ExternalSession, error) {
|
||||
return s.externalSessionStore.Get(ctx, extSessionID)
|
||||
func (s *UserAuthTokenService) GetExternalSession(ctx context.Context, externalSessionID int64) (*auth.ExternalSession, error) {
|
||||
return s.externalSessionStore.Get(ctx, externalSessionID)
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) FindExternalSessions(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) {
|
||||
return s.externalSessionStore.List(ctx, query)
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) UpdateExternalSession(ctx context.Context, externalSessionID int64, cmd *auth.UpdateExternalSessionCommand) error {
|
||||
return s.externalSessionStore.Update(ctx, externalSessionID, cmd)
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error) {
|
||||
if cmd.UnHashedToken == "" {
|
||||
return nil, auth.ErrInvalidSessionToken
|
||||
|
||||
@@ -27,11 +27,11 @@ func provideExternalSessionStore(sqlStore db.DB, secretService secrets.Service,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *store) Get(ctx context.Context, extSessionID int64) (*auth.ExternalSession, error) {
|
||||
func (s *store) Get(ctx context.Context, ID int64) (*auth.ExternalSession, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "externalsession.Get")
|
||||
defer span.End()
|
||||
|
||||
externalSession := &auth.ExternalSession{ID: extSessionID}
|
||||
externalSession := &auth.ExternalSession{ID: ID}
|
||||
|
||||
err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
found, err := sess.Get(externalSession)
|
||||
@@ -150,6 +150,45 @@ func (s *store) Create(ctx context.Context, extSession *auth.ExternalSession) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) Update(ctx context.Context, ID int64, cmd *auth.UpdateExternalSessionCommand) error {
|
||||
ctx, span := s.tracer.Start(ctx, "externalsession.Update")
|
||||
defer span.End()
|
||||
|
||||
var err error
|
||||
externalSession := &auth.ExternalSession{}
|
||||
|
||||
externalSession.AccessToken, err = s.encryptAndEncode(cmd.Token.AccessToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
externalSession.RefreshToken, err = s.encryptAndEncode(cmd.Token.RefreshToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var secretIdToken string
|
||||
if idToken, ok := cmd.Token.Extra("id_token").(string); ok && idToken != "" {
|
||||
secretIdToken, err = s.encryptAndEncode(idToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
externalSession.IDToken = secretIdToken
|
||||
}
|
||||
|
||||
externalSession.ExpiresAt = cmd.Token.Expiry
|
||||
|
||||
err = s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
_, err := sess.ID(ID).Cols("access_token", "refresh_token", "id_token", "expires_at").Update(externalSession)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) Delete(ctx context.Context, ID int64) error {
|
||||
ctx, span := s.tracer.Start(ctx, "externalsession.Delete")
|
||||
defer span.End()
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
// Code generated by mockery v2.42.1. DO NOT EDIT.
|
||||
|
||||
package authtest
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
auth "github.com/grafana/grafana/pkg/services/auth"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
usertoken "github.com/grafana/grafana/pkg/models/usertoken"
|
||||
)
|
||||
|
||||
// MockUserAuthTokenService is an autogenerated mock type for the UserTokenService type
|
||||
type MockUserAuthTokenService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ActiveTokenCount provides a mock function with given fields: ctx, userID
|
||||
func (_m *MockUserAuthTokenService) ActiveTokenCount(ctx context.Context, userID *int64) (int64, error) {
|
||||
ret := _m.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ActiveTokenCount")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *int64) (int64, error)); ok {
|
||||
return rf(ctx, userID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *int64) int64); ok {
|
||||
r0 = rf(ctx, userID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok {
|
||||
r1 = rf(ctx, userID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreateToken provides a mock function with given fields: ctx, cmd
|
||||
func (_m *MockUserAuthTokenService) CreateToken(ctx context.Context, cmd *auth.CreateTokenCommand) (*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateToken")
|
||||
}
|
||||
|
||||
var r0 *usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *auth.CreateTokenCommand) (*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, cmd)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *auth.CreateTokenCommand) *usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, cmd)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *auth.CreateTokenCommand) error); ok {
|
||||
r1 = rf(ctx, cmd)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// FindExternalSessions provides a mock function with given fields: ctx, query
|
||||
func (_m *MockUserAuthTokenService) FindExternalSessions(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) {
|
||||
ret := _m.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FindExternalSessions")
|
||||
}
|
||||
|
||||
var r0 []*auth.ExternalSession
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error)); ok {
|
||||
return rf(ctx, query)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *auth.ListExternalSessionQuery) []*auth.ExternalSession); ok {
|
||||
r0 = rf(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*auth.ExternalSession)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *auth.ListExternalSessionQuery) error); ok {
|
||||
r1 = rf(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetExternalSession provides a mock function with given fields: ctx, externalSessionID
|
||||
func (_m *MockUserAuthTokenService) GetExternalSession(ctx context.Context, externalSessionID int64) (*auth.ExternalSession, error) {
|
||||
ret := _m.Called(ctx, externalSessionID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetExternalSession")
|
||||
}
|
||||
|
||||
var r0 *auth.ExternalSession
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*auth.ExternalSession, error)); ok {
|
||||
return rf(ctx, externalSessionID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *auth.ExternalSession); ok {
|
||||
r0 = rf(ctx, externalSessionID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*auth.ExternalSession)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, externalSessionID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTokenByExternalSessionID provides a mock function with given fields: ctx, externalSessionID
|
||||
func (_m *MockUserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, externalSessionID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetTokenByExternalSessionID")
|
||||
}
|
||||
|
||||
var r0 *usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) (*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, externalSessionID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) *usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, externalSessionID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, externalSessionID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUserRevokedTokens provides a mock function with given fields: ctx, userID
|
||||
func (_m *MockUserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userID int64) ([]*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetUserRevokedTokens")
|
||||
}
|
||||
|
||||
var r0 []*usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) ([]*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, userID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) []*usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, userID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, userID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUserToken provides a mock function with given fields: ctx, userID, userTokenID
|
||||
func (_m *MockUserAuthTokenService) GetUserToken(ctx context.Context, userID int64, userTokenID int64) (*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, userID, userTokenID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetUserToken")
|
||||
}
|
||||
|
||||
var r0 *usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, userID, userTokenID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, userID, userTokenID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok {
|
||||
r1 = rf(ctx, userID, userTokenID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUserTokens provides a mock function with given fields: ctx, userID
|
||||
func (_m *MockUserAuthTokenService) GetUserTokens(ctx context.Context, userID int64) ([]*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetUserTokens")
|
||||
}
|
||||
|
||||
var r0 []*usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) ([]*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, userID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) []*usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, userID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||
r1 = rf(ctx, userID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LookupToken provides a mock function with given fields: ctx, unhashedToken
|
||||
func (_m *MockUserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, unhashedToken)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for LookupToken")
|
||||
}
|
||||
|
||||
var r0 *usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, unhashedToken)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, unhashedToken)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, unhashedToken)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RevokeAllUserTokens provides a mock function with given fields: ctx, userID
|
||||
func (_m *MockUserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userID int64) error {
|
||||
ret := _m.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RevokeAllUserTokens")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = rf(ctx, userID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RevokeToken provides a mock function with given fields: ctx, token, soft
|
||||
func (_m *MockUserAuthTokenService) RevokeToken(ctx context.Context, token *usertoken.UserToken, soft bool) error {
|
||||
ret := _m.Called(ctx, token, soft)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RevokeToken")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *usertoken.UserToken, bool) error); ok {
|
||||
r0 = rf(ctx, token, soft)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RotateToken provides a mock function with given fields: ctx, cmd
|
||||
func (_m *MockUserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateCommand) (*usertoken.UserToken, error) {
|
||||
ret := _m.Called(ctx, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RotateToken")
|
||||
}
|
||||
|
||||
var r0 *usertoken.UserToken
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, auth.RotateCommand) (*usertoken.UserToken, error)); ok {
|
||||
return rf(ctx, cmd)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, auth.RotateCommand) *usertoken.UserToken); ok {
|
||||
r0 = rf(ctx, cmd)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*usertoken.UserToken)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, auth.RotateCommand) error); ok {
|
||||
r1 = rf(ctx, cmd)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateExternalSession provides a mock function with given fields: ctx, externalSessionID, cmd
|
||||
func (_m *MockUserAuthTokenService) UpdateExternalSession(ctx context.Context, externalSessionID int64, cmd *auth.UpdateExternalSessionCommand) error {
|
||||
ret := _m.Called(ctx, externalSessionID, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateExternalSession")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, *auth.UpdateExternalSessionCommand) error); ok {
|
||||
r0 = rf(ctx, externalSessionID, cmd)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewMockUserAuthTokenService creates a new instance of MockUserAuthTokenService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockUserAuthTokenService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockUserAuthTokenService {
|
||||
mock := &MockUserAuthTokenService{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -147,6 +147,24 @@ func (_m *MockExternalSessionStore) List(ctx context.Context, query *auth.ListEx
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, ID, cmd
|
||||
func (_m *MockExternalSessionStore) Update(ctx context.Context, ID int64, cmd *auth.UpdateExternalSessionCommand) error {
|
||||
ret := _m.Called(ctx, ID, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, *auth.UpdateExternalSessionCommand) error); ok {
|
||||
r0 = rf(ctx, ID, cmd)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewMockExternalSessionStore creates a new instance of MockExternalSessionStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockExternalSessionStore(t interface {
|
||||
|
||||
@@ -14,12 +14,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
)
|
||||
|
||||
var _ auth.UserTokenService = (*FakeUserAuthTokenService)(nil)
|
||||
|
||||
type FakeUserAuthTokenService struct {
|
||||
CreateTokenProvider func(ctx context.Context, cmd *auth.CreateTokenCommand) (*auth.UserToken, error)
|
||||
RotateTokenProvider func(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error)
|
||||
GetTokenByExternalSessionIDProvider func(ctx context.Context, externalSessionID int64) (*auth.UserToken, error)
|
||||
GetExternalSessionProvider func(ctx context.Context, externalSessionID int64) (*auth.ExternalSession, error)
|
||||
FindExternalSessionsProvider func(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error)
|
||||
UpdateExternalSessionProvider func(ctx context.Context, externalSessionID int64, cmd *auth.UpdateExternalSessionCommand) error
|
||||
TryRotateTokenProvider func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, *auth.UserToken, error)
|
||||
LookupTokenProvider func(ctx context.Context, unhashedToken string) (*auth.UserToken, error)
|
||||
RevokeTokenProvider func(ctx context.Context, token *auth.UserToken, soft bool) error
|
||||
@@ -98,6 +101,10 @@ func (s *FakeUserAuthTokenService) FindExternalSessions(ctx context.Context, que
|
||||
return s.FindExternalSessionsProvider(context.Background(), query)
|
||||
}
|
||||
|
||||
func (s *FakeUserAuthTokenService) UpdateExternalSession(ctx context.Context, externalSessionID int64, cmd *auth.UpdateExternalSessionCommand) error {
|
||||
return s.UpdateExternalSessionProvider(context.Background(), externalSessionID, cmd)
|
||||
}
|
||||
|
||||
func (s *FakeUserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) {
|
||||
return s.LookupTokenProvider(context.Background(), unhashedToken)
|
||||
}
|
||||
@@ -149,16 +156,6 @@ func (ts *FakeOAuthTokenService) IsOAuthPassThruEnabled(*datasources.DataSource)
|
||||
return ts.passThruEnabled
|
||||
}
|
||||
|
||||
func (ts *FakeOAuthTokenService) HasOAuthEntry(context.Context, identity.Requester) (*login.UserAuth, bool, error) {
|
||||
if ts.ExpectedAuthUser != nil {
|
||||
return ts.ExpectedAuthUser, true, nil
|
||||
}
|
||||
if error, ok := ts.ExpectedErrors["HasOAuthEntry"]; ok {
|
||||
return nil, false, error
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (ts *FakeOAuthTokenService) InvalidateOAuthTokens(ctx context.Context, usr *login.UserAuth) error {
|
||||
ts.ExpectedAuthUser.OAuthAccessToken = ""
|
||||
ts.ExpectedAuthUser.OAuthRefreshToken = ""
|
||||
|
||||
@@ -3,6 +3,8 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type ExternalSession struct {
|
||||
@@ -43,6 +45,10 @@ func (e *ExternalSession) Clone() *ExternalSession {
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateExternalSessionCommand struct {
|
||||
Token *oauth2.Token
|
||||
}
|
||||
|
||||
type ListExternalSessionQuery struct {
|
||||
ID int64
|
||||
NameID string
|
||||
@@ -57,6 +63,8 @@ type ExternalSessionStore interface {
|
||||
List(ctx context.Context, query *ListExternalSessionQuery) ([]*ExternalSession, error)
|
||||
// Create creates a new external session for a user
|
||||
Create(ctx context.Context, extSesion *ExternalSession) error
|
||||
// Update updates an external session
|
||||
Update(ctx context.Context, ID int64, cmd *UpdateExternalSessionCommand) error
|
||||
// Delete deletes an external session
|
||||
Delete(ctx context.Context, ID int64) error
|
||||
// DeleteExternalSessionsByUserID deletes an external session
|
||||
|
||||
@@ -178,7 +178,7 @@ type RedirectClient interface {
|
||||
// that should happen during logout and supports client specific redirect URL.
|
||||
type LogoutClient interface {
|
||||
Client
|
||||
Logout(ctx context.Context, user identity.Requester) (*Redirect, bool)
|
||||
Logout(ctx context.Context, user identity.Requester, sessionToken *usertoken.UserToken) (*Redirect, bool)
|
||||
}
|
||||
|
||||
type SSOSettingsAwareClient interface {
|
||||
|
||||
@@ -37,7 +37,7 @@ func ProvideRegistration(
|
||||
jwtService auth.JWTVerifierService, userProtectionService login.UserProtectionService,
|
||||
loginAttempts loginattempt.Service, quotaService quota.Service,
|
||||
authInfoService login.AuthInfoService, renderService rendering.Service,
|
||||
features *featuremgmt.FeatureManager, oauthTokenService oauthtoken.OAuthTokenService,
|
||||
features featuremgmt.FeatureToggles, oauthTokenService oauthtoken.OAuthTokenService,
|
||||
socialService social.Service, cache *remotecache.RemoteCache,
|
||||
ldapService service.LDAP, settingsProviderService setting.Provider,
|
||||
tracer tracing.Tracer, tempUserService tempuser.Service, notificationService notifications.Service,
|
||||
@@ -108,13 +108,13 @@ func ProvideRegistration(
|
||||
}
|
||||
|
||||
// FIXME (jguer): move to User package
|
||||
userSync := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService, tracer)
|
||||
userSync := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService, tracer, features)
|
||||
orgSync := sync.ProvideOrgSync(userService, orgService, accessControlService, cfg, tracer)
|
||||
authnSvc.RegisterPostAuthHook(userSync.SyncUserHook, 10)
|
||||
authnSvc.RegisterPostAuthHook(userSync.EnableUserHook, 20)
|
||||
authnSvc.RegisterPostAuthHook(orgSync.SyncOrgRolesHook, 30)
|
||||
authnSvc.RegisterPostAuthHook(userSync.SyncLastSeenHook, 130)
|
||||
authnSvc.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService, tracer).SyncOauthTokenHook, 60)
|
||||
authnSvc.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService, tracer, features).SyncOauthTokenHook, 60)
|
||||
authnSvc.RegisterPostAuthHook(userSync.FetchSyncedUserHook, 100)
|
||||
|
||||
rbacSync := sync.ProvideRBACSync(accessControlService, tracer, permRegistry)
|
||||
|
||||
@@ -322,7 +322,7 @@ func (s *Service) Logout(ctx context.Context, user identity.Requester, sessionTo
|
||||
goto Default
|
||||
}
|
||||
|
||||
clientRedirect, ok := logoutClient.Logout(ctx, user)
|
||||
clientRedirect, ok := logoutClient.Logout(ctx, user, sessionToken)
|
||||
if !ok {
|
||||
goto Default
|
||||
}
|
||||
|
||||
@@ -507,7 +507,7 @@ func TestService_Logout(t *testing.T) {
|
||||
expectedRedirect: &authn.Redirect{URL: "http://idp.com/logout"},
|
||||
client: &authntest.MockClient{
|
||||
NameFunc: func() string { return "auth.client.azuread" },
|
||||
LogoutFunc: func(ctx context.Context, _ identity.Requester) (*authn.Redirect, bool) {
|
||||
LogoutFunc: func(ctx context.Context, _ identity.Requester, sessionToken *usertoken.UserToken) (*authn.Redirect, bool) {
|
||||
return &authn.Redirect{URL: "http://idp.com/logout"}, true
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,12 +17,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
)
|
||||
|
||||
const maxOAuthTokenCacheTTL = 5 * time.Minute
|
||||
|
||||
func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService auth.UserTokenService, socialService social.Service, tracer tracing.Tracer) *OAuthTokenSync {
|
||||
func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService auth.UserTokenService, socialService social.Service, tracer tracing.Tracer,
|
||||
features featuremgmt.FeatureToggles,
|
||||
) *OAuthTokenSync {
|
||||
return &OAuthTokenSync{
|
||||
log.New("oauth_token.sync"),
|
||||
service,
|
||||
@@ -31,6 +34,7 @@ func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService
|
||||
new(singleflight.Group),
|
||||
tracer,
|
||||
localcache.New(maxOAuthTokenCacheTTL, 15*time.Minute),
|
||||
features,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +46,7 @@ type OAuthTokenSync struct {
|
||||
singleflightGroup *singleflight.Group
|
||||
tracer tracing.Tracer
|
||||
cache *localcache.CacheService
|
||||
features featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error {
|
||||
@@ -72,6 +77,10 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, id *authn.Ident
|
||||
ctxLogger := s.log.FromContext(ctx).New("userID", userID)
|
||||
|
||||
cacheKey := fmt.Sprintf("token-check-%s", id.GetID())
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
cacheKey = fmt.Sprintf("token-check-%s-%d", id.GetID(), id.SessionToken.Id)
|
||||
}
|
||||
|
||||
if _, ok := s.cache.Get(cacheKey); ok {
|
||||
ctxLogger.Debug("Expiration check has been cached, no need to refresh")
|
||||
return nil
|
||||
@@ -83,7 +92,7 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, id *authn.Ident
|
||||
updateCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
token, refreshErr := s.service.TryTokenRefresh(updateCtx, id)
|
||||
token, refreshErr := s.service.TryTokenRefresh(updateCtx, id, id.SessionToken)
|
||||
if refreshErr != nil {
|
||||
if errors.Is(refreshErr, context.Canceled) {
|
||||
return nil, nil
|
||||
|
||||
@@ -20,6 +20,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/auth/authtest"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
|
||||
)
|
||||
@@ -85,7 +88,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) {
|
||||
)
|
||||
|
||||
service := &oauthtokentest.MockOauthTokenService{
|
||||
TryTokenRefreshFunc: func(ctx context.Context, usr identity.Requester) (*oauth2.Token, error) {
|
||||
TryTokenRefreshFunc: func(ctx context.Context, usr identity.Requester, _ *auth.UserToken) (*oauth2.Token, error) {
|
||||
tryRefreshCalled = true
|
||||
return nil, tt.expectedTryRefreshErr
|
||||
},
|
||||
@@ -116,9 +119,13 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) {
|
||||
singleflightGroup: new(singleflight.Group),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
cache: localcache.New(maxOAuthTokenCacheTTL, 15*time.Minute),
|
||||
features: featuremgmt.WithFeatures(),
|
||||
}
|
||||
|
||||
err := sync.SyncOauthTokenHook(context.Background(), tt.identity, nil)
|
||||
ctx := context.Background()
|
||||
reqCtx := context.WithValue(ctx, ctxkey.Key{}, &contextmodel.ReqContext{UserToken: nil})
|
||||
|
||||
err := sync.SyncOauthTokenHook(reqCtx, tt.identity, nil)
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
assert.Equal(t, tt.expectTryRefreshTokenCalled, tryRefreshCalled)
|
||||
assert.Equal(t, tt.expectRevokeTokenCalled, revokeTokenCalled)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
@@ -51,7 +52,9 @@ var (
|
||||
errSignupNotAllowed = errors.New("system administrator has disabled signup")
|
||||
)
|
||||
|
||||
func ProvideUserSync(userService user.Service, userProtectionService login.UserProtectionService, authInfoService login.AuthInfoService, quotaService quota.Service, tracer tracing.Tracer) *UserSync {
|
||||
func ProvideUserSync(userService user.Service, userProtectionService login.UserProtectionService, authInfoService login.AuthInfoService,
|
||||
quotaService quota.Service, tracer tracing.Tracer, features featuremgmt.FeatureToggles,
|
||||
) *UserSync {
|
||||
return &UserSync{
|
||||
userService: userService,
|
||||
authInfoService: authInfoService,
|
||||
@@ -59,6 +62,7 @@ func ProvideUserSync(userService user.Service, userProtectionService login.UserP
|
||||
quotaService: quotaService,
|
||||
log: log.New("user.sync"),
|
||||
tracer: tracer,
|
||||
features: features,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +73,7 @@ type UserSync struct {
|
||||
quotaService quota.Service
|
||||
log log.Logger
|
||||
tracer tracing.Tracer
|
||||
features featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
// SyncUserHook syncs a user with the database
|
||||
@@ -223,21 +228,30 @@ func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, ident
|
||||
// This can happen when: using multiple auth client where the same user exists in several or
|
||||
// changing to new auth client
|
||||
if createConnection {
|
||||
return s.authInfoService.SetAuthInfo(ctx, &login.SetAuthInfoCommand{
|
||||
setAuthInfoCmd := &login.SetAuthInfoCommand{
|
||||
UserId: userID,
|
||||
AuthModule: identity.AuthenticatedBy,
|
||||
AuthId: identity.AuthID,
|
||||
OAuthToken: identity.OAuthToken,
|
||||
})
|
||||
}
|
||||
|
||||
if !s.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
setAuthInfoCmd.OAuthToken = identity.OAuthToken
|
||||
}
|
||||
return s.authInfoService.SetAuthInfo(ctx, setAuthInfoCmd)
|
||||
}
|
||||
|
||||
s.log.FromContext(ctx).Debug("Updating auth connection for user", "id", identity.ID)
|
||||
return s.authInfoService.UpdateAuthInfo(ctx, &login.UpdateAuthInfoCommand{
|
||||
updateAuthInfoCmd := &login.UpdateAuthInfoCommand{
|
||||
UserId: userID,
|
||||
AuthId: identity.AuthID,
|
||||
AuthModule: identity.AuthenticatedBy,
|
||||
OAuthToken: identity.OAuthToken,
|
||||
})
|
||||
}
|
||||
|
||||
if !s.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
updateAuthInfoCmd.OAuthToken = identity.OAuthToken
|
||||
}
|
||||
|
||||
s.log.FromContext(ctx).Debug("Updating auth connection for user", "id", identity.ID)
|
||||
return s.authInfoService.UpdateAuthInfo(ctx, updateAuthInfoCmd)
|
||||
}
|
||||
|
||||
func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id *authn.Identity, userAuth *login.UserAuth) error {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfoimpl"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfotest"
|
||||
@@ -45,7 +46,9 @@ func TestUserSync_SyncUserHook(t *testing.T) {
|
||||
AuthModule: "oauth",
|
||||
AuthId: "2032",
|
||||
UserId: 1,
|
||||
Id: 1}}
|
||||
Id: 1,
|
||||
},
|
||||
}
|
||||
|
||||
userService := &usertest.FakeUserService{ExpectedUser: &user.User{
|
||||
ID: 1,
|
||||
@@ -434,7 +437,7 @@ func TestUserSync_SyncUserHook(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := ProvideUserSync(tt.fields.userService, userProtection, tt.fields.authInfoService, tt.fields.quotaService, tracing.InitializeTracerForTest())
|
||||
s := ProvideUserSync(tt.fields.userService, userProtection, tt.fields.authInfoService, tt.fields.quotaService, tracing.InitializeTracerForTest(), featuremgmt.WithFeatures())
|
||||
err := s.SyncUserHook(tt.args.ctx, tt.args.id, nil)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
|
||||
@@ -85,7 +85,7 @@ type MockClient struct {
|
||||
TestFunc func(ctx context.Context, r *authn.Request) bool
|
||||
PriorityFunc func() uint
|
||||
HookFunc func(ctx context.Context, identity *authn.Identity, r *authn.Request) error
|
||||
LogoutFunc func(ctx context.Context, user identity.Requester) (*authn.Redirect, bool)
|
||||
LogoutFunc func(ctx context.Context, user identity.Requester, sessionToken *usertoken.UserToken) (*authn.Redirect, bool)
|
||||
IdentityTypeFunc func() claims.IdentityType
|
||||
ResolveIdentityFunc func(ctx context.Context, orgID int64, typ claims.IdentityType, id string) (*authn.Identity, error)
|
||||
}
|
||||
@@ -133,9 +133,9 @@ func (m MockClient) Hook(ctx context.Context, identity *authn.Identity, r *authn
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockClient) Logout(ctx context.Context, user identity.Requester) (*authn.Redirect, bool) {
|
||||
func (m *MockClient) Logout(ctx context.Context, user identity.Requester, sessionToken *usertoken.UserToken) (*authn.Redirect, bool) {
|
||||
if m.LogoutFunc != nil {
|
||||
return m.LogoutFunc(ctx, user)
|
||||
return m.LogoutFunc(ctx, user, sessionToken)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/login/social/connectors"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
@@ -257,8 +258,8 @@ func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redir
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *OAuth) Logout(ctx context.Context, user identity.Requester) (*authn.Redirect, bool) {
|
||||
token := c.oauthService.GetCurrentOAuthToken(ctx, user)
|
||||
func (c *OAuth) Logout(ctx context.Context, user identity.Requester, sessionToken *auth.UserToken) (*authn.Redirect, bool) {
|
||||
token := c.oauthService.GetCurrentOAuthToken(ctx, user, sessionToken)
|
||||
|
||||
userID, err := identity.UserIdentifier(user.GetID())
|
||||
if err != nil {
|
||||
@@ -268,7 +269,7 @@ func (c *OAuth) Logout(ctx context.Context, user identity.Requester) (*authn.Red
|
||||
|
||||
ctxLogger := c.log.FromContext(ctx).New("userID", userID)
|
||||
|
||||
if err := c.oauthService.InvalidateOAuthTokens(ctx, user); err != nil {
|
||||
if err := c.oauthService.InvalidateOAuthTokens(ctx, user, sessionToken); err != nil {
|
||||
ctxLogger.Error("Failed to invalidate tokens", "error", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/login/social/socialtest"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
@@ -469,7 +470,7 @@ func TestOAuth_Logout(t *testing.T) {
|
||||
)
|
||||
|
||||
mockService := &oauthtokentest.MockOauthTokenService{
|
||||
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester) *oauth2.Token {
|
||||
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester, _ *auth.UserToken) *oauth2.Token {
|
||||
getTokenCalled = true
|
||||
token := &oauth2.Token{
|
||||
AccessToken: "some.access.token",
|
||||
@@ -479,7 +480,7 @@ func TestOAuth_Logout(t *testing.T) {
|
||||
"id_token": "some.id.token",
|
||||
})
|
||||
},
|
||||
InvalidateOAuthTokensFunc: func(_ context.Context, _ identity.Requester) error {
|
||||
InvalidateOAuthTokensFunc: func(_ context.Context, _ identity.Requester, _ *auth.UserToken) error {
|
||||
invalidateTokenCalled = true
|
||||
return nil
|
||||
},
|
||||
@@ -490,7 +491,7 @@ func TestOAuth_Logout(t *testing.T) {
|
||||
}
|
||||
c := ProvideOAuth(authn.ClientWithPrefix("azuread"), tt.cfg, mockService, fakeSocialSvc, &setting.OSSImpl{Cfg: tt.cfg}, featuremgmt.WithFeatures())
|
||||
|
||||
redirect, ok := c.Logout(context.Background(), &authn.Identity{ID: "1", Type: claims.TypeUser})
|
||||
redirect, ok := c.Logout(context.Background(), &authn.Identity{ID: "1", Type: claims.TypeUser}, nil)
|
||||
|
||||
assert.Equal(t, tt.expectedOK, ok)
|
||||
if tt.expectedOK {
|
||||
|
||||
@@ -3,6 +3,8 @@ package grpcutils
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -26,6 +28,11 @@ type GrpcServerConfig struct {
|
||||
AllowedAudiences []string
|
||||
Mode Mode
|
||||
LegacyFallback bool
|
||||
AllowInsecure bool
|
||||
}
|
||||
|
||||
func (c *GrpcServerConfig) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.StringVar(&c.SigningKeysURL, "grpc-server-authentication.signing-keys-url", "", "gRPC server authentication signing keys URL")
|
||||
}
|
||||
|
||||
func ReadGrpcServerConfig(cfg *setting.Cfg) (*GrpcServerConfig, error) {
|
||||
@@ -41,6 +48,7 @@ func ReadGrpcServerConfig(cfg *setting.Cfg) (*GrpcServerConfig, error) {
|
||||
AllowedAudiences: section.Key("allowed_audiences").Strings(","),
|
||||
Mode: mode,
|
||||
LegacyFallback: section.Key("legacy_fallback").MustBool(true),
|
||||
AllowInsecure: cfg.Env == setting.Dev,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -27,11 +27,7 @@ func NewInProcGrpcAuthenticator() *authnlib.GrpcAuthenticator {
|
||||
)
|
||||
}
|
||||
|
||||
func NewGrpcAuthenticator(cfg *setting.Cfg, tracer tracing.Tracer) (*authnlib.GrpcAuthenticator, error) {
|
||||
authCfg, err := ReadGrpcServerConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewGrpcAuthenticator(authCfg *GrpcServerConfig, tracer tracing.Tracer) (*authnlib.GrpcAuthenticator, error) {
|
||||
grpcAuthCfg := authnlib.GrpcAuthenticatorConfig{
|
||||
KeyRetrieverConfig: authnlib.KeyRetrieverConfig{
|
||||
SigningKeysURL: authCfg.SigningKeysURL,
|
||||
@@ -42,7 +38,7 @@ func NewGrpcAuthenticator(cfg *setting.Cfg, tracer tracing.Tracer) (*authnlib.Gr
|
||||
}
|
||||
|
||||
client := http.DefaultClient
|
||||
if cfg.Env == setting.Dev {
|
||||
if authCfg.AllowInsecure {
|
||||
// allow insecure connections in development mode to facilitate testing
|
||||
client = &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}
|
||||
}
|
||||
@@ -87,7 +83,7 @@ func NewGrpcAuthenticatorWithFallback(cfg *setting.Cfg, reg prometheus.Registere
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authenticator, err := NewGrpcAuthenticator(cfg, tracer)
|
||||
authenticator, err := NewGrpcAuthenticator(authCfg, tracer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -111,42 +107,49 @@ func FallbackUsed(ctx context.Context) bool {
|
||||
func (f *AuthenticatorWithFallback) Authenticate(ctx context.Context) (context.Context, error) {
|
||||
ctx, span := f.tracer.Start(ctx, "grpcutils.AuthenticatorWithFallback.Authenticate")
|
||||
defer span.End()
|
||||
span.SetAttributes(attribute.Bool("fallback_used", false))
|
||||
|
||||
// Try to authenticate with the new authenticator first
|
||||
span.SetAttributes(attribute.Bool("fallback_used", false))
|
||||
newCtx, err := f.authenticator.Authenticate(ctx)
|
||||
if err != nil {
|
||||
// In case of error, fallback to the legacy authenticator
|
||||
newCtx, err = f.fallback.Authenticate(ctx)
|
||||
f.metrics.fallbackCounter.WithLabelValues(fmt.Sprintf("%t", err == nil)).Inc()
|
||||
span.SetAttributes(attribute.Bool("fallback_used", true))
|
||||
if err == nil {
|
||||
// fallback not used, authentication successful
|
||||
f.metrics.requestsTotal.WithLabelValues("false", "true").Inc()
|
||||
return newCtx, nil
|
||||
}
|
||||
|
||||
// In case of error, fallback to the legacy authenticator
|
||||
span.SetAttributes(attribute.Bool("fallback_used", true))
|
||||
newCtx, err = f.fallback.Authenticate(ctx)
|
||||
if newCtx != nil {
|
||||
newCtx = context.WithValue(newCtx, contextFallbackKey{}, true)
|
||||
}
|
||||
f.metrics.requestsTotal.WithLabelValues("true", fmt.Sprintf("%t", err == nil)).Inc()
|
||||
return newCtx, err
|
||||
}
|
||||
|
||||
const (
|
||||
metricsNamespace = "grafana"
|
||||
metricsSubSystem = "grpc_authenticator"
|
||||
metricsSubSystem = "grpc_authenticator_with_fallback"
|
||||
)
|
||||
|
||||
type metrics struct {
|
||||
fallbackCounter *prometheus.CounterVec
|
||||
requestsTotal *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func newMetrics(reg prometheus.Registerer) *metrics {
|
||||
m := &metrics{
|
||||
fallbackCounter: prometheus.NewCounterVec(
|
||||
requestsTotal: prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: metricsSubSystem,
|
||||
Name: "fallback_total",
|
||||
Help: "Number of times the fallback authenticator was used",
|
||||
}, []string{"result"}),
|
||||
Name: "requests_total",
|
||||
Help: "Number requests using the authenticator with fallback",
|
||||
}, []string{"fallback_used", "result"}),
|
||||
}
|
||||
|
||||
if reg != nil {
|
||||
once.Do(func() {
|
||||
reg.MustRegister(m.fallbackCounter)
|
||||
reg.MustRegister(m.requestsTotal)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type TypeInfo struct {
|
||||
Type string
|
||||
Type string
|
||||
Relations []string
|
||||
}
|
||||
|
||||
var typedResources = map[string]TypeInfo{
|
||||
FormatGroupResource(
|
||||
folderalpha1.FolderResourceInfo.GroupResource().Group,
|
||||
folderalpha1.FolderResourceInfo.GroupResource().Resource,
|
||||
): {Type: "folder"},
|
||||
): {Type: "folder", Relations: append(ResourceRelations, RelationCreate)},
|
||||
}
|
||||
|
||||
func GetTypeInfo(group, resource string) (TypeInfo, bool) {
|
||||
|
||||
@@ -10,12 +10,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
TypeUser string = "user"
|
||||
TypeTeam string = "team"
|
||||
TypeRole string = "role"
|
||||
TypeFolder string = "folder"
|
||||
TypeResource string = "resource"
|
||||
TypeNamespace string = "namespace"
|
||||
TypeUser string = "user"
|
||||
TypeServiceAccount string = "service-account"
|
||||
TypeTeam string = "team"
|
||||
TypeRole string = "role"
|
||||
TypeFolder string = "folder"
|
||||
TypeResource string = "resource"
|
||||
TypeNamespace string = "namespace"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -50,7 +51,6 @@ const (
|
||||
var ResourceRelations = []string{
|
||||
RelationRead,
|
||||
RelationWrite,
|
||||
RelationCreate,
|
||||
RelationDelete,
|
||||
RelationPermissionsRead,
|
||||
RelationPermissionsWrite,
|
||||
@@ -58,6 +58,7 @@ var ResourceRelations = []string{
|
||||
|
||||
var FolderRelations = append(
|
||||
ResourceRelations,
|
||||
RelationCreate,
|
||||
RelationFolderResourceRead,
|
||||
RelationFolderResourceWrite,
|
||||
RelationFolderResourceCreate,
|
||||
|
||||
@@ -2,28 +2,30 @@ module core
|
||||
|
||||
type namespace
|
||||
relations
|
||||
define view: [user, team#member, role#assignee] or edit
|
||||
define edit: [user, team#member, role#assignee] or admin
|
||||
define admin: [user, team#member, role#assignee]
|
||||
define view: [user, service-account, team#member, role#assignee] or edit
|
||||
define edit: [user, service-account, team#member, role#assignee] or admin
|
||||
define admin: [user, service-account, team#member, role#assignee]
|
||||
|
||||
define read: [user, team#member, role#assignee] or view
|
||||
define create: [user, team#member, role#assignee] or edit
|
||||
define write: [user, team#member, role#assignee] or edit
|
||||
define delete: [user, team#member, role#assignee] or edit
|
||||
define permissions_read: [user, team#member, role#assignee] or admin
|
||||
define permissions_write: [user, team#member, role#assignee] or admin
|
||||
define read: [user, service-account, team#member, role#assignee] or view
|
||||
define create: [user, service-account, team#member, role#assignee] or edit
|
||||
define write: [user, service-account, team#member, role#assignee] or edit
|
||||
define delete: [user, service-account, team#member, role#assignee] or edit
|
||||
define permissions_read: [user, service-account, team#member, role#assignee] or admin
|
||||
define permissions_write: [user, service-account, team#member, role#assignee] or admin
|
||||
|
||||
type user
|
||||
|
||||
type service-account
|
||||
|
||||
type role
|
||||
relations
|
||||
define assignee: [user, team#member, role#assignee]
|
||||
define assignee: [user, service-account, team#member, role#assignee]
|
||||
|
||||
type team
|
||||
relations
|
||||
# Action sets
|
||||
define admin: [user]
|
||||
define member: [user] or admin
|
||||
define admin: [user, service-account]
|
||||
define member: [user, service-account] or admin
|
||||
|
||||
define read: [role#assignee] or member
|
||||
define write: [role#assignee] or admin
|
||||
|
||||
@@ -5,13 +5,13 @@ type folder
|
||||
define parent: [folder]
|
||||
|
||||
# Action sets
|
||||
define view: [user, team#member, role#assignee] or edit or view from parent
|
||||
define edit: [user, team#member, role#assignee] or admin or edit from parent
|
||||
define admin: [user, team#member, role#assignee] or admin from parent
|
||||
define view: [user, service-account, team#member, role#assignee] or edit or view from parent
|
||||
define edit: [user, service-account, team#member, role#assignee] or admin or edit from parent
|
||||
define admin: [user, service-account, team#member, role#assignee] or admin from parent
|
||||
|
||||
define read: [user, team#member, role#assignee] or view or read from parent
|
||||
define create: [user, team#member, role#assignee] or edit or create from parent
|
||||
define write: [user, team#member, role#assignee] or edit or write from parent
|
||||
define delete: [user, team#member, role#assignee] or edit or delete from parent
|
||||
define permissions_read: [user, team#member, role#assignee] or admin or permissions_read from parent
|
||||
define permissions_write: [user, team#member, role#assignee] or admin or permissions_write from parent
|
||||
define read: [user, service-account, team#member, role#assignee] or view or read from parent
|
||||
define create: [user, service-account, team#member, role#assignee] or edit or create from parent
|
||||
define write: [user, service-account, team#member, role#assignee] or edit or write from parent
|
||||
define delete: [user, service-account, team#member, role#assignee] or edit or delete from parent
|
||||
define permissions_read: [user, service-account, team#member, role#assignee] or admin or permissions_read from parent
|
||||
define permissions_write: [user, service-account, team#member, role#assignee] or admin or permissions_write from parent
|
||||
|
||||
@@ -2,29 +2,28 @@ module resource
|
||||
|
||||
extend type folder
|
||||
relations
|
||||
define resource_view: [user, team#member, role#assignee] or resource_edit or resource_view from parent
|
||||
define resource_edit: [user, team#member, role#assignee] or resource_admin or resource_edit from parent
|
||||
define resource_admin: [user, team#member, role#assignee] or resource_admin from parent
|
||||
define resource_view: [user, service-account, team#member, role#assignee] or resource_edit or resource_view from parent
|
||||
define resource_edit: [user, service-account, team#member, role#assignee] or resource_admin or resource_edit from parent
|
||||
define resource_admin: [user, service-account, team#member, role#assignee] or resource_admin from parent
|
||||
|
||||
define resource_read: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_view or resource_read from parent
|
||||
define resource_create: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_create from parent
|
||||
define resource_write: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_write from parent
|
||||
define resource_delete: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_delete from parent
|
||||
define resource_permissions_read: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_permissions_read from parent
|
||||
define resource_permissions_write: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_permissions_write from parent
|
||||
define resource_read: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_view or resource_read from parent
|
||||
define resource_create: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_create from parent
|
||||
define resource_write: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_write from parent
|
||||
define resource_delete: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_delete from parent
|
||||
define resource_permissions_read: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_permissions_read from parent
|
||||
define resource_permissions_write: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_permissions_write from parent
|
||||
|
||||
type resource
|
||||
relations
|
||||
define view: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
|
||||
define edit: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin
|
||||
define admin: [user with group_filter, team#member with group_filter, role#assignee with group_filter]
|
||||
define view: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
|
||||
define edit: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or admin
|
||||
define admin: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter]
|
||||
|
||||
define read: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or view
|
||||
define create: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
|
||||
define write: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
|
||||
define delete: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
|
||||
define permissions_read: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin
|
||||
define permissions_write: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin
|
||||
define read: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or view
|
||||
define write: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
|
||||
define delete: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
|
||||
define permissions_read: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or admin
|
||||
define permissions_write: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or admin
|
||||
|
||||
condition group_filter(requested_group: string, group_resource: string) {
|
||||
requested_group == group_resource
|
||||
|
||||
@@ -2,12 +2,15 @@ package server
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fullstorydev/grpchan/inprocgrpc"
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"github.com/openfga/language/pkg/go/transformer"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -17,6 +20,8 @@ const (
|
||||
resourceType = "resource"
|
||||
namespaceType = "namespace"
|
||||
folderTypePrefix = "folder:"
|
||||
|
||||
cacheCleanInterval = 2 * time.Minute
|
||||
)
|
||||
|
||||
var _ authzv1.AuthzServiceServer = (*Server)(nil)
|
||||
@@ -28,12 +33,15 @@ type Server struct {
|
||||
authzv1.UnimplementedAuthzServiceServer
|
||||
authzextv1.UnimplementedAuthzExtentionServiceServer
|
||||
|
||||
openfga openfgav1.OpenFGAServiceServer
|
||||
openfga openfgav1.OpenFGAServiceServer
|
||||
openfgaClient openfgav1.OpenFGAServiceClient
|
||||
|
||||
cfg setting.ZanzanaSettings
|
||||
logger log.Logger
|
||||
modules []transformer.ModuleFile
|
||||
stores map[string]storeInfo
|
||||
storesMU *sync.Mutex
|
||||
cache *localcache.CacheService
|
||||
}
|
||||
|
||||
type storeInfo struct {
|
||||
@@ -56,14 +64,21 @@ func WithSchema(modules []transformer.ModuleFile) ServerOption {
|
||||
}
|
||||
|
||||
func NewAuthzServer(cfg *setting.Cfg, openfga openfgav1.OpenFGAServiceServer) (*Server, error) {
|
||||
return NewAuthz(openfga)
|
||||
return NewAuthz(cfg, openfga)
|
||||
}
|
||||
|
||||
func NewAuthz(openfga openfgav1.OpenFGAServiceServer, opts ...ServerOption) (*Server, error) {
|
||||
func NewAuthz(cfg *setting.Cfg, openfga openfgav1.OpenFGAServiceServer, opts ...ServerOption) (*Server, error) {
|
||||
channel := &inprocgrpc.Channel{}
|
||||
openfgav1.RegisterOpenFGAServiceServer(channel, openfga)
|
||||
openFGAClient := openfgav1.NewOpenFGAServiceClient(channel)
|
||||
|
||||
s := &Server{
|
||||
openfga: openfga,
|
||||
storesMU: &sync.Mutex{},
|
||||
stores: make(map[string]storeInfo),
|
||||
openfga: openfga,
|
||||
openfgaClient: openFGAClient,
|
||||
storesMU: &sync.Mutex{},
|
||||
stores: make(map[string]storeInfo),
|
||||
cfg: cfg.Zanzana,
|
||||
cache: localcache.New(cfg.Zanzana.CheckQueryCacheTTL, cacheCleanInterval),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
|
||||
@@ -60,6 +60,8 @@ func (s *Server) batchCheckItem(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allowed = res.GetAllowed()
|
||||
groupResourceAccess[groupResource] = res.GetAllowed()
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
)
|
||||
|
||||
func testBatchCheck(t *testing.T, server *Server) {
|
||||
newReq := func(subject, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
|
||||
newReq := func(subject, verb, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
|
||||
for i, item := range items {
|
||||
items[i] = &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Name: item.GetName(),
|
||||
@@ -32,89 +32,116 @@ func testBatchCheck(t *testing.T, server *Server) {
|
||||
}
|
||||
|
||||
t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:1", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupPrefix].Items["1"])
|
||||
assert.False(t, res.Groups[groupPrefix].Items["2"])
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through namespace", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:2", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
assert.Len(t, res.Groups[groupResource].Items, 2)
|
||||
})
|
||||
|
||||
t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:3", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupPrefix].Items["1"])
|
||||
assert.False(t, res.Groups[groupPrefix].Items["2"])
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:4", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "3"},
|
||||
{Name: "3", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 3)
|
||||
require.Len(t, res.Groups[groupResource].Items, 3)
|
||||
|
||||
assert.True(t, res.Groups[groupPrefix].Items["1"])
|
||||
assert.True(t, res.Groups[groupPrefix].Items["2"])
|
||||
assert.False(t, res.Groups[groupPrefix].Items["3"])
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.True(t, res.Groups[groupResource].Items["2"])
|
||||
assert.False(t, res.Groups[groupResource].Items["3"])
|
||||
})
|
||||
|
||||
t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:5", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupPrefix].Items["1"])
|
||||
assert.False(t, res.Groups[groupPrefix].Items["2"])
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(folderGroup, folderResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:6", folderGroup, folderResource, []*authzextv1.BatchCheckItem{
|
||||
groupResource := zanzana.FormatGroupResource(folderGroup, folderResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1"},
|
||||
{Name: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupPrefix].Items["1"])
|
||||
assert.False(t, res.Groups[groupPrefix].Items["2"])
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:7 should be able to read folder {1,2} through namespace access", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(folderGroup, folderResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:7", folderGroup, folderResource, []*authzextv1.BatchCheckItem{
|
||||
groupResource := zanzana.FormatGroupResource(folderGroup, folderResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1"},
|
||||
{Name: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["1"])
|
||||
require.True(t, res.Groups[groupResource].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "20", Folder: "6"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["20"])
|
||||
})
|
||||
|
||||
t.Run("user:9 should be able to create dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "20", Folder: "6"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
t.Log(res.Groups)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["20"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func (s *Server) Capabilities(ctx context.Context, r *authzextv1.CapabilitiesReq
|
||||
|
||||
func (s *Server) capabilitiesTyped(ctx context.Context, r *authzextv1.CapabilitiesRequest, info common.TypeInfo, store *storeInfo) (*authzextv1.CapabilitiesResponse, error) {
|
||||
out := make([]string, 0, len(common.ResourceRelations))
|
||||
for _, relation := range common.ResourceRelations {
|
||||
for _, relation := range info.Relations {
|
||||
res, err := s.checkNamespace(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -50,7 +50,7 @@ func testCapabilities(t *testing.T, server *Server) {
|
||||
t.Run("user:5 should be able to read, write, create and delete resource:dashboards.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
|
||||
res, err := server.Capabilities(context.Background(), newReq("user:5", dashboardGroup, dashboardResource, "1", "1"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{common.RelationRead, common.RelationWrite, common.RelationCreate, common.RelationDelete}, res.GetCapabilities())
|
||||
assert.Equal(t, []string{common.RelationRead, common.RelationWrite, common.RelationDelete}, res.GetCapabilities())
|
||||
})
|
||||
|
||||
t.Run("user:6 should be able to read folder 1 ", func(t *testing.T) {
|
||||
|
||||
@@ -83,28 +83,32 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation, name string,
|
||||
func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, resource, name, folder string, store *storeInfo) (*authzv1.CheckResponse, error) {
|
||||
groupResource := structpb.NewStringValue(common.FormatGroupResource(group, resource))
|
||||
|
||||
// Check if subject has direct access to resource
|
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: subject,
|
||||
Relation: relation,
|
||||
Object: common.NewResourceIdent(group, resource, name),
|
||||
},
|
||||
Context: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"requested_group": groupResource,
|
||||
// Create relation can only exist on namespace or folder level.
|
||||
// So we skip direct resource access check.
|
||||
if relation != common.RelationCreate {
|
||||
// Check if subject has direct access to resource
|
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: subject,
|
||||
Relation: relation,
|
||||
Object: common.NewResourceIdent(group, resource, name),
|
||||
},
|
||||
},
|
||||
})
|
||||
Context: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"requested_group": groupResource,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.GetAllowed() {
|
||||
return &authzv1.CheckResponse{Allowed: true}, nil
|
||||
if res.GetAllowed() {
|
||||
return &authzv1.CheckResponse{Allowed: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
if folder == "" {
|
||||
@@ -112,7 +116,7 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res
|
||||
}
|
||||
|
||||
// Check if subject has access as a sub resource for the folder
|
||||
res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{
|
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
)
|
||||
|
||||
func testCheck(t *testing.T, server *Server) {
|
||||
newRead := func(subject, group, resource, folder, name string) *authzv1.CheckRequest {
|
||||
newReq := func(subject, verb, group, resource, folder, name string) *authzv1.CheckRequest {
|
||||
return &authzv1.CheckRequest{
|
||||
Namespace: namespace,
|
||||
Subject: subject,
|
||||
Verb: utils.VerbGet,
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Name: name,
|
||||
@@ -25,85 +25,91 @@ func testCheck(t *testing.T, server *Server) {
|
||||
}
|
||||
|
||||
t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
|
||||
res, err := server.Check(context.Background(), newRead("user:1", dashboardGroup, dashboardResource, "1", "1"))
|
||||
res, err := server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
// sanity check
|
||||
res, err = server.Check(context.Background(), newRead("user:1", dashboardGroup, dashboardResource, "1", "2"))
|
||||
res, err = server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "1", "2"))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/1 through namespace", func(t *testing.T) {
|
||||
res, err := server.Check(context.Background(), newRead("user:2", dashboardGroup, dashboardResource, "1", "1"))
|
||||
res, err := server.Check(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
|
||||
res, err := server.Check(context.Background(), newRead("user:3", dashboardGroup, dashboardResource, "1", "1"))
|
||||
res, err := server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
// sanity check
|
||||
res, err = server.Check(context.Background(), newRead("user:3", dashboardGroup, dashboardResource, "1", "2"))
|
||||
res, err = server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "1", "2"))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
|
||||
res, err := server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "1", "1"))
|
||||
res, err := server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "3", "2"))
|
||||
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "3", "2"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
// sanity check
|
||||
res, err = server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "1", "2"))
|
||||
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "1", "2"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "2", "2"))
|
||||
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "2", "2"))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
|
||||
res, err := server.Check(context.Background(), newRead("user:5", dashboardGroup, dashboardResource, "1", "1"))
|
||||
res, err := server.Check(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:6 should be able to read folder 1 ", func(t *testing.T) {
|
||||
res, err := server.Check(context.Background(), newRead("user:6", folderGroup, folderResource, "", "1"))
|
||||
res, err := server.Check(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", "1"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:7 should be able to read folder one through namespace access", func(t *testing.T) {
|
||||
res, err := server.Check(context.Background(), newRead("user:7", folderGroup, folderResource, "", "1"))
|
||||
res, err := server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "1"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(context.Background(), newRead("user:7", folderGroup, folderResource, "", "10"))
|
||||
res, err = server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "10"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboar in folder 6 through folder 5", func(t *testing.T) {
|
||||
res, err := server.Check(context.Background(), newRead("user:8", dashboardGroup, dashboardResource, "6", "10"))
|
||||
res, err := server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "6", "10"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(context.Background(), newRead("user:8", dashboardGroup, dashboardResource, "5", "11"))
|
||||
res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "5", "11"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(context.Background(), newRead("user:8", folderGroup, folderResource, "4", "12"))
|
||||
res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, folderGroup, folderResource, "4", "12"))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:9 should be able to create dashboards in folder 5", func(t *testing.T) {
|
||||
res, err := server.Check(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "5", ""))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,9 +47,16 @@ func (s *Server) List(ctx context.Context, r *authzextv1.ListRequest) (*authzext
|
||||
return s.listGeneric(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
|
||||
}
|
||||
|
||||
func (s *Server) listObjects(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
|
||||
if s.cfg.UseStreamedListObjects {
|
||||
return s.streamedListObjects(ctx, req)
|
||||
}
|
||||
return s.openfga.ListObjects(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Server) listTyped(ctx context.Context, subject, relation string, info common.TypeInfo, store *storeInfo) (*authzextv1.ListResponse, error) {
|
||||
// List all resources user has access too
|
||||
listRes, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
listRes, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
Type: info.Type,
|
||||
@@ -69,7 +76,7 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation, group, reso
|
||||
groupResource := structpb.NewStringValue(common.FormatGroupResource(group, resource))
|
||||
|
||||
// 1. List all folders subject has access to resource type in
|
||||
folders, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
folders, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
Type: common.TypeFolder,
|
||||
@@ -86,7 +93,7 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation, group, reso
|
||||
}
|
||||
|
||||
// 2. List all resource directly assigned to subject
|
||||
direct, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
direct, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
Type: common.TypeResource,
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
)
|
||||
|
||||
func (s *Server) streamedListObjects(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
|
||||
if !s.cfg.CheckQueryCache {
|
||||
return s.listObjectsWithStream(ctx, req)
|
||||
}
|
||||
return s.streamedListObjectsCached(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Server) streamedListObjectsCached(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authzServer.streamedListObjectsCached")
|
||||
defer span.End()
|
||||
|
||||
reqHash, err := getRequestHash(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res, ok := s.cache.Get(reqHash); ok {
|
||||
return res.(*openfgav1.ListObjectsResponse), nil
|
||||
}
|
||||
|
||||
res, err := s.listObjectsWithStream(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.cache.Set(reqHash, res, 0)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Server) listObjectsWithStream(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authzServer.listObjectsWithStream")
|
||||
defer span.End()
|
||||
|
||||
r := &openfgav1.StreamedListObjectsRequest{
|
||||
StoreId: req.GetStoreId(),
|
||||
AuthorizationModelId: req.GetAuthorizationModelId(),
|
||||
Type: req.GetType(),
|
||||
Relation: req.GetRelation(),
|
||||
User: req.GetUser(),
|
||||
Context: req.GetContext(),
|
||||
}
|
||||
|
||||
clientStream, err := s.openfgaClient.StreamedListObjects(ctx, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
var streamedObjectIDs []string
|
||||
var streamingErr error
|
||||
var streamingResp *openfgav1.StreamedListObjectsResponse
|
||||
go func() {
|
||||
for {
|
||||
streamingResp, streamingErr = clientStream.Recv()
|
||||
if streamingErr == nil {
|
||||
streamedObjectIDs = append(streamedObjectIDs, streamingResp.GetObject())
|
||||
} else {
|
||||
if errors.Is(streamingErr, io.EOF) {
|
||||
streamingErr = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
<-done
|
||||
|
||||
if streamingErr != nil {
|
||||
return nil, streamingErr
|
||||
}
|
||||
|
||||
return &openfgav1.ListObjectsResponse{
|
||||
Objects: streamedObjectIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getRequestHash(req *openfgav1.ListObjectsRequest) (string, error) {
|
||||
if req == nil {
|
||||
return "", errors.New("request must not be empty")
|
||||
}
|
||||
|
||||
hash := fnv.New64a()
|
||||
_, err := hash.Write([]byte(req.String()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
|
||||
openfga, err := NewOpenFGA(&cfg.Zanzana, store, log.NewNopLogger())
|
||||
require.NoError(t, err)
|
||||
|
||||
srv, err := NewAuthz(openfga)
|
||||
srv, err := NewAuthz(cfg, openfga)
|
||||
require.NoError(t, err)
|
||||
|
||||
storeInf, err := srv.getStoreInfo(context.Background(), namespace)
|
||||
@@ -92,7 +92,8 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
|
||||
common.NewNamespaceResourceTuple("user:7", "read", folderGroup, folderResource),
|
||||
common.NewFolderParentTuple("5", "4"),
|
||||
common.NewFolderParentTuple("6", "5"),
|
||||
common.NewFolderResourceTuple("user:8", "view", dashboardGroup, dashboardResource, "5"),
|
||||
common.NewFolderResourceTuple("user:8", "edit", dashboardGroup, dashboardResource, "5"),
|
||||
common.NewFolderResourceTuple("user:9", "create", dashboardGroup, dashboardResource, "5"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -11,12 +11,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
TypeUser = common.TypeUser
|
||||
TypeTeam = common.TypeTeam
|
||||
TypeRole = common.TypeRole
|
||||
TypeFolder = common.TypeFolder
|
||||
TypeResource = common.TypeResource
|
||||
TypeNamespace = common.TypeNamespace
|
||||
TypeUser = common.TypeUser
|
||||
TypeServiceAccount = common.TypeServiceAccount
|
||||
TypeTeam = common.TypeTeam
|
||||
TypeRole = common.TypeRole
|
||||
TypeFolder = common.TypeFolder
|
||||
TypeResource = common.TypeResource
|
||||
TypeNamespace = common.TypeNamespace
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
)
|
||||
|
||||
@@ -38,6 +39,7 @@ type cachingLegacyDataSourceLookup struct {
|
||||
retriever DataSourceRetriever
|
||||
cache map[string]cachedValue
|
||||
cacheMu sync.Mutex
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
type cachedValue struct {
|
||||
@@ -49,15 +51,18 @@ func ProvideLegacyDataSourceLookup(p *Service) LegacyDataSourceLookup {
|
||||
return &cachingLegacyDataSourceLookup{
|
||||
retriever: p,
|
||||
cache: make(map[string]cachedValue),
|
||||
log: log.New("legacy-datasource-lookup"),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *cachingLegacyDataSourceLookup) GetDataSourceFromDeprecatedFields(ctx context.Context, name string, id int64) (*data.DataSourceRef, error) {
|
||||
if id == 0 && name == "" {
|
||||
s.log.Error("missing id and name in GetDataSourceFromDeprecatedFields")
|
||||
return nil, fmt.Errorf("either name or ID must be set")
|
||||
}
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
s.log.Error("failed to get user from context after getRequester", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
key := fmt.Sprintf("%d/%s/%d", user.GetOrgID(), name, id)
|
||||
@@ -74,6 +79,9 @@ func (s *cachingLegacyDataSourceLookup) GetDataSourceFromDeprecatedFields(ctx co
|
||||
Name: name,
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
s.log.Error("failed to get datasource from retriever", "error", err)
|
||||
}
|
||||
if errors.Is(err, datasources.ErrDataSourceNotFound) && name != "" {
|
||||
ds, err = s.retriever.GetDataSource(ctx, &datasources.GetDataSourceQuery{
|
||||
OrgID: user.GetOrgID(),
|
||||
|
||||
@@ -1070,6 +1070,14 @@ var (
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaAlertingSquad,
|
||||
},
|
||||
{
|
||||
Name: "scopeApi",
|
||||
Description: "In-development feature flag for the scope api using the app platform.",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
HideFromAdminPage: true,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "promQLScope",
|
||||
Description: "In-development feature that will allow injection of labels into prometheus queries.",
|
||||
@@ -1658,6 +1666,13 @@ var (
|
||||
Owner: grafanaAlertingSquad,
|
||||
Expression: "true", // enabled by default
|
||||
},
|
||||
{
|
||||
Name: "azureMonitorEnableUserAuth",
|
||||
Description: "Enables user auth for Azure Monitor datasource only",
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Owner: grafanaPartnerPluginsSquad,
|
||||
Expression: "true", // Enabled by default for now
|
||||
},
|
||||
{
|
||||
Name: "alertingNotificationsStepMode",
|
||||
Description: "Enables simplified step mode in the notifications section",
|
||||
|
||||
@@ -141,6 +141,7 @@ onPremToCloudMigrations,preview,@grafana/grafana-operator-experience-squad,false
|
||||
onPremToCloudMigrationsAlerts,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
onPremToCloudMigrationsAuthApiMig,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false
|
||||
scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
promQLScope,GA,@grafana/observability-metrics,false,false,false
|
||||
logQLScope,privatePreview,@grafana/observability-logs,false,false,false
|
||||
sqlExpressions,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
@@ -221,4 +222,5 @@ crashDetection,experimental,@grafana/observability-traces-and-profiling,false,fa
|
||||
jaegerBackendMigration,experimental,@grafana/oss-big-tent,false,false,false
|
||||
reportingUseRawTimeRange,preview,@grafana/sharing-squad,false,false,false
|
||||
alertingUIOptimizeReducer,GA,@grafana/alerting-squad,false,false,true
|
||||
azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false
|
||||
alertingNotificationsStepMode,experimental,@grafana/alerting-squad,false,false,true
|
||||
|
||||
|
@@ -575,6 +575,10 @@ const (
|
||||
// Writes the state periodically to the database, asynchronous to rule evaluation
|
||||
FlagAlertingSaveStatePeriodic = "alertingSaveStatePeriodic"
|
||||
|
||||
// FlagScopeApi
|
||||
// In-development feature flag for the scope api using the app platform.
|
||||
FlagScopeApi = "scopeApi"
|
||||
|
||||
// FlagPromQLScope
|
||||
// In-development feature that will allow injection of labels into prometheus queries.
|
||||
FlagPromQLScope = "promQLScope"
|
||||
@@ -895,6 +899,10 @@ const (
|
||||
// Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query
|
||||
FlagAlertingUIOptimizeReducer = "alertingUIOptimizeReducer"
|
||||
|
||||
// FlagAzureMonitorEnableUserAuth
|
||||
// Enables user auth for Azure Monitor datasource only
|
||||
FlagAzureMonitorEnableUserAuth = "azureMonitorEnableUserAuth"
|
||||
|
||||
// FlagAlertingNotificationsStepMode
|
||||
// Enables simplified step mode in the notifications section
|
||||
FlagAlertingNotificationsStepMode = "alertingNotificationsStepMode"
|
||||
|
||||
@@ -660,6 +660,22 @@
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "azureMonitorEnableUserAuth",
|
||||
"resourceVersion": "1732189410576",
|
||||
"creationTimestamp": "2024-11-21T11:42:29Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2024-11-21 11:43:30.576196 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables user auth for Azure Monitor datasource only",
|
||||
"stage": "GA",
|
||||
"codeowner": "@grafana/partner-datasources",
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "azureMonitorLogLimit",
|
||||
@@ -3130,6 +3146,20 @@
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "scopeApi",
|
||||
"resourceVersion": "1732690644377",
|
||||
"creationTimestamp": "2024-11-27T06:57:24Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "In-development feature flag for the scope api using the app platform.",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/grafana-app-platform-squad",
|
||||
"hideFromAdminPage": true,
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "scopeFilters",
|
||||
|
||||
@@ -92,7 +92,6 @@ func (s *Store) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuer
|
||||
err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.Table("user_auth").In("user_id", params).OrderBy("created").Find(&userAuths)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -187,7 +186,9 @@ func (s *Store) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCom
|
||||
}
|
||||
|
||||
return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
upd, err := sess.MustCols("o_auth_expiry").Where("user_id = ? AND auth_module = ?", cmd.UserId, cmd.AuthModule).Update(authUser)
|
||||
upd, err := sess.MustCols("o_auth_expiry", "o_auth_access_token", "o_auth_refresh_token", "o_auth_id_token", "o_auth_token_type").
|
||||
Where("user_id = ? AND auth_module = ?", cmd.UserId, cmd.AuthModule).
|
||||
Update(authUser)
|
||||
|
||||
s.logger.Debug("Updated user_auth", "user_id", cmd.UserId, "auth_id", cmd.AuthId, "auth_module", cmd.AuthModule, "rows", upd)
|
||||
|
||||
@@ -198,7 +199,6 @@ func (s *Store) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCom
|
||||
"SELECT id FROM user_auth WHERE user_id = ? AND auth_module = ? AND auth_id = ?",
|
||||
cmd.UserId, cmd.AuthModule, cmd.AuthId,
|
||||
).Get(&id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -220,7 +220,7 @@ func (s *Store) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCom
|
||||
|
||||
func (s *Store) DeleteUserAuthInfo(ctx context.Context, userID int64) error {
|
||||
return s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
var rawSQL = "DELETE FROM user_auth WHERE user_id = ?"
|
||||
rawSQL := "DELETE FROM user_auth WHERE user_id = ?"
|
||||
_, err := sess.Exec(rawSQL, userID)
|
||||
return err
|
||||
})
|
||||
|
||||
@@ -11,10 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
maxInvalidLoginAttempts int64 = 5
|
||||
loginAttemptsWindow = time.Minute * 5
|
||||
)
|
||||
const loginAttemptsWindow = time.Minute * 5
|
||||
|
||||
func ProvideService(db db.DB, cfg *setting.Cfg, lock *serverlock.ServerLockService) *Service {
|
||||
return &Service{
|
||||
@@ -80,7 +77,7 @@ func (s *Service) Validate(ctx context.Context, username string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if count >= maxInvalidLoginAttempts {
|
||||
if count >= s.cfg.BruteForceLoginProtectionMaxAttempts {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
)
|
||||
|
||||
func TestService_Validate(t *testing.T) {
|
||||
const maxInvalidLoginAttempts = 5
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
loginAttempts int64
|
||||
@@ -64,6 +66,7 @@ func TestService_Validate(t *testing.T) {
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.BruteForceLoginProtectionMaxAttempts = maxInvalidLoginAttempts
|
||||
cfg.DisableBruteForceLoginProtection = tt.disabled
|
||||
service := &Service{
|
||||
store: fakeStore{
|
||||
@@ -84,6 +87,7 @@ func TestLoginAttempts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cfg := setting.NewCfg()
|
||||
cfg.DisableBruteForceLoginProtection = false
|
||||
cfg.BruteForceLoginProtectionMaxAttempts = 5
|
||||
db := db.InitTestDB(t)
|
||||
service := ProvideService(db, cfg, nil)
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -39,28 +41,33 @@ type Service struct {
|
||||
Cfg *setting.Cfg
|
||||
SocialService social.Service
|
||||
AuthInfoService login.AuthInfoService
|
||||
sessionService auth.UserTokenService
|
||||
features featuremgmt.FeatureToggles
|
||||
serverLock *serverlock.ServerLockService
|
||||
tracer tracing.Tracer
|
||||
|
||||
tokenRefreshDuration *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
var _ OAuthTokenService = (*Service)(nil)
|
||||
|
||||
//go:generate mockery --name OAuthTokenService --structname MockService --outpkg oauthtokentest --filename service_mock.go --output ./oauthtokentest/
|
||||
type OAuthTokenService interface {
|
||||
GetCurrentOAuthToken(context.Context, identity.Requester) *oauth2.Token
|
||||
GetCurrentOAuthToken(context.Context, identity.Requester, *auth.UserToken) *oauth2.Token
|
||||
IsOAuthPassThruEnabled(*datasources.DataSource) bool
|
||||
HasOAuthEntry(context.Context, identity.Requester) (*login.UserAuth, bool, error)
|
||||
TryTokenRefresh(context.Context, identity.Requester) (*oauth2.Token, error)
|
||||
InvalidateOAuthTokens(context.Context, identity.Requester) error
|
||||
TryTokenRefresh(context.Context, identity.Requester, *auth.UserToken) (*oauth2.Token, error)
|
||||
InvalidateOAuthTokens(context.Context, identity.Requester, *auth.UserToken) error
|
||||
}
|
||||
|
||||
func ProvideService(socialService social.Service, authInfoService login.AuthInfoService, cfg *setting.Cfg, registerer prometheus.Registerer,
|
||||
serverLockService *serverlock.ServerLockService, tracer tracing.Tracer,
|
||||
serverLockService *serverlock.ServerLockService, tracer tracing.Tracer, sessionService auth.UserTokenService, features featuremgmt.FeatureToggles,
|
||||
) *Service {
|
||||
return &Service{
|
||||
AuthInfoService: authInfoService,
|
||||
sessionService: sessionService,
|
||||
Cfg: cfg,
|
||||
SocialService: socialService,
|
||||
features: features,
|
||||
serverLock: serverLockService,
|
||||
tokenRefreshDuration: newTokenRefreshDurationMetric(registerer),
|
||||
tracer: tracer,
|
||||
@@ -68,7 +75,7 @@ func ProvideService(socialService social.Service, authInfoService login.AuthInfo
|
||||
}
|
||||
|
||||
// GetCurrentOAuthToken returns the OAuth token, if any, for the authenticated user. Will try to refresh the token if it has expired.
|
||||
func (o *Service) GetCurrentOAuthToken(ctx context.Context, usr identity.Requester) *oauth2.Token {
|
||||
func (o *Service) GetCurrentOAuthToken(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) *oauth2.Token {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.GetCurrentOAuthToken")
|
||||
defer span.End()
|
||||
|
||||
@@ -93,30 +100,54 @@ func (o *Service) GetCurrentOAuthToken(ctx context.Context, usr identity.Request
|
||||
|
||||
ctxLogger = ctxLogger.New("userID", userID)
|
||||
|
||||
authInfo, ok, _ := o.HasOAuthEntry(ctx, usr)
|
||||
if !ok {
|
||||
if !strings.HasPrefix(usr.GetAuthenticatedBy(), "oauth_") {
|
||||
ctxLogger.Warn("The specified user's auth provider is not oauth",
|
||||
"authmodule", usr.GetAuthenticatedBy())
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkOAuthRefreshToken(authInfo); err != nil {
|
||||
if errors.Is(err, ErrNoRefreshTokenFound) {
|
||||
return buildOAuthTokenFromAuthInfo(authInfo)
|
||||
var persistedToken *oauth2.Token
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
externalSession, err := o.sessionService.GetExternalSession(ctx, sessionToken.ExternalSessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrExternalSessionNotFound) {
|
||||
return nil
|
||||
}
|
||||
ctxLogger.Error("Failed to fetch external session", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
persistedToken = buildOAuthTokenFromExternalSession(externalSession)
|
||||
|
||||
persistedToken := buildOAuthTokenFromAuthInfo(authInfo)
|
||||
if persistedToken.RefreshToken == "" {
|
||||
return persistedToken
|
||||
}
|
||||
} else {
|
||||
authInfo, ok, _ := o.hasOAuthEntry(ctx, usr)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkOAuthRefreshToken(authInfo); err != nil {
|
||||
if errors.Is(err, ErrNoRefreshTokenFound) {
|
||||
return buildOAuthTokenFromAuthInfo(authInfo)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
persistedToken = buildOAuthTokenFromAuthInfo(authInfo)
|
||||
}
|
||||
|
||||
refreshNeeded := needTokenRefresh(ctx, persistedToken)
|
||||
if !refreshNeeded {
|
||||
return persistedToken
|
||||
}
|
||||
|
||||
token, err := o.TryTokenRefresh(ctx, usr)
|
||||
token, err := o.TryTokenRefresh(ctx, usr, sessionToken)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNoRefreshTokenFound) {
|
||||
return buildOAuthTokenFromAuthInfo(authInfo)
|
||||
return persistedToken
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -130,9 +161,9 @@ func (o *Service) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool {
|
||||
return IsOAuthPassThruEnabled(ds)
|
||||
}
|
||||
|
||||
// HasOAuthEntry returns true and the UserAuth object when OAuth info exists for the specified User
|
||||
func (o *Service) HasOAuthEntry(ctx context.Context, usr identity.Requester) (*login.UserAuth, bool, error) {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.HasOAuthEntry")
|
||||
// hasOAuthEntry returns true and the UserAuth object when OAuth info exists for the specified User
|
||||
func (o *Service) hasOAuthEntry(ctx context.Context, usr identity.Requester) (*login.UserAuth, bool, error) {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.hasOAuthEntry")
|
||||
defer span.End()
|
||||
|
||||
if usr == nil || usr.IsNil() {
|
||||
@@ -167,12 +198,19 @@ func (o *Service) HasOAuthEntry(ctx context.Context, usr identity.Requester) (*l
|
||||
if !strings.Contains(authInfo.AuthModule, "oauth") {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// An extra check to ensure that the user has an OAuth token
|
||||
// It's required to handle the case when the `improvedExternalSessionHandling` feature flag gets disabled
|
||||
if authInfo.OAuthAccessToken == "" {
|
||||
ctxLogger.Debug("No access token found for user")
|
||||
return nil, false, fmt.Errorf("no access token found for user %d", userID)
|
||||
}
|
||||
return authInfo, true, nil
|
||||
}
|
||||
|
||||
// TryTokenRefresh returns an error in case the OAuth token refresh was unsuccessful
|
||||
// It uses a server lock to prevent getting the Refresh Token multiple times for a given User
|
||||
func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) (*oauth2.Token, error) {
|
||||
func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error) {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.TryTokenRefresh")
|
||||
defer span.End()
|
||||
|
||||
@@ -218,6 +256,9 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) (
|
||||
}
|
||||
|
||||
lockKey := fmt.Sprintf("oauth-refresh-token-%d", userID)
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
lockKey = fmt.Sprintf("oauth-refresh-token-%d-%d", userID, sessionToken.ExternalSessionId)
|
||||
}
|
||||
|
||||
lockTimeConfig := serverlock.LockTimeConfig{
|
||||
MaxInterval: 30 * time.Second,
|
||||
@@ -242,15 +283,32 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) (
|
||||
|
||||
ctxLogger.Debug("Serverlock request for getting a new access token", "key", lockKey)
|
||||
|
||||
authInfo, exists, err := o.HasOAuthEntry(ctx, usr)
|
||||
if !exists {
|
||||
var persistedToken *oauth2.Token
|
||||
var externalSession *auth.ExternalSession
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
externalSession, err = o.sessionService.GetExternalSession(ctx, sessionToken.ExternalSessionId)
|
||||
if err != nil {
|
||||
ctxLogger.Debug("Failed to fetch oauth entry", "error", err)
|
||||
if errors.Is(err, auth.ErrExternalSessionNotFound) {
|
||||
ctxLogger.Error("External session was not found for user", "error", err)
|
||||
return
|
||||
}
|
||||
ctxLogger.Error("Failed to fetch external session", "error", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
persistedToken := buildOAuthTokenFromAuthInfo(authInfo)
|
||||
persistedToken = buildOAuthTokenFromExternalSession(externalSession)
|
||||
} else {
|
||||
authInfo, exists, err := o.hasOAuthEntry(ctx, usr)
|
||||
if !exists {
|
||||
if err != nil {
|
||||
ctxLogger.Debug("Failed to fetch oauth entry", "error", err)
|
||||
cmdErr = err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
persistedToken = buildOAuthTokenFromAuthInfo(authInfo)
|
||||
}
|
||||
|
||||
needRefresh := needTokenRefresh(ctx, persistedToken)
|
||||
if !needRefresh {
|
||||
@@ -259,7 +317,7 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) (
|
||||
return
|
||||
}
|
||||
|
||||
newToken, cmdErr = o.tryGetOrRefreshOAuthToken(ctx, persistedToken, usr)
|
||||
newToken, cmdErr = o.tryGetOrRefreshOAuthToken(ctx, persistedToken, usr, sessionToken)
|
||||
}, retryOpt)
|
||||
if lockErr != nil {
|
||||
ctxLogger.Error("Failed to obtain token refresh lock", "error", lockErr)
|
||||
@@ -274,45 +332,27 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) (
|
||||
return newToken, cmdErr
|
||||
}
|
||||
|
||||
func buildOAuthTokenFromAuthInfo(authInfo *login.UserAuth) *oauth2.Token {
|
||||
token := &oauth2.Token{
|
||||
AccessToken: authInfo.OAuthAccessToken,
|
||||
Expiry: authInfo.OAuthExpiry,
|
||||
RefreshToken: authInfo.OAuthRefreshToken,
|
||||
TokenType: authInfo.OAuthTokenType,
|
||||
}
|
||||
|
||||
if authInfo.OAuthIdToken != "" {
|
||||
token = token.WithExtra(map[string]any{"id_token": authInfo.OAuthIdToken})
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func checkOAuthRefreshToken(authInfo *login.UserAuth) error {
|
||||
if !strings.Contains(authInfo.AuthModule, "oauth") {
|
||||
logger.Warn("The specified user's auth provider is not oauth",
|
||||
"authmodule", authInfo.AuthModule, "userid", authInfo.UserId)
|
||||
return ErrNotAnOAuthProvider
|
||||
}
|
||||
|
||||
if authInfo.OAuthRefreshToken == "" {
|
||||
logger.Warn("No refresh token available",
|
||||
"authmodule", authInfo.AuthModule, "userid", authInfo.UserId)
|
||||
return ErrNoRefreshTokenFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvalidateOAuthTokens invalidates the OAuth tokens (access_token, refresh_token) and sets the Expiry to default/zero
|
||||
func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester) error {
|
||||
func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) error {
|
||||
userID, err := usr.GetInternalID()
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert user id to int", "id", usr.GetID(), "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ctxLogger := logger.FromContext(ctx).New("userID", userID)
|
||||
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
err := o.sessionService.UpdateExternalSession(ctx, sessionToken.ExternalSessionId, &auth.UpdateExternalSessionCommand{
|
||||
Token: &oauth2.Token{},
|
||||
})
|
||||
if err != nil {
|
||||
ctxLogger.Error("Failed to update external session", "error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should this run regardless of the feature flag?
|
||||
return o.AuthInfoService.UpdateAuthInfo(ctx, &login.UpdateAuthInfoCommand{
|
||||
UserId: userID,
|
||||
AuthModule: usr.GetAuthenticatedBy(),
|
||||
@@ -325,7 +365,7 @@ func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Reques
|
||||
})
|
||||
}
|
||||
|
||||
func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken *oauth2.Token, usr identity.Requester) (*oauth2.Token, error) {
|
||||
func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken *oauth2.Token, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error) {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.tryGetOrRefreshOAuthToken")
|
||||
defer span.End()
|
||||
|
||||
@@ -374,7 +414,7 @@ func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken
|
||||
"provider", usr.GetAuthenticatedBy(), "error", err)
|
||||
|
||||
// token refresh failed, invalidate the old token
|
||||
if err := o.InvalidateOAuthTokens(ctx, usr); err != nil {
|
||||
if err := o.InvalidateOAuthTokens(ctx, usr, sessionToken); err != nil {
|
||||
ctxLogger.Warn("Failed to invalidate OAuth tokens", "authID", usr.GetAuthID(), "error", err)
|
||||
}
|
||||
|
||||
@@ -399,10 +439,20 @@ func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken
|
||||
)
|
||||
}
|
||||
|
||||
if err := o.AuthInfoService.UpdateAuthInfo(ctx, updateAuthCommand); err != nil {
|
||||
ctxLogger.Error("Failed to update auth info during token refresh", "authID", usr.GetAuthID(), "error", err)
|
||||
return token, err
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
if err := o.sessionService.UpdateExternalSession(ctx, sessionToken.ExternalSessionId, &auth.UpdateExternalSessionCommand{
|
||||
Token: token,
|
||||
}); err != nil {
|
||||
ctxLogger.Error("Failed to update external session during token refresh", "error", err)
|
||||
return token, err
|
||||
}
|
||||
} else {
|
||||
if err := o.AuthInfoService.UpdateAuthInfo(ctx, updateAuthCommand); err != nil {
|
||||
ctxLogger.Error("Failed to update auth info during token refresh", "authID", usr.GetAuthID(), "error", err)
|
||||
return token, err
|
||||
}
|
||||
}
|
||||
|
||||
ctxLogger.Debug("Updated oauth info for user")
|
||||
}
|
||||
|
||||
@@ -467,6 +517,51 @@ func needTokenRefresh(ctx context.Context, persistedToken *oauth2.Token) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func buildOAuthTokenFromAuthInfo(authInfo *login.UserAuth) *oauth2.Token {
|
||||
token := &oauth2.Token{
|
||||
AccessToken: authInfo.OAuthAccessToken,
|
||||
Expiry: authInfo.OAuthExpiry,
|
||||
RefreshToken: authInfo.OAuthRefreshToken,
|
||||
TokenType: authInfo.OAuthTokenType,
|
||||
}
|
||||
|
||||
if authInfo.OAuthIdToken != "" {
|
||||
token = token.WithExtra(map[string]any{"id_token": authInfo.OAuthIdToken})
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func buildOAuthTokenFromExternalSession(externalSession *auth.ExternalSession) *oauth2.Token {
|
||||
token := &oauth2.Token{
|
||||
AccessToken: externalSession.AccessToken,
|
||||
Expiry: externalSession.ExpiresAt,
|
||||
RefreshToken: externalSession.RefreshToken,
|
||||
}
|
||||
|
||||
if externalSession.IDToken != "" {
|
||||
token = token.WithExtra(map[string]any{"id_token": externalSession.IDToken})
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func checkOAuthRefreshToken(authInfo *login.UserAuth) error {
|
||||
if !strings.Contains(authInfo.AuthModule, "oauth") {
|
||||
logger.Warn("The specified user's auth provider is not oauth",
|
||||
"authmodule", authInfo.AuthModule, "userid", authInfo.UserId)
|
||||
return ErrNotAnOAuthProvider
|
||||
}
|
||||
|
||||
if authInfo.OAuthRefreshToken == "" {
|
||||
logger.Warn("No refresh token available",
|
||||
"authmodule", authInfo.AuthModule, "userid", authInfo.UserId)
|
||||
return ErrNoRefreshTokenFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIDTokenExpiry extracts the expiry time from the ID token
|
||||
func GetIDTokenExpiry(token *oauth2.Token) (time.Time, error) {
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
|
||||
@@ -3,32 +3,29 @@ package oauthtoken
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/login/social/socialtest"
|
||||
"github.com/grafana/grafana/pkg/models/usertoken"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/auth/authtest"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfoimpl"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfotest"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const EXPIRED_ID_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6InlvdXItY2xpZW50LWlkIiwiZXhwIjoxNjAwMDAwMDAwLCJpYXQiOjE2MDAwMDAwMDAsIm5hbWUiOiJKb2huIERvZSIsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSJ9.c2lnbmF0dXJl" // #nosec G101 not a hardcoded credential
|
||||
@@ -39,104 +36,6 @@ func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
func TestService_HasOAuthEntry(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *user.SignedInUser
|
||||
want *login.UserAuth
|
||||
wantExist bool
|
||||
wantErr bool
|
||||
err error
|
||||
getAuthInfoErr error
|
||||
getAuthInfoUser login.UserAuth
|
||||
}{
|
||||
{
|
||||
name: "returns false without an error in case user is nil",
|
||||
user: nil,
|
||||
want: nil,
|
||||
wantExist: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "returns false and an error in case GetAuthInfo returns an error",
|
||||
user: &user.SignedInUser{UserID: 1},
|
||||
want: nil,
|
||||
wantExist: false,
|
||||
wantErr: true,
|
||||
getAuthInfoErr: errors.New("error"),
|
||||
},
|
||||
{
|
||||
name: "returns false without an error in case auth entry is not found",
|
||||
user: &user.SignedInUser{UserID: 1},
|
||||
want: nil,
|
||||
wantExist: false,
|
||||
wantErr: false,
|
||||
getAuthInfoErr: user.ErrUserNotFound,
|
||||
},
|
||||
{
|
||||
name: "returns false without an error in case the auth entry is not oauth",
|
||||
user: &user.SignedInUser{UserID: 1},
|
||||
want: nil,
|
||||
wantExist: false,
|
||||
wantErr: false,
|
||||
getAuthInfoUser: login.UserAuth{AuthModule: "auth_saml"},
|
||||
},
|
||||
{
|
||||
name: "returns true when the auth entry is found",
|
||||
user: &user.SignedInUser{UserID: 1},
|
||||
want: &login.UserAuth{AuthModule: login.GenericOAuthModule},
|
||||
wantExist: true,
|
||||
wantErr: false,
|
||||
getAuthInfoUser: login.UserAuth{AuthModule: login.GenericOAuthModule},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srv, authInfoStore, _ := setupOAuthTokenService(t)
|
||||
authInfoStore.ExpectedOAuth = &tc.getAuthInfoUser
|
||||
authInfoStore.ExpectedError = tc.getAuthInfoErr
|
||||
|
||||
entry, exists, err := srv.HasOAuthEntry(context.Background(), tc.user)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
if tc.want != nil {
|
||||
assert.True(t, reflect.DeepEqual(tc.want, entry))
|
||||
}
|
||||
assert.Equal(t, tc.wantExist, exists)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupOAuthTokenService(t *testing.T) (*Service, *FakeAuthInfoStore, *socialtest.MockSocialConnector) {
|
||||
t.Helper()
|
||||
|
||||
socialConnector := &socialtest.MockSocialConnector{}
|
||||
socialService := &socialtest.FakeSocialService{
|
||||
ExpectedConnector: socialConnector,
|
||||
ExpectedAuthInfoProvider: &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
},
|
||||
}
|
||||
|
||||
authInfoStore := &FakeAuthInfoStore{ExpectedOAuth: &login.UserAuth{}}
|
||||
authInfoService := authinfoimpl.ProvideService(authInfoStore, remotecache.NewFakeCacheStorage(), secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()))
|
||||
|
||||
store := db.InitTestDB(t)
|
||||
|
||||
return &Service{
|
||||
Cfg: setting.NewCfg(),
|
||||
SocialService: socialService,
|
||||
AuthInfoService: authInfoService,
|
||||
serverLock: serverlock.ProvideService(store, tracing.InitializeTracerForTest()),
|
||||
tokenRefreshDuration: newTokenRefreshDurationMetric(prometheus.NewRegistry()),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
}, authInfoStore, socialConnector
|
||||
}
|
||||
|
||||
type FakeAuthInfoStore struct {
|
||||
login.Store
|
||||
ExpectedError error
|
||||
@@ -379,10 +278,12 @@ func TestService_TryTokenRefresh(t *testing.T) {
|
||||
prometheus.NewRegistry(),
|
||||
env.serverLock,
|
||||
tracing.InitializeTracerForTest(),
|
||||
nil,
|
||||
featuremgmt.WithFeatures(),
|
||||
)
|
||||
|
||||
// token refresh
|
||||
actualToken, err := env.service.TryTokenRefresh(context.Background(), tt.identity)
|
||||
actualToken, err := env.service.TryTokenRefresh(context.Background(), tt.identity, nil)
|
||||
|
||||
if tt.expectedErr != nil {
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
@@ -407,6 +308,275 @@ func TestService_TryTokenRefresh(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_TryTokenRefresh_WithExternalSessions(t *testing.T) {
|
||||
unexpiredToken := &oauth2.Token{
|
||||
AccessToken: "testaccess",
|
||||
RefreshToken: "testrefresh",
|
||||
Expiry: time.Now().Add(time.Hour),
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
unexpiredTokenWithIDToken := unexpiredToken.WithExtra(map[string]interface{}{
|
||||
"id_token": UNEXPIRED_ID_TOKEN,
|
||||
})
|
||||
|
||||
expiredToken := &oauth2.Token{
|
||||
AccessToken: "testaccess",
|
||||
RefreshToken: "testrefresh",
|
||||
Expiry: time.Now().Add(-time.Hour),
|
||||
TokenType: "Bearer",
|
||||
}
|
||||
|
||||
userIdentity := &authn.Identity{
|
||||
AuthenticatedBy: login.GenericOAuthModule,
|
||||
ID: "1234",
|
||||
Type: claims.TypeUser,
|
||||
}
|
||||
|
||||
type environment struct {
|
||||
sessionService *authtest.MockUserAuthTokenService
|
||||
serverLock *serverlock.ServerLockService
|
||||
socialConnector *socialtest.MockSocialConnector
|
||||
socialService *socialtest.FakeSocialService
|
||||
|
||||
service *Service
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
desc string
|
||||
identity identity.Requester
|
||||
setup func(env *environment)
|
||||
expectedToken *oauth2.Token
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
desc: "should skip sync when identity is nil",
|
||||
},
|
||||
{
|
||||
desc: "should skip sync when identity is not a user",
|
||||
identity: &authn.Identity{ID: "1", Type: claims.TypeServiceAccount},
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh and return nil if namespace and id cannot be converted to user ID",
|
||||
identity: &authn.Identity{ID: "invalid", Type: claims.TypeUser},
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh if there's an unexpected error while looking up the user oauth entry, additionally, no error should be returned",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(nil, assert.AnError).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
// Kinda impossible to happen, can only happen after the feature is enabled and logged in users don't have their external sessions set
|
||||
{
|
||||
desc: "should skip token refresh if the user doesn't have an external session",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(nil, auth.ErrExternalSessionNotFound).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh when no oauth provider was found",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.socialService.ExpectedAuthInfoProvider = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh when oauth provider token handling is disabled (UseRefreshToken is false)",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh when the token is still valid and no id token is present",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(&auth.ExternalSession{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
AccessToken: unexpiredTokenWithIDToken.AccessToken,
|
||||
RefreshToken: unexpiredTokenWithIDToken.RefreshToken,
|
||||
ExpiresAt: unexpiredTokenWithIDToken.Expiry,
|
||||
}, nil).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
expectedToken: unexpiredToken,
|
||||
},
|
||||
{
|
||||
desc: "should not do token refresh if access token or id token have not expired yet",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(&auth.ExternalSession{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
AccessToken: unexpiredTokenWithIDToken.AccessToken,
|
||||
RefreshToken: unexpiredTokenWithIDToken.RefreshToken,
|
||||
ExpiresAt: unexpiredTokenWithIDToken.Expiry,
|
||||
IDToken: UNEXPIRED_ID_TOKEN,
|
||||
}, nil).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
expectedToken: unexpiredTokenWithIDToken,
|
||||
},
|
||||
{
|
||||
desc: "should skip token refresh when there is no refresh token",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(&auth.ExternalSession{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
AccessToken: unexpiredTokenWithIDToken.AccessToken,
|
||||
RefreshToken: "",
|
||||
ExpiresAt: unexpiredTokenWithIDToken.Expiry,
|
||||
}, nil).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
expectedToken: &oauth2.Token{
|
||||
AccessToken: unexpiredTokenWithIDToken.AccessToken,
|
||||
RefreshToken: "",
|
||||
Expiry: unexpiredTokenWithIDToken.Expiry,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should refresh token when the access token is expired",
|
||||
identity: &authn.Identity{
|
||||
AuthenticatedBy: login.GenericOAuthModule,
|
||||
ID: "1",
|
||||
Type: claims.TypeUser,
|
||||
},
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(&auth.ExternalSession{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
AccessToken: expiredToken.AccessToken,
|
||||
IDToken: UNEXPIRED_ID_TOKEN,
|
||||
RefreshToken: expiredToken.RefreshToken,
|
||||
ExpiresAt: expiredToken.Expiry,
|
||||
}, nil).Once()
|
||||
|
||||
env.sessionService.On("UpdateExternalSession", mock.Anything, int64(1), mock.MatchedBy(verifyUpdateExternalSessionCommand(unexpiredTokenWithIDToken))).Return(nil).Once()
|
||||
|
||||
env.socialConnector.On("TokenSource", mock.Anything, mock.Anything).Return(oauth2.StaticTokenSource(unexpiredTokenWithIDToken)).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
},
|
||||
expectedToken: unexpiredTokenWithIDToken,
|
||||
},
|
||||
{
|
||||
desc: "should refresh token when the id token is expired",
|
||||
identity: userIdentity,
|
||||
setup: func(env *environment) {
|
||||
env.sessionService.On("GetExternalSession", mock.Anything, int64(1)).Return(&auth.ExternalSession{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
AccessToken: unexpiredTokenWithIDToken.AccessToken,
|
||||
RefreshToken: unexpiredTokenWithIDToken.RefreshToken,
|
||||
ExpiresAt: unexpiredTokenWithIDToken.Expiry,
|
||||
IDToken: EXPIRED_ID_TOKEN,
|
||||
}, nil).Once()
|
||||
|
||||
env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{
|
||||
UseRefreshToken: true,
|
||||
}
|
||||
|
||||
env.sessionService.On("UpdateExternalSession", mock.Anything, int64(1), mock.MatchedBy(verifyUpdateExternalSessionCommand(unexpiredTokenWithIDToken))).Return(nil).Once()
|
||||
|
||||
env.socialConnector.On("TokenSource", mock.Anything, mock.Anything).Return(oauth2.StaticTokenSource(unexpiredTokenWithIDToken)).Once()
|
||||
},
|
||||
expectedToken: unexpiredTokenWithIDToken,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
socialConnector := socialtest.NewMockSocialConnector(t)
|
||||
|
||||
store := db.InitTestDB(t)
|
||||
|
||||
env := environment{
|
||||
sessionService: authtest.NewMockUserAuthTokenService(t),
|
||||
serverLock: serverlock.ProvideService(store, tracing.InitializeTracerForTest()),
|
||||
socialConnector: socialConnector,
|
||||
socialService: &socialtest.FakeSocialService{
|
||||
ExpectedConnector: socialConnector,
|
||||
},
|
||||
}
|
||||
|
||||
if tt.setup != nil {
|
||||
tt.setup(&env)
|
||||
}
|
||||
|
||||
env.service = ProvideService(
|
||||
env.socialService,
|
||||
nil,
|
||||
setting.NewCfg(),
|
||||
prometheus.NewRegistry(),
|
||||
env.serverLock,
|
||||
tracing.InitializeTracerForTest(),
|
||||
env.sessionService,
|
||||
featuremgmt.WithFeatures(featuremgmt.FlagImprovedExternalSessionHandling),
|
||||
)
|
||||
|
||||
// token refresh
|
||||
actualToken, err := env.service.TryTokenRefresh(context.Background(), tt.identity, &usertoken.UserToken{ExternalSessionId: 1})
|
||||
|
||||
if tt.expectedErr != nil {
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
if tt.expectedToken == nil {
|
||||
assert.Nil(t, actualToken)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedToken.AccessToken, actualToken.AccessToken)
|
||||
assert.Equal(t, tt.expectedToken.RefreshToken, actualToken.RefreshToken)
|
||||
assert.Equal(t, tt.expectedToken.Expiry, actualToken.Expiry)
|
||||
if tt.expectedToken.Extra("id_token") != nil {
|
||||
assert.Equal(t, tt.expectedToken.Extra("id_token").(string), actualToken.Extra("id_token").(string))
|
||||
} else {
|
||||
assert.Nil(t, actualToken.Extra("id_token"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func verifyUpdateExternalSessionCommand(token *oauth2.Token) func(*auth.UpdateExternalSessionCommand) bool {
|
||||
return func(cmd *auth.UpdateExternalSessionCommand) bool {
|
||||
idToken := cmd.Token.Extra("id_token")
|
||||
return cmd.Token.AccessToken == token.AccessToken &&
|
||||
cmd.Token.RefreshToken == token.RefreshToken &&
|
||||
cmd.Token.Expiry == token.Expiry &&
|
||||
idToken == token.Extra("id_token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuthTokenSync_needTokenRefresh(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -6,21 +6,20 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
)
|
||||
|
||||
type MockOauthTokenService struct {
|
||||
GetCurrentOauthTokenFunc func(ctx context.Context, usr identity.Requester) *oauth2.Token
|
||||
GetCurrentOauthTokenFunc func(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) *oauth2.Token
|
||||
IsOAuthPassThruEnabledFunc func(ds *datasources.DataSource) bool
|
||||
HasOAuthEntryFunc func(ctx context.Context, usr identity.Requester) (*login.UserAuth, bool, error)
|
||||
InvalidateOAuthTokensFunc func(ctx context.Context, usr identity.Requester) error
|
||||
TryTokenRefreshFunc func(ctx context.Context, usr identity.Requester) (*oauth2.Token, error)
|
||||
InvalidateOAuthTokensFunc func(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) error
|
||||
TryTokenRefreshFunc func(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error)
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) GetCurrentOAuthToken(ctx context.Context, usr identity.Requester) *oauth2.Token {
|
||||
func (m *MockOauthTokenService) GetCurrentOAuthToken(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) *oauth2.Token {
|
||||
if m.GetCurrentOauthTokenFunc != nil {
|
||||
return m.GetCurrentOauthTokenFunc(ctx, usr)
|
||||
return m.GetCurrentOauthTokenFunc(ctx, usr, sessionToken)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -32,23 +31,16 @@ func (m *MockOauthTokenService) IsOAuthPassThruEnabled(ds *datasources.DataSourc
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) HasOAuthEntry(ctx context.Context, usr identity.Requester) (*login.UserAuth, bool, error) {
|
||||
if m.HasOAuthEntryFunc != nil {
|
||||
return m.HasOAuthEntryFunc(ctx, usr)
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester) error {
|
||||
func (m *MockOauthTokenService) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) error {
|
||||
if m.InvalidateOAuthTokensFunc != nil {
|
||||
return m.InvalidateOAuthTokensFunc(ctx, usr)
|
||||
return m.InvalidateOAuthTokensFunc(ctx, usr, sessionToken)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) TryTokenRefresh(ctx context.Context, usr identity.Requester) (*oauth2.Token, error) {
|
||||
func (m *MockOauthTokenService) TryTokenRefresh(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error) {
|
||||
if m.TryTokenRefreshFunc != nil {
|
||||
return m.TryTokenRefreshFunc(ctx, usr)
|
||||
return m.TryTokenRefreshFunc(ctx, usr, sessionToken)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ func ProvideService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) GetCurrentOAuthToken(context.Context, identity.Requester) *oauth2.Token {
|
||||
func (s *Service) GetCurrentOAuthToken(context.Context, identity.Requester, *auth.UserToken) *oauth2.Token {
|
||||
return s.Token
|
||||
}
|
||||
|
||||
@@ -29,14 +29,10 @@ func (s *Service) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool {
|
||||
return oauthtoken.IsOAuthPassThruEnabled(ds)
|
||||
}
|
||||
|
||||
func (s *Service) HasOAuthEntry(context.Context, identity.Requester) (*login.UserAuth, bool, error) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (s *Service) TryTokenRefresh(context.Context, identity.Requester) (*oauth2.Token, error) {
|
||||
func (s *Service) TryTokenRefresh(context.Context, identity.Requester, *auth.UserToken) (*oauth2.Token, error) {
|
||||
return s.Token, nil
|
||||
}
|
||||
|
||||
func (s *Service) InvalidateOAuthTokens(context.Context, identity.Requester) error {
|
||||
func (s *Service) InvalidateOAuthTokens(context.Context, identity.Requester, *auth.UserToken) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.40.1. DO NOT EDIT.
|
||||
// Code generated by mockery v2.42.1. DO NOT EDIT.
|
||||
|
||||
package oauthtokentest
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
identity "github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
datasources "github.com/grafana/grafana/pkg/services/datasources"
|
||||
|
||||
login "github.com/grafana/grafana/pkg/services/login"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
oauth2 "golang.org/x/oauth2"
|
||||
|
||||
usertoken "github.com/grafana/grafana/pkg/models/usertoken"
|
||||
)
|
||||
|
||||
// MockService is an autogenerated mock type for the OAuthTokenService type
|
||||
@@ -20,17 +20,17 @@ type MockService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetCurrentOAuthToken provides a mock function with given fields: _a0, _a1
|
||||
func (_m *MockService) GetCurrentOAuthToken(_a0 context.Context, _a1 identity.Requester) *oauth2.Token {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
// GetCurrentOAuthToken provides a mock function with given fields: _a0, _a1, _a2
|
||||
func (_m *MockService) GetCurrentOAuthToken(_a0 context.Context, _a1 identity.Requester, _a2 *usertoken.UserToken) *oauth2.Token {
|
||||
ret := _m.Called(_a0, _a1, _a2)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetCurrentOAuthToken")
|
||||
}
|
||||
|
||||
var r0 *oauth2.Token
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) *oauth2.Token); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, *usertoken.UserToken) *oauth2.Token); ok {
|
||||
r0 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*oauth2.Token)
|
||||
@@ -40,54 +40,17 @@ func (_m *MockService) GetCurrentOAuthToken(_a0 context.Context, _a1 identity.Re
|
||||
return r0
|
||||
}
|
||||
|
||||
// HasOAuthEntry provides a mock function with given fields: _a0, _a1
|
||||
func (_m *MockService) HasOAuthEntry(_a0 context.Context, _a1 identity.Requester) (*login.UserAuth, bool, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for HasOAuthEntry")
|
||||
}
|
||||
|
||||
var r0 *login.UserAuth
|
||||
var r1 bool
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) (*login.UserAuth, bool, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) *login.UserAuth); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*login.UserAuth)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, identity.Requester) bool); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Get(1).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func(context.Context, identity.Requester) error); ok {
|
||||
r2 = rf(_a0, _a1)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// InvalidateOAuthTokens provides a mock function with given fields: _a0, _a1
|
||||
func (_m *MockService) InvalidateOAuthTokens(_a0 context.Context, _a1 *login.UserAuth) error {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
// InvalidateOAuthTokens provides a mock function with given fields: _a0, _a1, _a2
|
||||
func (_m *MockService) InvalidateOAuthTokens(_a0 context.Context, _a1 identity.Requester, _a2 *usertoken.UserToken) error {
|
||||
ret := _m.Called(_a0, _a1, _a2)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for InvalidateOAuthTokens")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *login.UserAuth) error); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, *usertoken.UserToken) error); ok {
|
||||
r0 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
@@ -113,22 +76,34 @@ func (_m *MockService) IsOAuthPassThruEnabled(_a0 *datasources.DataSource) bool
|
||||
return r0
|
||||
}
|
||||
|
||||
// TryTokenRefresh provides a mock function with given fields: _a0, _a1
|
||||
func (_m *MockService) TryTokenRefresh(_a0 context.Context, _a1 identity.Requester) error {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
// TryTokenRefresh provides a mock function with given fields: _a0, _a1, _a2
|
||||
func (_m *MockService) TryTokenRefresh(_a0 context.Context, _a1 identity.Requester, _a2 *usertoken.UserToken) (*oauth2.Token, error) {
|
||||
ret := _m.Called(_a0, _a1, _a2)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for TryTokenRefresh")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) error); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
var r0 *oauth2.Token
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, *usertoken.UserToken) (*oauth2.Token, error)); ok {
|
||||
return rf(_a0, _a1, _a2)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, *usertoken.UserToken) *oauth2.Token); ok {
|
||||
r0 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*oauth2.Token)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
if rf, ok := ret.Get(1).(func(context.Context, identity.Requester, *usertoken.UserToken) error); ok {
|
||||
r1 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
|
||||
@@ -222,6 +222,11 @@ func (s *sqlStore) ListAll(ctx context.Context, orgId int64) ([]playlist.Playlis
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = rows.Close()
|
||||
}()
|
||||
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&playlistId, &itemType, &itemValue)
|
||||
if err != nil {
|
||||
|
||||
@@ -50,7 +50,7 @@ func (m *OAuthTokenMiddleware) applyToken(ctx context.Context, pCtx backend.Plug
|
||||
}
|
||||
|
||||
if m.oAuthTokenService.IsOAuthPassThruEnabled(ds) {
|
||||
if token := m.oAuthTokenService.GetCurrentOAuthToken(ctx, reqCtx.SignedInUser); token != nil {
|
||||
if token := m.oAuthTokenService.GetCurrentOAuthToken(ctx, reqCtx.SignedInUser, reqCtx.UserToken); token != nil {
|
||||
authorizationHeader := fmt.Sprintf("%s %s", token.Type(), token.AccessToken)
|
||||
idTokenHeader := ""
|
||||
|
||||
|
||||
@@ -509,11 +509,30 @@ func (s *ServiceAccountsStoreImpl) CreateServiceAccountFromApikey(ctx context.Co
|
||||
IsServiceAccount: true,
|
||||
}
|
||||
|
||||
// maximum number of attempts for creating a service account
|
||||
attempts := 10
|
||||
|
||||
return s.sqlStore.InTransaction(ctx, func(tctx context.Context) error {
|
||||
newSA, errCreateSA := s.userService.CreateServiceAccount(tctx, &cmd)
|
||||
if errCreateSA != nil {
|
||||
if errors.Is(errCreateSA, serviceaccounts.ErrServiceAccountAlreadyExists) {
|
||||
// The service account we tried to create already exists with that login name. We will attempt to create
|
||||
// a unique service account by adding suffixes to the initial login name (e.g. -001, -002, ... , -010).
|
||||
for i := 1; errCreateSA != nil && i <= attempts; i++ {
|
||||
serviceAccountName := fmt.Sprintf("%s-%03d", key.Name, i)
|
||||
cmd.Login = generateLogin(prefix, key.OrgID, serviceAccountName)
|
||||
newSA, errCreateSA = s.userService.CreateServiceAccount(tctx, &cmd)
|
||||
if errCreateSA != nil && !errors.Is(errCreateSA, serviceaccounts.ErrServiceAccountAlreadyExists) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errCreateSA != nil {
|
||||
return fmt.Errorf("failed to create service account: %w", errCreateSA)
|
||||
}
|
||||
|
||||
return s.assignApiKeyToServiceAccount(tctx, key.ID, newSA.ID)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -324,26 +324,85 @@ func TestIntegrationStore_MigrateApiKeys(t *testing.T) {
|
||||
t.Skip("skipping test in short mode")
|
||||
}
|
||||
cases := []struct {
|
||||
desc string
|
||||
key tests.TestApiKey
|
||||
expectedErr error
|
||||
desc string
|
||||
serviceAccounts []user.CreateUserCommand
|
||||
key tests.TestApiKey
|
||||
expectedLogin string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
desc: "api key should be migrated to service account token",
|
||||
key: tests.TestApiKey{Name: "Test1", Role: org.RoleEditor, OrgId: 1},
|
||||
expectedErr: nil,
|
||||
desc: "api key should be migrated to service account token",
|
||||
serviceAccounts: []user.CreateUserCommand{},
|
||||
key: tests.TestApiKey{Name: "test1", Role: org.RoleEditor, OrgId: 1},
|
||||
expectedLogin: "sa-autogen-1-test1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "api key should be migrated to service account token on second attempt",
|
||||
serviceAccounts: []user.CreateUserCommand{
|
||||
{Login: "sa-autogen-1-test2"},
|
||||
},
|
||||
key: tests.TestApiKey{Name: "test2", Role: org.RoleEditor, OrgId: 1},
|
||||
expectedLogin: "sa-autogen-1-test2-001",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "api key should be migrated to service account token on last attempt (the 10th)",
|
||||
serviceAccounts: []user.CreateUserCommand{
|
||||
{Login: "sa-autogen-1-test3"},
|
||||
{Login: "sa-autogen-1-test3-001"},
|
||||
{Login: "sa-autogen-1-test3-002"},
|
||||
{Login: "sa-autogen-1-test3-003"},
|
||||
{Login: "sa-autogen-1-test3-004"},
|
||||
{Login: "sa-autogen-1-test3-005"},
|
||||
{Login: "sa-autogen-1-test3-006"},
|
||||
{Login: "sa-autogen-1-test3-007"},
|
||||
{Login: "sa-autogen-1-test3-008"},
|
||||
{Login: "sa-autogen-1-test3-009"},
|
||||
},
|
||||
key: tests.TestApiKey{Name: "test3", Role: org.RoleEditor, OrgId: 1},
|
||||
expectedLogin: "sa-autogen-1-test3-010",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "api key should not be migrated to service account token because all attempts failed",
|
||||
serviceAccounts: []user.CreateUserCommand{
|
||||
{Login: "sa-autogen-1-test4"},
|
||||
{Login: "sa-autogen-1-test4-001"},
|
||||
{Login: "sa-autogen-1-test4-002"},
|
||||
{Login: "sa-autogen-1-test4-003"},
|
||||
{Login: "sa-autogen-1-test4-004"},
|
||||
{Login: "sa-autogen-1-test4-005"},
|
||||
{Login: "sa-autogen-1-test4-006"},
|
||||
{Login: "sa-autogen-1-test4-007"},
|
||||
{Login: "sa-autogen-1-test4-008"},
|
||||
{Login: "sa-autogen-1-test4-009"},
|
||||
{Login: "sa-autogen-1-test4-010"},
|
||||
},
|
||||
key: tests.TestApiKey{Name: "test4", Role: org.RoleEditor, OrgId: 1},
|
||||
expectedErr: serviceaccounts.ErrServiceAccountAlreadyExists,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
db, store := setupTestDatabase(t)
|
||||
|
||||
store.cfg.AutoAssignOrg = true
|
||||
store.cfg.AutoAssignOrgId = 1
|
||||
store.cfg.AutoAssignOrgRole = "Viewer"
|
||||
_, err := store.orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "main"})
|
||||
require.NoError(t, err)
|
||||
|
||||
key := tests.SetupApiKey(t, db, store.cfg, c.key)
|
||||
|
||||
for _, sa := range c.serviceAccounts {
|
||||
sa.IsServiceAccount = true
|
||||
sa.OrgID = key.OrgID
|
||||
_, err := store.userService.CreateServiceAccount(context.Background(), &sa)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err = store.MigrateApiKey(context.Background(), key.OrgID, key.ID)
|
||||
if c.expectedErr != nil {
|
||||
require.ErrorIs(t, err, c.expectedErr)
|
||||
@@ -352,7 +411,7 @@ func TestIntegrationStore_MigrateApiKeys(t *testing.T) {
|
||||
|
||||
q := serviceaccounts.SearchOrgServiceAccountsQuery{
|
||||
OrgID: key.OrgID,
|
||||
Query: "",
|
||||
Query: c.expectedLogin,
|
||||
Page: 1,
|
||||
Limit: 50,
|
||||
SignedInUser: &user.SignedInUser{
|
||||
@@ -370,6 +429,7 @@ func TestIntegrationStore_MigrateApiKeys(t *testing.T) {
|
||||
require.Equal(t, int64(1), serviceAccounts.TotalCount)
|
||||
saMigrated := serviceAccounts.ServiceAccounts[0]
|
||||
require.Equal(t, string(key.Role), saMigrated.Role)
|
||||
require.Equal(t, c.expectedLogin, saMigrated.Login)
|
||||
|
||||
tokens, err := store.ListTokens(context.Background(), &serviceaccounts.GetSATokensQuery{
|
||||
OrgID: &key.OrgID,
|
||||
|
||||
+20
-12
@@ -154,18 +154,19 @@ type Cfg struct {
|
||||
RendererDefaultImageScale float64
|
||||
|
||||
// Security
|
||||
DisableInitAdminCreation bool
|
||||
DisableBruteForceLoginProtection bool
|
||||
CookieSecure bool
|
||||
CookieSameSiteDisabled bool
|
||||
CookieSameSiteMode http.SameSite
|
||||
AllowEmbedding bool
|
||||
XSSProtectionHeader bool
|
||||
ContentTypeProtectionHeader bool
|
||||
StrictTransportSecurity bool
|
||||
StrictTransportSecurityMaxAge int
|
||||
StrictTransportSecurityPreload bool
|
||||
StrictTransportSecuritySubDomains bool
|
||||
DisableInitAdminCreation bool
|
||||
DisableBruteForceLoginProtection bool
|
||||
BruteForceLoginProtectionMaxAttempts int64
|
||||
CookieSecure bool
|
||||
CookieSameSiteDisabled bool
|
||||
CookieSameSiteMode http.SameSite
|
||||
AllowEmbedding bool
|
||||
XSSProtectionHeader bool
|
||||
ContentTypeProtectionHeader bool
|
||||
StrictTransportSecurity bool
|
||||
StrictTransportSecurityMaxAge int
|
||||
StrictTransportSecurityPreload bool
|
||||
StrictTransportSecuritySubDomains bool
|
||||
// CSPEnabled toggles Content Security Policy support.
|
||||
CSPEnabled bool
|
||||
// CSPTemplate contains the Content Security Policy template.
|
||||
@@ -1498,7 +1499,14 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
|
||||
security := iniFile.Section("security")
|
||||
cfg.SecretKey = valueAsString(security, "secret_key", "")
|
||||
cfg.DisableGravatar = security.Key("disable_gravatar").MustBool(true)
|
||||
|
||||
cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false)
|
||||
cfg.BruteForceLoginProtectionMaxAttempts = security.Key("brute_force_login_protection_max_attempts").MustInt64(5)
|
||||
|
||||
// Ensure at least one login attempt can be performed.
|
||||
if cfg.BruteForceLoginProtectionMaxAttempts <= 0 {
|
||||
cfg.BruteForceLoginProtectionMaxAttempts = 1
|
||||
}
|
||||
|
||||
CookieSecure = security.Key("cookie_secure").MustBool(false)
|
||||
cfg.CookieSecure = CookieSecure
|
||||
|
||||
@@ -34,6 +34,9 @@ type ZanzanaSettings struct {
|
||||
ListObjectsMaxResults uint32
|
||||
// Deadline for the ListObjects() query. Default is 3 seconds.
|
||||
ListObjectsDeadline time.Duration
|
||||
// Use streamed version of list objects.
|
||||
// Returns full list of objects, but takes more time.
|
||||
UseStreamedListObjects bool
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readZanzanaSettings() {
|
||||
@@ -58,6 +61,7 @@ func (cfg *Cfg) readZanzanaSettings() {
|
||||
s.CheckQueryCacheTTL = sec.Key("check_query_cache_ttl").MustDuration(10 * time.Second)
|
||||
s.ListObjectsDeadline = sec.Key("list_objects_deadline").MustDuration(3 * time.Second)
|
||||
s.ListObjectsMaxResults = uint32(sec.Key("list_objects_max_results").MustUint(1000))
|
||||
s.UseStreamedListObjects = sec.Key("use_streamed_list_objects").MustBool(false)
|
||||
|
||||
cfg.Zanzana = s
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ type groupResource map[string]map[string]interface{}
|
||||
// For now, it makes one call to the authz service for each list items. This is known to be inefficient.
|
||||
type authzLimitedClient struct {
|
||||
client authz.AccessChecker
|
||||
// whitelist is a map of group to resources that are compatible with RBAC.
|
||||
whitelist groupResource
|
||||
// allowlist is a map of group to resources that are compatible with RBAC.
|
||||
allowlist groupResource
|
||||
logger *slog.Logger
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func NewAuthzLimitedClient(client authz.AccessChecker, opts AuthzOptions) authz.
|
||||
}
|
||||
return &authzLimitedClient{
|
||||
client: client,
|
||||
whitelist: groupResource{
|
||||
allowlist: groupResource{
|
||||
"dashboard.grafana.app": map[string]interface{}{"dashboards": nil},
|
||||
"folder.grafana.app": map[string]interface{}{"folders": nil},
|
||||
},
|
||||
@@ -107,7 +107,7 @@ func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req
|
||||
))
|
||||
defer span.End()
|
||||
if grpcutils.FallbackUsed(ctx) {
|
||||
c.logger.Debug("Check", "group", req.Group, "resource", req.Resource, "fallback", true, "rbac", false, "allowed", true)
|
||||
c.logger.Debug("Compile.Check", "group", req.Group, "resource", req.Resource, "fallback", true, "rbac", false, "allowed", true)
|
||||
return true
|
||||
}
|
||||
// TODO: Implement For now we perform the check for each item.
|
||||
@@ -134,8 +134,8 @@ func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req
|
||||
}
|
||||
|
||||
func (c authzLimitedClient) IsCompatibleWithRBAC(group, resource string) bool {
|
||||
if _, ok := c.whitelist[group]; ok {
|
||||
if _, ok := c.whitelist[group][resource]; ok {
|
||||
if _, ok := c.allowlist[group]; ok {
|
||||
if _, ok := c.allowlist[group][resource]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ func (s *StandardDocumentBuilders) GetDocumentBuilders() ([]resource.DocumentBui
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = rows.Close()
|
||||
}()
|
||||
|
||||
for rows.Next() {
|
||||
info := &dashboard.DatasourceQueryResult{}
|
||||
err = rows.Scan(&info.UID, &info.Type, &info.Name, &info.IsDefault)
|
||||
|
||||
@@ -146,6 +146,11 @@ func (b *backend) Namespaces(ctx context.Context) ([]string, error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = rows.Close()
|
||||
}()
|
||||
|
||||
for rows.Next() {
|
||||
var ns string
|
||||
err = rows.Scan(&ns)
|
||||
@@ -155,8 +160,7 @@ func (b *backend) Namespaces(ctx context.Context) ([]string, error) {
|
||||
namespaces = append(namespaces, ns)
|
||||
}
|
||||
|
||||
err = rows.Close()
|
||||
return err
|
||||
return nil
|
||||
})
|
||||
|
||||
return namespaces, err
|
||||
@@ -679,7 +683,7 @@ func (b *backend) poll(ctx context.Context, grp string, res string, since int64,
|
||||
nextRV = rec.ResourceVersion
|
||||
prevRV := rec.PreviousRV
|
||||
if prevRV == nil {
|
||||
*prevRV = int64(0)
|
||||
prevRV = new(int64)
|
||||
}
|
||||
stream <- &resource.WrittenEvent{
|
||||
WriteEvent: resource.WriteEvent{
|
||||
|
||||
@@ -165,6 +165,10 @@ func Query[T any](ctx context.Context, x db.ContextExecer, tmpl *template.Templa
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = rows.Close()
|
||||
}()
|
||||
|
||||
var ret []T
|
||||
for rows.Next() {
|
||||
v, err := scanRow(rows, req)
|
||||
|
||||
@@ -101,7 +101,7 @@ func (r *sqlResourceHistoryPollRequest) Validate() error {
|
||||
func (r *sqlResourceHistoryPollRequest) Results() (*historyPollResponse, error) {
|
||||
prevRV := r.Response.PreviousRV
|
||||
if prevRV == nil {
|
||||
*prevRV = int64(0)
|
||||
prevRV = new(int64)
|
||||
}
|
||||
return &historyPollResponse{
|
||||
Key: resource.ResourceKey{
|
||||
|
||||
@@ -1131,8 +1131,8 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) {
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedOutput: []dtos.FolderSearchHit{
|
||||
dtos.FolderSearchHit{UID: "foo", Title: "Folder 1"},
|
||||
dtos.FolderSearchHit{UID: "qux", Title: "Folder 3"},
|
||||
{UID: "foo", Title: "Folder 1"},
|
||||
{UID: "qux", Title: "Folder 3"},
|
||||
},
|
||||
permissions: folderReadAndCreatePermission,
|
||||
},
|
||||
@@ -1146,7 +1146,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) {
|
||||
params: "?parentUid=foo",
|
||||
expectedCode: http.StatusOK,
|
||||
expectedOutput: []dtos.FolderSearchHit{
|
||||
dtos.FolderSearchHit{UID: "bar", Title: "Folder 2", ParentUID: "foo"},
|
||||
{UID: "bar", Title: "Folder 2", ParentUID: "foo"},
|
||||
},
|
||||
permissions: folderReadAndCreatePermission,
|
||||
},
|
||||
|
||||
@@ -28,9 +28,9 @@ func TestIntegrationScopes(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: false, // required for experimental APIs
|
||||
AppModeProduction: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service
|
||||
featuremgmt.FlagScopeApi, // Required to register the API
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana-azure-sdk-go/v2/azcredentials"
|
||||
"github.com/grafana/grafana-azure-sdk-go/v2/azsettings"
|
||||
"github.com/grafana/grafana-azure-sdk-go/v2/azusercontext"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
@@ -139,6 +140,10 @@ func NewInstanceSettings(clientProvider *httpclient.Provider, executors map[stri
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if credentials.AzureAuthType() == azcredentials.AzureAuthCurrentUserIdentity && !backend.GrafanaConfigFromContext(ctx).FeatureToggles().IsEnabled("azureMonitorEnableUserAuth") {
|
||||
return nil, backend.DownstreamError(errors.New("current user authentication is not enabled for azure monitor"))
|
||||
}
|
||||
|
||||
model := types.DatasourceInfo{
|
||||
Credentials: credentials,
|
||||
Settings: azMonitorSettings,
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/types"
|
||||
|
||||
@@ -59,9 +60,29 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
settings backend.DataSourceInstanceSettings
|
||||
expectedModel types.DatasourceInfo
|
||||
expectedModel *types.DatasourceInfo
|
||||
Err require.ErrorAssertionFunc
|
||||
setupContext func(ctx context.Context) context.Context
|
||||
}{
|
||||
{
|
||||
name: "current user authentication disabled by feature toggle",
|
||||
settings: backend.DataSourceInstanceSettings{
|
||||
JSONData: []byte(`{"azureAuthType":"currentuser"}`),
|
||||
DecryptedSecureJSONData: map[string]string{},
|
||||
ID: 60,
|
||||
},
|
||||
expectedModel: nil,
|
||||
Err: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "current user authentication is not enabled for azure monitor")
|
||||
},
|
||||
setupContext: func(ctx context.Context) context.Context {
|
||||
featureToggles := backend.NewGrafanaCfg(map[string]string{
|
||||
featuretoggles.EnabledFeatures: "", // No enabled features
|
||||
})
|
||||
return backend.WithGrafanaConfig(ctx, featureToggles)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "creates an instance",
|
||||
settings: backend.DataSourceInstanceSettings{
|
||||
@@ -69,7 +90,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
DecryptedSecureJSONData: map[string]string{"key": "value"},
|
||||
ID: 40,
|
||||
},
|
||||
expectedModel: types.DatasourceInfo{
|
||||
expectedModel: &types.DatasourceInfo{
|
||||
Credentials: &azcredentials.AzureManagedIdentityCredentials{},
|
||||
Settings: types.AzureMonitorSettings{},
|
||||
Routes: testRoutes,
|
||||
@@ -87,7 +108,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
DecryptedSecureJSONData: map[string]string{"clientSecret": "secret"},
|
||||
ID: 50,
|
||||
},
|
||||
expectedModel: types.DatasourceInfo{
|
||||
expectedModel: &types.DatasourceInfo{
|
||||
Credentials: &azcredentials.AzureClientSecretCredentials{
|
||||
AzureCloud: "AzureCustomizedCloud",
|
||||
ClientSecret: "secret",
|
||||
@@ -117,11 +138,23 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if tt.setupContext != nil {
|
||||
ctx = tt.setupContext(ctx)
|
||||
}
|
||||
|
||||
factory := NewInstanceSettings(&httpclient.Provider{}, map[string]azDatasourceExecutor{}, log.DefaultLogger)
|
||||
instance, err := factory(context.Background(), tt.settings)
|
||||
instance, err := factory(ctx, tt.settings)
|
||||
|
||||
tt.Err(t, err)
|
||||
if !cmp.Equal(instance, tt.expectedModel) {
|
||||
t.Errorf("Unexpected instance: %v", cmp.Diff(instance, tt.expectedModel))
|
||||
|
||||
if tt.expectedModel == nil {
|
||||
require.Nil(t, instance, "Expected instance to be nil")
|
||||
} else {
|
||||
require.NotNil(t, instance, "Expected instance to be created")
|
||||
if !cmp.Equal(instance, *tt.expectedModel) {
|
||||
t.Errorf("Unexpected instance: %v", cmp.Diff(instance, *tt.expectedModel))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,17 +3,22 @@ package sqleng
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
err := e.Ping()
|
||||
if err != nil {
|
||||
logCheckHealthError(ctx, e.dsInfo, err, e.log)
|
||||
if req.PluginContext.User.Role == "Admin" {
|
||||
return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: err.Error()}, nil
|
||||
if strings.EqualFold(req.PluginContext.User.Role, "Admin") {
|
||||
return ErrToHealthCheckResult(err)
|
||||
}
|
||||
errResponse := &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusError,
|
||||
@@ -24,7 +29,51 @@ func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckH
|
||||
return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "Database Connection OK"}, nil
|
||||
}
|
||||
|
||||
func logCheckHealthError(ctx context.Context, dsInfo DataSourceInfo, err error, logger log.Logger) {
|
||||
// ErrToHealthCheckResult converts error into user friendly health check message
|
||||
// This should be called with non nil error. If the err parameter is empty, we will send Internal Server Error
|
||||
func ErrToHealthCheckResult(err error) (*backend.CheckHealthResult, error) {
|
||||
if err == nil {
|
||||
return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: "Internal Server Error"}, nil
|
||||
}
|
||||
res := &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: err.Error()}
|
||||
details := map[string]string{
|
||||
"verboseMessage": err.Error(),
|
||||
"errorDetailsLink": "https://grafana.com/docs/grafana/latest/datasources/postgres",
|
||||
}
|
||||
var opErr *net.OpError
|
||||
if errors.As(err, &opErr) {
|
||||
res.Message = "Network error: Failed to connect to the server"
|
||||
if opErr != nil && opErr.Err != nil {
|
||||
res.Message += fmt.Sprintf(". Error message: %s", opErr.Err.Error())
|
||||
}
|
||||
}
|
||||
if errors.Is(err, pq.ErrSSLNotSupported) {
|
||||
res.Message = "SSL error: Failed to connect to the server"
|
||||
}
|
||||
if strings.HasPrefix(err.Error(), "pq") {
|
||||
res.Message = "Database error: Failed to connect to the postgres server"
|
||||
if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil {
|
||||
details["verboseMessage"] = unwrappedErr.Error()
|
||||
}
|
||||
}
|
||||
var pqErr *pq.Error
|
||||
if errors.As(err, &pqErr) {
|
||||
if pqErr != nil {
|
||||
if pqErr.Code != "" {
|
||||
res.Message += fmt.Sprintf(". Postgres error code: %s", pqErr.Code.Name())
|
||||
}
|
||||
details["verboseMessage"] = pqErr.Message
|
||||
}
|
||||
}
|
||||
detailBytes, marshalErr := json.Marshal(details)
|
||||
if marshalErr != nil {
|
||||
return res, nil
|
||||
}
|
||||
res.JSONDetails = detailBytes
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func logCheckHealthError(_ context.Context, dsInfo DataSourceInfo, err error, logger log.Logger) {
|
||||
configSummary := map[string]any{
|
||||
"config_url_length": len(dsInfo.URL),
|
||||
"config_user_length": len(dsInfo.User),
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package sqleng
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/lib/pq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestErrToHealthCheckResult(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want *backend.CheckHealthResult
|
||||
}{
|
||||
{
|
||||
name: "without error",
|
||||
want: &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: "Internal Server Error"},
|
||||
},
|
||||
{
|
||||
name: "network error",
|
||||
err: errors.Join(errors.New("foo"), &net.OpError{Op: "read", Net: "tcp", Err: errors.New("some op")}),
|
||||
want: &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusError,
|
||||
Message: "Network error: Failed to connect to the server. Error message: some op",
|
||||
JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/postgres","verboseMessage":"foo\nread tcp: some op"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "db error",
|
||||
err: errors.Join(errors.New("foo"), &pq.Error{Message: pq.ErrCouldNotDetectUsername.Error(), Code: pq.ErrorCode("28P01")}),
|
||||
want: &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusError,
|
||||
Message: "foo\npq: pq: Could not detect default username. Please provide one explicitly. Postgres error code: invalid_password",
|
||||
JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/postgres","verboseMessage":"pq: Could not detect default username. Please provide one explicitly"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "regular error",
|
||||
err: errors.New("internal server error"),
|
||||
want: &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusError,
|
||||
Message: "internal server error",
|
||||
JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/postgres","verboseMessage":"internal server error"}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ErrToHealthCheckResult(tt.err)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, string(tt.want.JSONDetails), string(got.JSONDetails))
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user