Merge remote-tracking branch 'origin/main' into jguer/update-delete-role-hook

This commit is contained in:
Jo Garnier
2025-10-27 09:59:28 +00:00
710 changed files with 11235 additions and 3973 deletions
+7 -7
View File
@@ -118,18 +118,18 @@ require (
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
golang.org/x/term v0.36.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.13.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
+14 -14
View File
@@ -314,15 +314,15 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
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=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -330,8 +330,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.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
@@ -355,23 +355,23 @@ 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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8=
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
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=
+5
View File
@@ -121,11 +121,13 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/admin/provisioning", reqOrgAdmin, hs.Index)
r.Get("/admin/provisioning/*", reqOrgAdmin, hs.Index)
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) {
r.Get("/admin/migrate-to-cloud", authorize(cloudmigration.MigrationAssistantAccess), hs.Index)
}
// secrets management page
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) && hs.Features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatformUI) {
r.Get("/admin/secrets", authorize(ac.EvalAny(
ac.EvalPermission(secret.ActionSecretSecureValuesCreate),
@@ -213,6 +215,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Post("/api/user/email/start-verify", reqSignedInNoAnonymous, routing.Wrap(hs.StartEmailVerificaton))
}
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabledGlobally(featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
r.Post("/api/login/passwordless/start", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), hs.StartPasswordless)
r.Post("/api/login/passwordless/authenticate", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPasswordless))
@@ -307,11 +310,13 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Get("/quotas", authorize(ac.EvalPermission(ac.ActionOrgsQuotasRead)), routing.Wrap(hs.GetCurrentOrgQuotas))
})
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabledGlobally(featuremgmt.FlagStorage) {
// Will eventually be replaced with the 'object' route
apiRoute.Group("/storage", hs.StorageService.RegisterHTTPRoutes)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabledGlobally(featuremgmt.FlagPanelTitleSearch) {
apiRoute.Group("/search-v2", hs.SearchV2HTTPService.RegisterHTTPRoutes)
}
+1 -1
View File
@@ -1262,7 +1262,7 @@ type GetHomeDashboardResponseBody struct {
// swagger:response dashboardVersionsResponse
type DashboardVersionsResponse struct {
// in: body
Body []dashver.DashboardVersionMeta `json:"body"`
Body *dashver.DashboardVersionResponseMeta `json:"body"`
}
// swagger:response dashboardVersionResponse
+1
View File
@@ -25,6 +25,7 @@ import (
// r.Post("/api/snapshots/"
func (hs *HTTPServer) getCreatedSnapshotHandler() web.Handler {
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesSnapshots) {
namespaceMapper := request.GetNamespaceMapper(hs.Cfg)
return func(w http.ResponseWriter, r *http.Request) {
+1
View File
@@ -34,6 +34,7 @@ func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalRespons
// metrics.go
func (hs *HTTPServer) getDSQueryEndpoint() web.Handler {
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabledGlobally(featuremgmt.FlagQueryServiceRewrite) {
// rewrite requests from /ds/query to the new query service
namespaceMapper := request.GetNamespaceMapper(hs.Cfg)
+1 -1
View File
@@ -216,7 +216,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
hs.HooksService.RunIndexDataHooks(&data, c)
data.NavTree.ApplyCostManagementIA()
data.NavTree.ApplyHelpVersion(data.Settings.BuildInfo.VersionString) // RunIndexDataHooks can modify the version string
data.NavTree.Sort()
return &data, nil
@@ -304,6 +303,7 @@ func (hs *HTTPServer) getThemeForIndexData(themePrefId string, themeURLParam str
if pref.IsValidThemeID(themePrefId) {
theme := pref.GetThemeByID(themePrefId)
// TODO refactor
//nolint:staticcheck // not yet migrated to OpenFeature
if !theme.IsExtra || hs.Features.IsEnabledGlobally(featuremgmt.FlagGrafanaconThemes) {
return theme
}
+2
View File
@@ -202,6 +202,7 @@ func (hs *HTTPServer) tryAutoLogin(c *contextmodel.ReqContext) bool {
for providerName, provider := range oauthInfos {
if provider.AutoLogin || hs.Cfg.OAuthAutoLogin {
redirectUrl := hs.Cfg.AppSubURL + "/login/" + providerName
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
redirectUrl += hs.getRedirectToForAutoLogin(c)
}
@@ -213,6 +214,7 @@ func (hs *HTTPServer) tryAutoLogin(c *contextmodel.ReqContext) bool {
if samlAutoLogin {
redirectUrl := hs.Cfg.AppSubURL + "/login/saml"
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
redirectUrl += hs.getRedirectToForAutoLogin(c)
}
+1
View File
@@ -38,6 +38,7 @@ func (hs *HTTPServer) OAuthLogin(reqCtx *contextmodel.ReqContext) {
cookies.WriteCookie(reqCtx.Resp, OauthStateCookieName, redirect.Extra[authn.KeyOAuthState], hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
cookies.WriteCookie(reqCtx.Resp, "redirectTo", redirectTo, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
}
+5 -4
View File
@@ -40,6 +40,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync/installsyncfakes"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
@@ -527,7 +528,7 @@ func callGetPluginAsset(sc *scenarioContext) {
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string,
cfg *setting.Cfg, pluginRegistry registry.Service, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
require.NoError(t, err)
hs := HTTPServer{
@@ -642,7 +643,7 @@ func Test_PluginsList_AccessControl(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
require.NoError(t, err)
hs.Cfg = setting.NewCfg()
@@ -832,7 +833,7 @@ func Test_PluginsSettings(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
require.NoError(t, err)
hs.Cfg = setting.NewCfg()
@@ -902,7 +903,7 @@ func Test_UpdatePluginSetting(t *testing.T) {
t.Run("should return an error when trying to disable an auto-enabled plugin", func(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
require.NoError(t, err)
hs.Cfg = setting.NewCfg()
+2
View File
@@ -30,6 +30,8 @@ import (
func (hs *HTTPServer) registerShortURLAPI(apiRoute routing.RouteRegister) {
reqSignedIn := middleware.ReqSignedIn
//nolint:staticcheck // not yet migrated to OpenFeature
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) {
handler := newShortURLK8sHandler(hs)
apiRoute.Post("/api/short-urls", reqSignedIn, handler.createKubernetesShortURLsHandler)
+2 -2
View File
@@ -45,8 +45,8 @@ require (
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
+4 -4
View File
@@ -96,16 +96,16 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
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/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.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
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=
+6 -5
View File
@@ -5,7 +5,7 @@ go 1.25.3
require (
github.com/google/go-cmp v0.7.0
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37
github.com/grafana/grafana-app-sdk/logging v0.46.0
github.com/grafana/grafana-app-sdk/logging v0.48.1
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
@@ -84,14 +84,15 @@ require (
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/term v0.36.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.13.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
google.golang.org/grpc v1.76.0 // indirect
+12 -12
View File
@@ -71,8 +71,8 @@ github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbP
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/grafana/grafana-app-sdk/logging v0.46.0 h1:JhQ+ZK5orcmM+dZ3YZdT9uCizJEFU2I6JBNUSFWvCC8=
github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng=
github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
@@ -207,8 +207,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -219,8 +219,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.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
@@ -237,21 +237,21 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
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=
+10 -1
View File
@@ -132,6 +132,7 @@ type NewBackendOpts struct {
Tags []string
Static bool
WireTag string
CGOEnabled bool
GoBuildCache *dagger.CacheVolume
GoModCache *dagger.CacheVolume
}
@@ -198,6 +199,12 @@ func NewBackendFromString(ctx context.Context, log *slog.Logger, artifact string
goCacheProg = val
}
cgoDisabled, err := options.Bool(flags.CGODisabled)
if err != nil {
return nil, err
}
cgoEnabled := !cgoDisabled
bopts := &backend.BuildOpts{
Version: p.Version,
Enterprise: p.Enterprise,
@@ -206,6 +213,7 @@ func NewBackendFromString(ctx context.Context, log *slog.Logger, artifact string
Static: static,
WireTag: wireTag,
Tags: tags,
CGOEnabled: cgoEnabled,
}
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
@@ -233,9 +241,10 @@ func NewBackend(ctx context.Context, log *slog.Logger, artifact string, opts *Ne
Tags: opts.Tags,
Static: opts.Static,
WireTag: opts.WireTag,
CGOEnabled: opts.CGOEnabled,
}
log.Info("Initializing backend artifact with options", "static", opts.Static, "version", opts.Version, "name", opts.Name, "distro", opts.Distribution)
log.Info("Initializing backend artifact with options", "static", opts.Static, "version", opts.Version, "name", opts.Name, "distro", opts.Distribution, "cgo enabled", opts.CGOEnabled)
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Type: pipeline.ArtifactTypeDirectory,
@@ -119,7 +119,12 @@ func NewTarballFromString(ctx context.Context, log *slog.Logger, artifact string
if err != nil {
return nil, err
}
return NewTarball(ctx, log, artifact, p.Distribution, p.Enterprise, p.Name, p.Version, p.BuildID, src, yarnCache, goModCache, goBuildCache, static, wireTag, tags, goVersion, viceroyVersion, experiments)
cgoDisabled, err := options.Bool(flags.CGODisabled)
if err != nil {
return nil, err
}
cgoEnabled := !cgoDisabled
return NewTarball(ctx, log, artifact, p.Distribution, p.Enterprise, p.Name, p.Version, p.BuildID, src, yarnCache, goModCache, goBuildCache, static, wireTag, tags, goVersion, viceroyVersion, experiments, cgoEnabled)
}
// NewTarball returns a properly initialized Tarball artifact.
@@ -143,6 +148,7 @@ func NewTarball(
goVersion string,
viceroyVersion string,
experiments []string,
cgoEnabled bool,
) (*pipeline.Artifact, error) {
backendArtifact, err := NewBackend(ctx, log, artifact, &NewBackendOpts{
Name: name,
@@ -158,6 +164,7 @@ func NewTarball(
Enterprise: enterprise,
GoBuildCache: goBuildCache,
GoModCache: goModCache,
CGOEnabled: cgoEnabled,
})
if err != nil {
return nil, err
+1 -1
View File
@@ -62,7 +62,7 @@ func Build(
ldflags := LDFlagsDynamic(vcsinfo)
if opts.Static {
if opts.Static && opts.CGOEnabled {
ldflags = LDFlagsStatic(vcsinfo)
}
+37 -18
View File
@@ -19,9 +19,23 @@ type BuildOpts struct {
GoCacheProg string
Static bool
Enterprise bool
CGOEnabled bool
}
func distroOptsFunc(log *slog.Logger, distro Distribution) (DistroBuildOptsFunc, error) {
func distroOptsFunc(log *slog.Logger, distro Distribution, opts *BuildOpts) (DistroBuildOptsFunc, error) {
if !opts.CGOEnabled {
return func(distro Distribution, experiments, tags []string) *GoBuildOpts {
os, arch := OSAndArch(distro)
archv := ArchVersion(distro)
return &GoBuildOpts{
OS: os,
Arch: arch,
GoARM: GoARM(archv),
CGOEnabled: false,
}
}, nil
}
if val, ok := DistributionGoOpts[distro]; ok {
return DistroOptsLogger(log, val), nil
}
@@ -29,7 +43,7 @@ func distroOptsFunc(log *slog.Logger, distro Distribution) (DistroBuildOptsFunc,
}
func WithGoEnv(log *slog.Logger, container *dagger.Container, distro Distribution, opts *BuildOpts) (*dagger.Container, error) {
fn, err := distroOptsFunc(log, distro)
fn, err := distroOptsFunc(log, distro, opts)
if err != nil {
return nil, err
}
@@ -39,7 +53,7 @@ func WithGoEnv(log *slog.Logger, container *dagger.Container, distro Distributio
}
func WithViceroyEnv(log *slog.Logger, container *dagger.Container, distro Distribution, opts *BuildOpts) (*dagger.Container, error) {
fn, err := distroOptsFunc(log, distro)
fn, err := distroOptsFunc(log, distro, opts)
if err != nil {
return nil, err
}
@@ -91,25 +105,30 @@ func GolangContainer(
opts *BuildOpts,
) (*dagger.Container, error) {
os, _ := OSAndArch(distro)
// Only use viceroy for all darwin and only windows/amd64
if os == "darwin" || distro == DistWindowsAMD64 {
// Only use viceroy for all darwin builds
if opts.CGOEnabled && os == "darwin" {
return ViceroyContainer(d, log, distro, goVersion, viceroyVersion, opts)
}
container := golang.Container(d, platform, goVersion).
WithExec([]string{"apk", "add", "--update", "wget", "build-base", "alpine-sdk", "musl", "musl-dev", "xz"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/zig-linux-x86_64-0.11.0.tar.xz"}).
WithExec([]string{"tar", "--strip-components=1", "-C", "/", "-xf", "zig-linux-x86_64-0.11.0.tar.xz"}).
WithExec([]string{"mv", "/zig", "/bin/zig"}).
// Install the toolchain specifically for armv7 until we figure out why it's crashing w/ zig container = container.
WithExec([]string{"mkdir", "/toolchain"}).
WithExec([]string{"wget", "-q", "http://dl.grafana.com/ci/arm-linux-musleabihf-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/arm-linux-musleabihf-cross.tgz", "-C", "/toolchain"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/s390x-linux-musl-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/s390x-linux-musl-cross.tgz", "-C", "/toolchain"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/riscv64-linux-musl-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/riscv64-linux-musl-cross.tgz", "-C", "/toolchain"})
container := golang.Container(d, platform, goVersion)
if opts.CGOEnabled {
container = container.
WithExec([]string{"apk", "add", "--update", "wget", "build-base", "alpine-sdk", "musl", "musl-dev", "xz"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/zig-linux-x86_64-0.11.0.tar.xz"}).
WithExec([]string{"tar", "--strip-components=1", "-C", "/", "-xf", "zig-linux-x86_64-0.11.0.tar.xz"}).
WithExec([]string{"mv", "/zig", "/bin/zig"}).
// Install the toolchain specifically for armv7 until we figure out why it's crashing w/ zig container = container.
WithExec([]string{"mkdir", "/toolchain"}).
WithExec([]string{"wget", "-q", "http://dl.grafana.com/ci/arm-linux-musleabihf-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/arm-linux-musleabihf-cross.tgz", "-C", "/toolchain"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/s390x-linux-musl-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/s390x-linux-musl-cross.tgz", "-C", "/toolchain"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/riscv64-linux-musl-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/riscv64-linux-musl-cross.tgz", "-C", "/toolchain"}).
WithExec([]string{"wget", "-q", "https://dl.grafana.com/ci/x86_64-w64-mingw32-cross.tgz", "-P", "/toolchain"}).
WithExec([]string{"tar", "-xf", "/toolchain/x86_64-w64-mingw32-cross.tgz", "-C", "/toolchain"})
}
return WithGoEnv(log, container, distro, opts)
}
+31 -2
View File
@@ -264,7 +264,7 @@ func BuildOptsStaticS390X(distro Distribution, experiments []string, tags []stri
}
}
// BuildOptsStaticS390X builds Grafana statically for the s390x arch
// BuildOptsStaticRiscv64 builds Grafana statically for the riscv64 arch
func BuildOptsStaticRiscv64(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
var (
os, _ = OSAndArch(distro)
@@ -280,6 +280,22 @@ func BuildOptsStaticRiscv64(distro Distribution, experiments []string, tags []st
}
}
// BuildOptsStaticWindows builds Grafana statically for Windows on amd64
func BuildOptsStaticWindows(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
var (
os, _ = OSAndArch(distro)
)
return &GoBuildOpts{
CC: "/toolchain/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-gcc",
CXX: "/toolchain/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-cpp",
ExperimentalFlags: experiments,
OS: os,
Arch: "amd64",
CGOEnabled: true,
}
}
func StdZigBuildOpts(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
var (
os, arch = OSAndArch(distro)
@@ -322,6 +338,19 @@ func ViceroyBuildOpts(distro Distribution, experiments []string, tags []string)
}
}
func BuildOptsNoCGO(distro Distribution, experiments []string, tags []string) *GoBuildOpts {
var (
os, arch = OSAndArch(distro)
)
return &GoBuildOpts{
ExperimentalFlags: experiments,
OS: os,
Arch: arch,
CGOEnabled: false,
}
}
var ZigTargets = map[Distribution]string{
DistLinuxAMD64: "x86_64-linux-musl",
DistLinuxAMD64Dynamic: "x86_64-linux-gnu",
@@ -351,7 +380,7 @@ var DistributionGoOpts = map[Distribution]DistroBuildOptsFunc{
// Non-Linux distros can have whatever they want in CC and CXX; it'll get overridden
// but it's probably not best to rely on that.
DistWindowsAMD64: ViceroyBuildOpts,
DistWindowsAMD64: BuildOptsStaticWindows,
DistWindowsARM64: StdZigBuildOpts,
DistDarwinAMD64: ViceroyBuildOpts,
DistDarwinARM64: ViceroyBuildOpts,
+7 -8
View File
@@ -82,6 +82,13 @@ func GoBuildEnv(opts *GoBuildOpts) []containers.Env {
// https://github.com/mattn/go-sqlite3/issues/1164#issuecomment-1635253695
env = append(env, containers.EnvVar("CGO_CFLAGS", "-D_LARGEFILE64_SOURCE"))
if opts.CC != "" {
env = append(env, containers.EnvVar("CC", opts.CC))
}
if opts.CXX != "" {
env = append(env, containers.EnvVar("CXX", opts.CXX))
}
} else {
env = append(env, containers.EnvVar("CGO_ENABLED", "0"))
}
@@ -90,14 +97,6 @@ func GoBuildEnv(opts *GoBuildOpts) []containers.Env {
env = append(env, containers.EnvVar("GOEXPERIMENT", strings.Join(opts.ExperimentalFlags, ",")))
}
if opts.CC != "" {
env = append(env, containers.EnvVar("CC", opts.CC))
}
if opts.CXX != "" {
env = append(env, containers.EnvVar("CXX", opts.CXX))
}
return env
}
@@ -15,5 +15,6 @@ func ValidatePackage(ctx context.Context, d *dagger.Client, service *dagger.Serv
return c.WithServiceBinding("grafana", service).
WithEnvVariable("GRAFANA_URL", "http://grafana:3000").
WithEnvVariable("PW_TEST_HTML_REPORT_OPEN", "never").
WithExec([]string{"yarn", "e2e:acceptance"}), nil
}
+11
View File
@@ -19,6 +19,7 @@ const (
GoTags pipeline.FlagOption = "go-tag"
GoExperiments pipeline.FlagOption = "go-experiments"
Sign pipeline.FlagOption = "sign"
CGODisabled pipeline.FlagOption = "nocgo"
// Pretty much only used to set the deb or RPM internal package name (and file name) to `{}-nightly` and/or `{}-rpi`
Nightly pipeline.FlagOption = "nightly"
@@ -81,6 +82,13 @@ var SignFlag = pipeline.Flag{
},
}
var CGODisabledFlag = pipeline.Flag{
Name: "nocgo",
Options: map[pipeline.FlagOption]any{
CGODisabled: true,
},
}
var NightlyFlag = pipeline.Flag{
Name: "nightly",
Options: map[pipeline.FlagOption]any{
@@ -95,5 +103,8 @@ func StdPackageFlags() []pipeline.Flag {
return JoinFlags(
distros,
names,
[]pipeline.Flag{
CGODisabledFlag,
},
)
}
+1 -1
View File
@@ -13,7 +13,7 @@ require (
go.opentelemetry.io/otel v1.38.0 // indirect; @grafana/grafana-backend-group
go.opentelemetry.io/otel/sdk v1.38.0 // indirect; @grafana/grafana-backend-group
go.opentelemetry.io/otel/trace v1.38.0 // indirect; @grafana/grafana-backend-group
golang.org/x/net v0.45.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
golang.org/x/net v0.46.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
golang.org/x/sync v0.17.0 // @grafana/alerting-backend
golang.org/x/text v0.30.0 // indirect; @grafana/grafana-backend-group
google.golang.org/grpc v1.76.0 // indirect; @grafana/plugins-platform-backend
+2 -2
View File
@@ -95,8 +95,8 @@ go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOV
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+2 -2
View File
@@ -6,10 +6,10 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/subcommands v1.2.0
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
golang.org/x/tools v0.37.0
golang.org/x/tools v0.38.0
)
require (
golang.org/x/mod v0.28.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.17.0 // indirect
)
+4 -4
View File
@@ -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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
+4 -4
View File
@@ -6,10 +6,10 @@ require (
cuelang.org/go v0.11.1
github.com/dave/dst v0.27.3
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d
github.com/grafana/cog v0.0.43
github.com/grafana/cog v0.0.44
github.com/grafana/cuetsy v0.1.11
github.com/matryer/is v1.4.1
golang.org/x/tools v0.37.0
golang.org/x/tools v0.38.0
)
require (
@@ -47,8 +47,8 @@ require (
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yalue/merged_fs v1.3.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+8 -8
View File
@@ -31,8 +31,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cog v0.0.43 h1:6EDzJVc8hbP3+AjPnRnAK6mKfyWgqLKbi/jmiStilFk=
github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
github.com/grafana/cog v0.0.44 h1:N8UP7g6XBHZXf1wY7AOOWC2HdqlTPBJPJiAZQrkf4XQ=
github.com/grafana/cog v0.0.44/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ=
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws=
github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk=
@@ -100,16 +100,16 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY=
github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+1
View File
@@ -333,6 +333,7 @@ func (s *Service) buildGraph(ctx context.Context, req *Request) (*simple.Directe
case TypeCMDNode:
node, err = buildCMDNode(ctx, rn, s.features, s.cfg)
case TypeMLNode:
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabledGlobally(featuremgmt.FlagMlExpressions) {
node, err = s.buildMLNode(dp, rn, req)
if err != nil {
+1
View File
@@ -124,6 +124,7 @@ func buildCMDNode(ctx context.Context, rn *rawNode, toggles featuremgmt.FeatureT
}
if commandType == TypeSQL {
//nolint:staticcheck // not yet migrated to OpenFeature
if !toggles.IsEnabledGlobally(featuremgmt.FlagSqlExpressions) {
return nil, fmt.Errorf("sql expressions are disabled")
}
+6
View File
@@ -92,6 +92,12 @@ func ParseTracingConfig(cfg *setting.Cfg) (*TracingConfig, error) {
return nil, err
}
// Allow overriding service name via configuration
serviceName := section.Key("service_name").MustString("")
if serviceName != "" {
tc.ServiceName = serviceName
}
// if sampler_type is set in tracing.opentelemetry, we ignore the config in tracing.jaeger
sampler := section.Key("sampler_type").MustString("")
if sampler != "" {
+2 -1
View File
@@ -206,7 +206,8 @@ type Panel struct {
// NewPanel creates a new Panel object.
func NewPanel() *Panel {
return &Panel{
Transparent: (func(input bool) *bool { return &input })(false),
Transparent: (func(input bool) *bool { return &input })(false),
RepeatDirection: (func(input PanelRepeatDirection) *PanelRepeatDirection { return &input })(PanelRepeatDirectionH),
}
}
+2
View File
@@ -57,6 +57,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
Buckets: sizeDefBuckets, // 100B ... ~1MB
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagEnableNativeHTTPHistogram) {
// the recommended default value from the prom_client
// https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go#L411
@@ -70,6 +71,7 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
reqDurationOptions.NativeHistogramMinResetDuration = time.Hour
reqSizeOptions.NativeHistogramMinResetDuration = time.Hour
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagDisableClassicHTTPHistogram) {
// setting Buckets to nil with native options set means the classic
// histogram will no longer be exposed - this can be a good way to
+4 -4
View File
@@ -7,7 +7,7 @@ replace github.com/grafana/grafana/pkg/codegen => ../../codegen
require (
cuelang.org/go v0.11.1
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d
github.com/grafana/cog v0.0.43
github.com/grafana/cog v0.0.44
github.com/grafana/cuetsy v0.1.11
github.com/grafana/grafana/pkg/codegen v0.0.0-20250514132646-acbc7b54ed9e
)
@@ -43,11 +43,11 @@ require (
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yalue/merged_fs v1.3.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/tools v0.38.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+10 -10
View File
@@ -30,8 +30,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cog v0.0.43 h1:6EDzJVc8hbP3+AjPnRnAK6mKfyWgqLKbi/jmiStilFk=
github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
github.com/grafana/cog v0.0.44 h1:N8UP7g6XBHZXf1wY7AOOWC2HdqlTPBJPJiAZQrkf4XQ=
github.com/grafana/cog v0.0.44/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk=
github.com/grafana/cuetsy v0.1.11/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -94,20 +94,20 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY=
github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+5 -5
View File
@@ -97,14 +97,14 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.13.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.235.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
+12 -12
View File
@@ -319,20 +319,20 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
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.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -352,20 +352,20 @@ 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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8=
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
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=
+1
View File
@@ -503,6 +503,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
// Split dashboards when they are large
var largeObjects apistore.LargeObjectSupport
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageBigObjectsSupport) {
largeObjects = NewDashboardLargeObjectSupport(opts.Scheme, opts.StorageOpts.BlobThresholdBytes)
storageOpts.LargeObjectSupport = largeObjects
+1 -1
View File
@@ -147,7 +147,7 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob
access.CanStar = user.IsIdentityType(authlib.TypeUser)
access.AnnotationsPermissions = &dashboard.AnnotationPermission{}
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard)
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Dashboard, dashScope)
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization)
title := obj.FindTitle("")
@@ -73,6 +73,7 @@ func RegisterAPIService(
sql db.DB,
reg prometheus.Registerer,
) *SnapshotsAPIBuilder {
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil // skip registration unless opting into experimental apis
}
+3
View File
@@ -63,9 +63,11 @@ func RegisterAPIService(
reg prometheus.Registerer,
) (*DataSourceAPIBuilder, error) {
// We want to expose just a limited set of plugins
//nolint:staticcheck // not yet migrated to OpenFeature
explicitPluginList := features.IsEnabledGlobally(featuremgmt.FlagDatasourceAPIServers)
// This requires devmode!
//nolint:staticcheck // not yet migrated to OpenFeature
if !explicitPluginList && !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil, nil // skip registration unless opting into experimental apis
}
@@ -111,6 +113,7 @@ func RegisterAPIService(
datasources.GetDatasourceProvider(pluginJSON),
contextProvider,
accessControl,
//nolint:staticcheck // not yet migrated to OpenFeature
features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
false,
)
@@ -155,6 +155,7 @@ func (s *folderStorage) setDefaultFolderPermissions(ctx context.Context, orgID i
var permissions []accesscontrol.SetResourcePermissionCommand
isNested := parentUID != ""
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesDashboards) && isNested {
// No permissions on nested folders when kubernetesDashboards is enabled
return nil
+1
View File
@@ -83,6 +83,7 @@ func (b *FolderAPIBuilder) afterDelete(obj runtime.Object, _ *metav1.DeleteOptio
return
}
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
log.Info("Propagating deleted folder to Zanzana", "folder", meta.GetName(), "parent", meta.GetFolder())
err = b.permissionStore.DeleteFolderParents(ctx, meta.GetNamespace(), meta.GetName())
+1
View File
@@ -288,6 +288,7 @@ func (b *FolderAPIBuilder) setDefaultFolderPermissions(ctx context.Context, key
func (b *FolderAPIBuilder) registerPermissionHooks(store *genericregistry.Store) {
log := logging.FromContext(context.Background())
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
log.Info("Enabling Zanzana folder propagation hooks")
store.BeginCreate = b.beginCreate
+267 -15
View File
@@ -97,16 +97,22 @@ func NewResourceTuple(object string, resource iamv0.ResourcePermissionspecResour
return key, nil
}
// tupleToTupleKeyWithoutCondition converts a TupleKey to TupleKeyWithoutCondition
// This is needed for delete operations which don't support conditions
func tupleToTupleKeyWithoutCondition(tuple *v1.TupleKey) *v1.TupleKeyWithoutCondition {
return &v1.TupleKeyWithoutCondition{
User: tuple.User,
Relation: tuple.Relation,
Object: tuple.Object,
}
}
// toTupleKeysWithoutCondition converts v1.TupleKey to v1.TupleKeyWithoutCondition
// by stripping the condition field, which is required for delete operations
func toTupleKeysWithoutCondition(tuples []*v1.TupleKey) []*v1.TupleKeyWithoutCondition {
result := make([]*v1.TupleKeyWithoutCondition, len(tuples))
for i, t := range tuples {
result[i] = &v1.TupleKeyWithoutCondition{
User: t.User,
Relation: t.Relation,
Object: t.Object,
}
result[i] = tupleToTupleKeyWithoutCondition(t)
}
return result
}
@@ -119,19 +125,29 @@ func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj r
rp, ok := obj.(*iamv0.ResourcePermission)
if !ok {
b.logger.Error("failed to convert object to resourcePermission type", "object", obj)
return
}
resourceType := "resourcepermission"
operation := "create"
// Grab a ticket to write to Zanzana
// This limits the amount of concurrent writes to Zanzana
// This limits the amount of concurrent connections to Zanzana
wait := time.Now()
b.zTickets <- true
hooksWaitHistogram.Observe(time.Since(wait).Seconds()) // Record wait time
hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) // Record wait time
go func(rp *iamv0.ResourcePermission) {
start := time.Now()
status := "success"
defer func() {
// Release the ticket after write is done
<-b.zTickets
// Record operation duration and count
hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds())
hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc()
}()
resource := rp.Spec.Resource
@@ -157,6 +173,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj r
// Avoid writing if there are no valid tuples
if len(tuples) == 0 {
b.logger.Warn("no valid tuples to write", "namespace", rp.Namespace, "resource", object)
status = "failure"
return
}
@@ -176,12 +193,252 @@ func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj r
},
})
if err != nil {
status = "failure"
b.logger.Error("failed to write resource permission to zanzana",
"err", err,
"namespace", rp.Namespace,
"object", object,
"tuplesCnt", len(tuples),
)
} else {
// Record successful tuple writes
hooksTuplesCounter.WithLabelValues(resourceType, operation, "write").Add(float64(len(tuples)))
}
}(rp.DeepCopy()) // Pass a copy of the object
}
// BeginResourcePermissionUpdate is a pre-update hook that prepares zanzana updates
// It converts old and new permissions to tuples and performs the zanzana write after K8s update succeeds
func (b *IdentityAccessManagementAPIBuilder) BeginResourcePermissionUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) {
if b.zClient == nil {
return nil, nil
}
// Extract permissions from both old and new objects
oldRP, ok := oldObj.(*iamv0.ResourcePermission)
if !ok {
return nil, nil
}
newRP, ok := obj.(*iamv0.ResourcePermission)
if !ok {
return nil, nil
}
// Convert old permissions to tuples for deletion
var oldTuples []*v1.TupleKey
if len(oldRP.Spec.Permissions) > 0 {
oldResource := oldRP.Spec.Resource
oldObject := zanzana.NewObjectEntry(toZanzanaType(oldResource.ApiGroup), oldResource.ApiGroup, oldResource.Resource, "", oldResource.Name)
oldTuples = make([]*v1.TupleKey, 0, len(oldRP.Spec.Permissions))
for _, p := range oldRP.Spec.Permissions {
tuple, err := NewResourceTuple(oldObject, oldResource, p)
if err != nil {
b.logger.Error("failed to create old resource permission tuple",
"namespace", oldRP.Namespace,
"object", oldObject,
"err", err,
)
continue
}
oldTuples = append(oldTuples, tuple)
}
}
// Convert new permissions to tuples for writing
var newTuples []*v1.TupleKey
if len(newRP.Spec.Permissions) > 0 {
newResource := newRP.Spec.Resource
newObject := zanzana.NewObjectEntry(toZanzanaType(newResource.ApiGroup), newResource.ApiGroup, newResource.Resource, "", newResource.Name)
newTuples = make([]*v1.TupleKey, 0, len(newRP.Spec.Permissions))
for _, p := range newRP.Spec.Permissions {
tuple, err := NewResourceTuple(newObject, newResource, p)
if err != nil {
b.logger.Error("failed to create new resource permission tuple",
"namespace", newRP.Namespace,
"object", newObject,
"err", err,
)
continue
}
newTuples = append(newTuples, tuple)
}
}
// Return a finish function that performs the zanzana write only on success
return func(ctx context.Context, success bool) {
if !success {
// Update failed, don't write to zanzana
return
}
// Grab a ticket to write to Zanzana
// This limits the amount of concurrent connections to Zanzana
wait := time.Now()
b.zTickets <- true
hooksWaitHistogram.WithLabelValues("resourcepermission", "update").Observe(time.Since(wait).Seconds())
go func() {
start := time.Now()
status := "success"
defer func() {
<-b.zTickets
// Record operation duration and count
hooksDurationHistogram.WithLabelValues("resourcepermission", "update", status).Observe(time.Since(start).Seconds())
hooksOperationCounter.WithLabelValues("resourcepermission", "update", status).Inc()
}()
b.logger.Debug("updating resource permission in zanzana",
"namespace", newRP.Namespace,
"oldPermissionsCnt", len(oldRP.Spec.Permissions),
"newPermissionsCnt", len(newRP.Spec.Permissions),
)
ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
defer cancel()
// Prepare write request
req := &v1.WriteRequest{
Namespace: newRP.Namespace,
}
// Add deletes for old tuples
if len(oldTuples) > 0 {
deleteTuples := toTupleKeysWithoutCondition(oldTuples)
req.Deletes = &v1.WriteRequestDeletes{
TupleKeys: deleteTuples,
}
b.logger.Debug("deleting existing resource permissions from zanzana",
"namespace", newRP.Namespace,
"tuplesCnt", len(deleteTuples),
)
}
// Add writes for new tuples
if len(newTuples) > 0 {
req.Writes = &v1.WriteRequestWrites{
TupleKeys: newTuples,
}
b.logger.Debug("writing new resource permissions to zanzana",
"namespace", newRP.Namespace,
"tuplesCnt", len(newTuples),
)
}
// Only make the request if there are deletes or writes
if (req.Deletes != nil && len(req.Deletes.TupleKeys) > 0) || (req.Writes != nil && len(req.Writes.TupleKeys) > 0) {
err := b.zClient.Write(ctx, req)
if err != nil {
status = "failure"
b.logger.Error("failed to update resource permission in zanzana",
"err", err,
"namespace", newRP.Namespace,
)
} else {
// Record successful tuple operations
if len(oldTuples) > 0 {
hooksTuplesCounter.WithLabelValues("resourcepermission", "update", "delete").Add(float64(len(oldTuples)))
}
if len(newTuples) > 0 {
hooksTuplesCounter.WithLabelValues("resourcepermission", "update", "write").Add(float64(len(newTuples)))
}
}
} else {
b.logger.Debug("no tuples to update in zanzana", "namespace", newRP.Namespace)
}
}()
}, nil
}
// AfterResourcePermissionDelete is a post-delete hook that removes the resource permission from Zanzana (openFGA)
func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionDelete(obj runtime.Object, _ *metav1.DeleteOptions) {
if b.zClient == nil {
return
}
rp, ok := obj.(*iamv0.ResourcePermission)
if !ok {
b.logger.Error("failed to convert object to resourcePermission type", "object", obj)
return
}
resourceType := "resourcepermission"
operation := "delete"
// Grab a ticket to write to Zanzana
// This limits the amount of concurrent connections to Zanzana
wait := time.Now()
b.zTickets <- true
hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) // Record wait time
go func(rp *iamv0.ResourcePermission) {
start := time.Now()
status := "success"
defer func() {
// Release the ticket after write is done
<-b.zTickets
// Record operation duration and count
hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds())
hooksOperationCounter.WithLabelValues(resourceType, operation, status).Inc()
}()
resource := rp.Spec.Resource
permissions := rp.Spec.Permissions
object := zanzana.NewObjectEntry(toZanzanaType(resource.ApiGroup), resource.ApiGroup, resource.Resource, "", resource.Name)
// Generate delete tuples from the permissions
deleteTuples := make([]*v1.TupleKeyWithoutCondition, 0, len(permissions))
for _, p := range permissions {
tuple, err := NewResourceTuple(object, resource, p)
if err != nil {
b.logger.Error("failed to create resource permission tuple for deletion",
"namespace", rp.Namespace,
"object", object,
"err", err,
)
continue
}
deleteTuples = append(deleteTuples, tupleToTupleKeyWithoutCondition(tuple))
}
// Avoid writing if there are no valid tuples
if len(deleteTuples) == 0 {
b.logger.Warn("no valid tuples to delete", "namespace", rp.Namespace, "resource", object)
status = "failure"
return
}
b.logger.Debug("deleting resource permission from zanzana",
"namespace", rp.Namespace,
"object", object,
"tuplesCnt", len(deleteTuples),
)
ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
defer cancel()
err := b.zClient.Write(ctx, &v1.WriteRequest{
Namespace: rp.Namespace,
Deletes: &v1.WriteRequestDeletes{
TupleKeys: deleteTuples,
},
})
if err != nil {
status = "failure"
b.logger.Error("failed to delete resource permission from zanzana",
"err", err,
"namespace", rp.Namespace,
"object", object,
"tuplesCnt", len(deleteTuples),
)
} else {
// Record successful tuple deletions
hooksTuplesCounter.WithLabelValues(resourceType, operation, "delete").Add(float64(len(deleteTuples)))
}
}(rp.DeepCopy()) // Pass a copy of the object
}
@@ -251,7 +508,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleCreate(obj runtime.Object,
wait := time.Now()
b.zTickets <- true
hooksWaitHistogram.Observe(time.Since(wait).Seconds())
hooksWaitHistogram.WithLabelValues("role", "create").Observe(time.Since(wait).Seconds())
go func() {
defer func() {
@@ -346,7 +603,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterRoleDelete(obj runtime.Object,
wait := time.Now()
b.zTickets <- true
hooksWaitHistogram.Observe(time.Since(wait).Seconds())
hooksWaitHistogram.WithLabelValues(roleType, "delete").Observe(time.Since(wait).Seconds()) // Record wait time
go func() {
defer func() {
@@ -499,15 +756,10 @@ func (b *IdentityAccessManagementAPIBuilder) BeginRoleUpdate(ctx context.Context
return
}
// Prime the ticket channel if empty, then grab a ticket (receive) to avoid test hangs
select {
case b.zTickets <- true:
default:
}
// Grab a ticket to write to Zanzana
wait := time.Now()
<-b.zTickets
hooksWaitHistogram.Observe(time.Since(wait).Seconds())
hooksWaitHistogram.WithLabelValues(roleType, "update").Observe(time.Since(wait).Seconds()) // Record wait time
go func() {
defer func() {
+257
View File
@@ -17,6 +17,7 @@ type FakeZanzanaClient struct {
zanzana.Client
readCallback func(context.Context, *v1.ReadRequest) (*v1.ReadResponse, error)
writeCallback func(context.Context, *v1.WriteRequest) error
readCallback func(context.Context, *v1.ReadRequest) (*v1.ReadResponse, error)
}
// Read implements zanzana.Client.
@@ -32,6 +33,14 @@ func (f *FakeZanzanaClient) Write(ctx context.Context, req *v1.WriteRequest) err
return f.writeCallback(ctx, req)
}
// Read implements zanzana.Client.
func (f *FakeZanzanaClient) Read(ctx context.Context, req *v1.ReadRequest) (*v1.ReadResponse, error) {
if f.readCallback != nil {
return f.readCallback(ctx, req)
}
return &v1.ReadResponse{}, nil
}
func requireTuplesMatch(t *testing.T, actual []*v1.TupleKey, expected []*v1.TupleKey, msgAndArgs ...interface{}) {
t.Helper()
for _, exp := range expected {
@@ -164,6 +173,254 @@ func TestAfterResourcePermissionCreate(t *testing.T) {
})
}
func TestBeginResourcePermissionUpdate(t *testing.T) {
b := &IdentityAccessManagementAPIBuilder{
logger: log.NewNopLogger(),
zTickets: make(chan bool, 1),
}
t.Run("should update zanzana entries for folder resource permissions", func(t *testing.T) {
oldFolderPerm := iamv0.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-2",
},
Spec: iamv0.ResourcePermissionSpec{
Resource: iamv0.ResourcePermissionspecResource{
ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1",
},
Permissions: []iamv0.ResourcePermissionspecPermission{
{Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"},
},
},
}
newFolderPerm := iamv0.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-2",
},
Spec: iamv0.ResourcePermissionSpec{
Resource: iamv0.ResourcePermissionspecResource{
ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1",
},
Permissions: []iamv0.ResourcePermissionspecPermission{
{Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u2", Verb: "Edit"},
{Kind: iamv0.ResourcePermissionSpecPermissionKindTeam, Name: "team1", Verb: "View"},
},
},
}
testFolderWrite := func(ctx context.Context, req *v1.WriteRequest) error {
require.NotNil(t, req)
require.Equal(t, "org-2", req.Namespace)
// Should delete old permission
require.NotNil(t, req.Deletes)
require.Len(t, req.Deletes.TupleKeys, 1)
require.Equal(
t,
req.Deletes.TupleKeys[0],
&v1.TupleKeyWithoutCondition{User: "user:u1", Relation: "view", Object: "folder:fold1"},
)
// Should write new permissions
require.NotNil(t, req.Writes)
require.Len(t, req.Writes.TupleKeys, 2)
expectedWrites := []*v1.TupleKey{
{User: "user:u2", Relation: "edit", Object: "folder:fold1"},
{User: "team:team1#member", Relation: "view", Object: "folder:fold1"},
}
requireTuplesMatch(t, req.Writes.TupleKeys, expectedWrites)
return nil
}
b.zClient = &FakeZanzanaClient{writeCallback: testFolderWrite}
// Call BeginUpdate which does all the work
finishFunc, err := b.BeginResourcePermissionUpdate(context.Background(), &newFolderPerm, &oldFolderPerm, nil)
require.NoError(t, err)
require.NotNil(t, finishFunc)
// Call the finish function with success=true to trigger the zanzana write
finishFunc(context.Background(), true)
})
// Wait for the ticket to be released
<-b.zTickets
t.Run("should update zanzana entries for dashboard resource permissions", func(t *testing.T) {
oldDashPerm := iamv0.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
},
Spec: iamv0.ResourcePermissionSpec{
Resource: iamv0.ResourcePermissionspecResource{
ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1",
},
Permissions: []iamv0.ResourcePermissionspecPermission{
{Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"},
},
},
}
newDashPerm := iamv0.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
},
Spec: iamv0.ResourcePermissionSpec{
Resource: iamv0.ResourcePermissionspecResource{
ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1",
},
Permissions: []iamv0.ResourcePermissionspecPermission{
{Kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, Name: "sa1", Verb: "Edit"},
},
},
}
object := "resource:dashboard.grafana.app/dashboards/dash1"
testDashWrite := func(ctx context.Context, req *v1.WriteRequest) error {
require.NotNil(t, req)
require.Equal(t, "default", req.Namespace)
// Should delete old permission
require.NotNil(t, req.Deletes)
require.Len(t, req.Deletes.TupleKeys, 1)
require.Equal(
t,
req.Deletes.TupleKeys[0],
&v1.TupleKeyWithoutCondition{User: "user:u1", Relation: "view", Object: object},
)
// Should write new permission
require.NotNil(t, req.Writes)
require.Len(t, req.Writes.TupleKeys, 1)
tuple := req.Writes.TupleKeys[0]
require.NotNil(t, tuple.Condition)
require.Equal(t, "group_filter", tuple.Condition.Name)
tuple.Condition = nil
require.Equal(
t,
tuple,
&v1.TupleKey{User: "service-account:sa1", Relation: "edit", Object: object},
)
return nil
}
b.zClient = &FakeZanzanaClient{writeCallback: testDashWrite}
// Call BeginUpdate which does all the work
finishFunc, err := b.BeginResourcePermissionUpdate(context.Background(), &newDashPerm, &oldDashPerm, nil)
require.NoError(t, err)
require.NotNil(t, finishFunc)
// Call the finish function with success=true to trigger the zanzana write
finishFunc(context.Background(), true)
})
}
func TestAfterResourcePermissionDelete(t *testing.T) {
b := &IdentityAccessManagementAPIBuilder{
logger: log.NewNopLogger(),
zTickets: make(chan bool, 1),
}
t.Run("should delete zanzana entries for folder resource permissions", func(t *testing.T) {
folderPerm := iamv0.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Namespace: "org-2",
},
Spec: iamv0.ResourcePermissionSpec{
Resource: iamv0.ResourcePermissionspecResource{
ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1",
},
Permissions: []iamv0.ResourcePermissionspecPermission{
{Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"},
{Kind: iamv0.ResourcePermissionSpecPermissionKindBasicRole, Name: "Editor", Verb: "Edit"},
},
},
}
testFolderDelete := func(ctx context.Context, req *v1.WriteRequest) error {
require.NotNil(t, req)
require.Equal(t, "org-2", req.Namespace)
// Should have deletes but no writes
require.NotNil(t, req.Deletes)
require.Len(t, req.Deletes.TupleKeys, 2)
require.Nil(t, req.Writes)
require.Equal(
t,
req.Deletes.TupleKeys[0],
&v1.TupleKeyWithoutCondition{User: "user:u1", Relation: "view", Object: "folder:fold1"},
)
require.Equal(
t,
req.Deletes.TupleKeys[1],
&v1.TupleKeyWithoutCondition{User: "role:basic_editor#assignee", Relation: "edit", Object: "folder:fold1"},
)
return nil
}
b.zClient = &FakeZanzanaClient{writeCallback: testFolderDelete}
b.AfterResourcePermissionDelete(&folderPerm, nil)
})
// Wait for the ticket to be released
<-b.zTickets
t.Run("should delete zanzana entries for dashboard resource permissions", func(t *testing.T) {
dashPerm := iamv0.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
},
Spec: iamv0.ResourcePermissionSpec{
Resource: iamv0.ResourcePermissionspecResource{
ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1",
},
Permissions: []iamv0.ResourcePermissionspecPermission{
{Kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, Name: "sa1", Verb: "View"},
{Kind: iamv0.ResourcePermissionSpecPermissionKindTeam, Name: "team1", Verb: "Edit"},
},
},
}
testDashDelete := func(ctx context.Context, req *v1.WriteRequest) error {
object := "resource:dashboard.grafana.app/dashboards/dash1"
require.NotNil(t, req)
require.Equal(t, "default", req.Namespace)
// Should have deletes but no writes
require.NotNil(t, req.Deletes)
require.Len(t, req.Deletes.TupleKeys, 2)
require.Nil(t, req.Writes)
require.Equal(
t,
req.Deletes.TupleKeys[0],
&v1.TupleKeyWithoutCondition{User: "service-account:sa1", Relation: "view", Object: object},
)
require.Equal(
t,
req.Deletes.TupleKeys[1],
&v1.TupleKeyWithoutCondition{User: "team:team1#member", Relation: "edit", Object: object},
)
return nil
}
b.zClient = &FakeZanzanaClient{writeCallback: testDashDelete}
b.AfterResourcePermissionDelete(&dashPerm, nil)
})
// Wait for the ticket to be released
<-b.zTickets
}
func TestAfterCoreRoleCreate(t *testing.T) {
t.Run("should create zanzana entries for core role with folder permissions", func(t *testing.T) {
b := &IdentityAccessManagementAPIBuilder{
@@ -0,0 +1,2 @@
DELETE FROM {{ .Ident .TeamMemberTable }}
WHERE uid = {{ .Arg .Command.UID }}
+2
View File
@@ -37,6 +37,8 @@ type LegacyIdentityStore interface {
CreateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd CreateTeamMemberCommand) (*CreateTeamMemberResult, error)
ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error)
ListTeamMembers(ctx context.Context, ns claims.NamespaceInfo, query ListTeamMembersQuery) (*ListTeamMembersResult, error)
UpdateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateTeamMemberCommand) (*UpdateTeamMemberResult, error)
DeleteTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd DeleteTeamMemberCommand) error
}
var _ LegacyIdentityStore = (*legacySQLStore)(nil)
+30
View File
@@ -85,12 +85,24 @@ func TestIdentityQueries(t *testing.T) {
return &v
}
updateTeamMember := func(cmd *UpdateTeamMemberCommand) sqltemplate.SQLTemplate {
v := newUpdateTeamMember(nodb, cmd)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
listTeamMembers := func(q *ListTeamMembersQuery) sqltemplate.SQLTemplate {
v := newListTeamMembers(nodb, q)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
deleteTeamMember := func(q *DeleteTeamMemberCommand) sqltemplate.SQLTemplate {
v := newDeleteTeamMember(nodb, q)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
deleteTeam := func(q *DeleteTeamCommand) sqltemplate.SQLTemplate {
v := newDeleteTeam(nodb, q)
v.SQLTemplate = mocks.NewTestingSQLTemplate()
@@ -260,6 +272,16 @@ func TestIdentityQueries(t *testing.T) {
}),
},
},
sqlUpdateTeamMemberQuery: {
{
Name: "update_team_member_basic",
Data: updateTeamMember(&UpdateTeamMemberCommand{
UID: "team-member-1",
Permission: team.PermissionTypeAdmin,
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
}),
},
},
sqlQueryTeamMembersTemplate: {
{
Name: "team_1_members_page_1",
@@ -278,6 +300,14 @@ func TestIdentityQueries(t *testing.T) {
}),
},
},
sqlDeleteTeamMemberQuery: {
{
Name: "delete_team_member_basic",
Data: deleteTeamMember(&DeleteTeamMemberCommand{
UID: "team-member-1",
}),
},
},
sqlQueryUserTeamsTemplate: {
{
Name: "team_1_members_page_1",
@@ -301,6 +301,128 @@ func (s *legacySQLStore) ListTeamMembers(ctx context.Context, ns claims.Namespac
return res, err
}
type UpdateTeamMemberCommand struct {
UID string
Permission team.PermissionType
Updated legacysql.DBTime
}
type UpdateTeamMemberResult struct {
UID string
Permission team.PermissionType
Updated legacysql.DBTime
}
var sqlUpdateTeamMemberQuery = mustTemplate("update_team_member_query.sql")
func newUpdateTeamMember(sql *legacysql.LegacyDatabaseHelper, cmd *UpdateTeamMemberCommand) updateTeamMemberQuery {
return updateTeamMemberQuery{
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
TeamMemberTable: sql.Table("team_member"),
Command: cmd,
}
}
type updateTeamMemberQuery struct {
sqltemplate.SQLTemplate
TeamMemberTable string
Command *UpdateTeamMemberCommand
}
func (r updateTeamMemberQuery) Validate() error {
return nil
}
func (s *legacySQLStore) UpdateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateTeamMemberCommand) (*UpdateTeamMemberResult, error) {
now := time.Now().UTC()
cmd.Updated = legacysql.NewDBTime(now)
sql, err := s.sql(ctx)
if err != nil {
return nil, err
}
req := newUpdateTeamMember(sql, &cmd)
var result UpdateTeamMemberResult
err = sql.DB.GetSqlxSession().WithTransaction(ctx, func(st *session.SessionTx) error {
teamMemberQuery, err := sqltemplate.Execute(sqlUpdateTeamMemberQuery, req)
if err != nil {
return fmt.Errorf("failed to execute team member template %q: %w", sqlUpdateTeamMemberQuery.Name(), err)
}
_, err = st.Exec(ctx, teamMemberQuery, req.GetArgs()...)
if err != nil {
return fmt.Errorf("failed to update team member: %w", err)
}
result = UpdateTeamMemberResult(cmd)
return nil
})
if err != nil {
return nil, err
}
return &result, nil
}
type DeleteTeamMemberCommand struct {
UID string
}
var sqlDeleteTeamMemberQuery = mustTemplate("delete_team_member_query.sql")
func newDeleteTeamMember(sql *legacysql.LegacyDatabaseHelper, cmd *DeleteTeamMemberCommand) deleteTeamMemberQuery {
return deleteTeamMemberQuery{
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
TeamMemberTable: sql.Table("team_member"),
Command: cmd,
}
}
type deleteTeamMemberQuery struct {
sqltemplate.SQLTemplate
TeamMemberTable string
Command *DeleteTeamMemberCommand
}
func (r deleteTeamMemberQuery) Validate() error {
return nil
}
func (s *legacySQLStore) DeleteTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd DeleteTeamMemberCommand) error {
sql, err := s.sql(ctx)
if err != nil {
return err
}
req := newDeleteTeamMember(sql, &cmd)
if err := req.Validate(); err != nil {
return err
}
err = sql.DB.GetSqlxSession().WithTransaction(ctx, func(st *session.SessionTx) error {
teamMemberQuery, err := sqltemplate.Execute(sqlDeleteTeamMemberQuery, req)
if err != nil {
return fmt.Errorf("failed to execute team member template %q: %w", sqlDeleteTeamMemberQuery.Name(), err)
}
_, err = st.Exec(ctx, teamMemberQuery, req.GetArgs()...)
if err != nil {
return fmt.Errorf("failed to delete team member: %w", err)
}
return nil
})
if err != nil {
return err
}
return nil
}
func scanMember(rows *sql.Rows) (TeamMember, error) {
m := TeamMember{}
err := rows.Scan(&m.ID, &m.UID, &m.TeamUID, &m.TeamID, &m.UserUID, &m.UserID, &m.Name, &m.Email, &m.Username, &m.External, &m.Created, &m.Updated, &m.Permission)
@@ -0,0 +1,2 @@
DELETE FROM `grafana`.`team_member`
WHERE uid = 'team-member-1'
@@ -0,0 +1,4 @@
UPDATE `grafana`.`team_member`
SET permission = 'Admin',
updated = '2023-01-01 12:00:00'
WHERE uid = 'team-member-1'
@@ -0,0 +1,2 @@
DELETE FROM "grafana"."team_member"
WHERE uid = 'team-member-1'
@@ -0,0 +1,4 @@
UPDATE "grafana"."team_member"
SET permission = 'Admin',
updated = '2023-01-01 12:00:00'
WHERE uid = 'team-member-1'
@@ -0,0 +1,2 @@
DELETE FROM "grafana"."team_member"
WHERE uid = 'team-member-1'
@@ -0,0 +1,4 @@
UPDATE "grafana"."team_member"
SET permission = 'Admin',
updated = '2023-01-01 12:00:00'
WHERE uid = 'team-member-1'
@@ -0,0 +1,4 @@
UPDATE {{ .Ident .TeamMemberTable }}
SET permission = {{ .Arg .Command.Permission }},
updated = {{ .Arg .Command.Updated }}
WHERE uid = {{ .Arg .Command.UID }}
+38 -4
View File
@@ -14,19 +14,53 @@ const (
var (
registerOnce sync.Once
hooksWaitHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
hooksWaitHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: metricsSubSystem,
Name: "hooks_wait_duration_seconds",
Help: "Time spent in the hooks waiting for a ticket to start processing",
Buckets: prometheus.ExponentialBuckets(0.001, 2, 5), // 1ms to ~16s
})
}, []string{"resource_type", "operation"})
// hooksDurationHistogram tracks the total duration of hook operations
hooksDurationHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: metricsSubSystem,
Name: "hooks_operation_duration_seconds",
Help: "Time spent executing hook operations (create, update, delete)",
Buckets: prometheus.ExponentialBuckets(0.001, 2, 10), // 1ms to ~1s
}, []string{"resource_type", "operation", "status"})
// hooksOperationCounter tracks the number of hook operations
hooksOperationCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricsSubSystem,
Name: "hooks_operations_total",
Help: "Total number of hook operations by resource type, operation, and status",
}, []string{"resource_type", "operation", "status"})
// hooksTuplesCounter tracks the number of tuples written/deleted
hooksTuplesCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricsSubSystem,
Name: "hooks_tuples_total",
Help: "Total number of tuples written or deleted by resource type and operation type",
}, []string{"resource_type", "operation", "action"})
)
func registerMetrics(reg prometheus.Registerer) {
registerOnce.Do(func() {
if err := reg.Register(hooksWaitHistogram); err != nil {
log.New("iam.apis").Warn("failed to register iam apiserver metrics", "error", err)
metrics := []prometheus.Collector{
hooksWaitHistogram,
hooksDurationHistogram,
hooksOperationCounter,
hooksTuplesCounter,
}
for _, metric := range metrics {
if err := reg.Register(metric); err != nil {
log.New("iam.apis").Warn("failed to register iam apiserver metrics", "error", err)
}
}
})
}
+6
View File
@@ -13,7 +13,9 @@ import (
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
var _ builder.APIGroupBuilder = (*IdentityAccessManagementAPIBuilder)(nil)
@@ -59,6 +61,10 @@ type IdentityAccessManagementAPIBuilder struct {
reg prometheus.Registerer
logger log.Logger
dual dualwrite.Service
unified resource.ResourceClient
userSearchClient resourcepb.ResourceIndexClient
// non-k8s api route
display *user.LegacyDisplayREST
+25 -4
View File
@@ -42,7 +42,9 @@ import (
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
legacyuser "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
@@ -61,6 +63,9 @@ func RegisterAPIService(
coreRolesStorage CoreRoleStorageBackend,
rolesStorage RoleStorageBackend,
roleBindingsStorage RoleBindingStorageBackend,
dual dualwrite.Service,
unified resource.ResourceClient,
userService legacyuser.Service,
) (*IdentityAccessManagementAPIBuilder, error) {
dbProvider := legacysql.NewDatabaseProvider(sql)
store := legacy.NewLegacySQLStores(dbProvider)
@@ -85,6 +90,9 @@ func RegisterAPIService(
logger: log.New("iam.apis"),
features: features,
enableDualWriter: true,
dual: dual,
unified: unified,
userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), unified, user.NewUserLegacySearchClient(userService), features),
}
apiregistration.RegisterAPI(builder)
@@ -130,11 +138,13 @@ func (b *IdentityAccessManagementAPIBuilder) GetGroupVersion() schema.GroupVersi
}
func (b *IdentityAccessManagementAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis) {
if err := iamv0.AddAuthZKnownTypes(scheme); err != nil {
return err
}
}
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) {
if err := iamv0.AddResourcePermissionKnownTypes(scheme, iamv0.SchemeGroupVersion); err != nil {
return err
@@ -162,7 +172,9 @@ func (b *IdentityAccessManagementAPIBuilder) AllowedV0Alpha1Resources() []string
func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
storage := map[string]rest.Storage{}
//nolint:staticcheck // not yet migrated to OpenFeature
enableAuthnMutation := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation)
//nolint:staticcheck // not yet migrated to OpenFeature
enableZanzanaSync := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzZanzanaSync)
// teams + users must have shorter names because they are often used as part of another name
@@ -257,6 +269,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis) {
// v0alpha1
coreRoleStore, err := NewLocalStore(iamv0.CoreRoleInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.coreRolesStorage)
@@ -289,15 +302,17 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
}
storage[iamv0.RoleBindingInfo.StoragePath()] = roleBindingStore
}
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) {
resourcePermissionStore, err := NewLocalStore(iamv0.ResourcePermissionInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.resourcePermissionsStorage)
if err != nil {
return err
}
if enableZanzanaSync {
b.logger.Info("Enabling AfterCreate hook for ResourcePermission to sync to Zanzana")
b.logger.Info("Enabling AfterCreate, BeginUpdate, and AfterDelete hooks for ResourcePermission to sync to Zanzana")
resourcePermissionStore.AfterCreate = b.AfterResourcePermissionCreate
resourcePermissionStore.BeginUpdate = b.BeginResourcePermissionUpdate
resourcePermissionStore.AfterDelete = b.AfterResourcePermissionDelete
}
storage[iamv0.ResourcePermissionInfo.StoragePath()] = resourcePermissionStore
}
@@ -384,7 +399,7 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm
case admission.Create:
switch typedObj := a.GetObject().(type) {
case *iamv0.User:
return user.ValidateOnCreate(ctx, typedObj)
return user.ValidateOnCreate(ctx, b.userSearchClient, typedObj)
case *iamv0.ServiceAccount:
return serviceaccount.ValidateOnCreate(ctx, typedObj)
case *iamv0.Team:
@@ -402,7 +417,7 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm
if !ok {
return fmt.Errorf("expected old object to be a User, got %T", oldUserObj)
}
return user.ValidateOnUpdate(ctx, oldUserObj, typedObj)
return user.ValidateOnUpdate(ctx, b.userSearchClient, oldUserObj, typedObj)
case *iamv0.ResourcePermission:
return resourcepermission.ValidateCreateAndUpdateInput(ctx, typedObj)
case *iamv0.Team:
@@ -411,6 +426,12 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm
return fmt.Errorf("expected old object to be a Team, got %T", oldTeamObj)
}
return team.ValidateOnUpdate(ctx, typedObj, oldTeamObj)
case *iamv0.TeamBinding:
oldTeamBindingObj, ok := a.GetOldObject().(*iamv0.TeamBinding)
if !ok {
return fmt.Errorf("expected old object to be a TeamBinding, got %T", oldTeamBindingObj)
}
return teambinding.ValidateOnUpdate(ctx, typedObj, oldTeamBindingObj)
}
return nil
case admission.Delete:
+77 -2
View File
@@ -73,11 +73,86 @@ func (l *LegacyBindingStore) ConvertToTable(ctx context.Context, object runtime.
}
func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update")
if !l.enableAuthnMutation {
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update")
}
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, false, err
}
oldObj, err := l.Get(ctx, name, nil)
if err != nil {
return oldObj, false, err
}
obj, err := objInfo.UpdatedObject(ctx, oldObj)
if err != nil {
return oldObj, false, err
}
teamBindingObj, ok := obj.(*iamv0alpha1.TeamBinding)
if !ok {
return nil, false, fmt.Errorf("expected TeamBinding object, got %T", obj)
}
if updateValidation != nil {
if err := updateValidation(ctx, obj, oldObj); err != nil {
return oldObj, false, err
}
}
var permission team.PermissionType
switch teamBindingObj.Spec.Permission {
case iamv0alpha1.TeamBindingTeamPermissionAdmin:
permission = team.PermissionTypeAdmin
case iamv0alpha1.TeamBindingTeamPermissionMember:
permission = team.PermissionTypeMember
}
updateCmd := legacy.UpdateTeamMemberCommand{
UID: teamBindingObj.Name,
Permission: permission,
}
_, err = l.store.UpdateTeamMember(ctx, ns, updateCmd)
if err != nil {
return oldObj, false, err
}
return teamBindingObj, false, nil
}
func (l *LegacyBindingStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete")
if !l.enableAuthnMutation {
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete")
}
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, false, err
}
// Check if the team binding exists
_, err = l.Get(ctx, name, nil)
if err != nil {
return nil, false, err
}
err = l.store.DeleteTeamMember(ctx, ns, legacy.DeleteTeamMemberCommand{
UID: name,
})
if err != nil {
return nil, false, err
}
return &iamv0alpha1.TeamBinding{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns.Value,
},
}, true, nil
}
func (l *LegacyBindingStore) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
@@ -29,3 +29,28 @@ func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.TeamBinding) error {
return nil
}
func ValidateOnUpdate(ctx context.Context, obj, old *iamv0alpha1.TeamBinding) error {
_, err := identity.GetRequester(ctx)
if err != nil {
return apierrors.NewUnauthorized("no identity found")
}
if obj.Spec.TeamRef.Name != old.Spec.TeamRef.Name {
return apierrors.NewBadRequest("teamRef is immutable")
}
if obj.Spec.Subject.Name != old.Spec.Subject.Name {
return apierrors.NewBadRequest("subject is immutable")
}
if obj.Spec.External != old.Spec.External {
return apierrors.NewBadRequest("external is immutable")
}
if obj.Spec.Permission != iamv0alpha1.TeamBindingTeamPermissionAdmin && obj.Spec.Permission != iamv0alpha1.TeamBindingTeamPermissionMember {
return apierrors.NewBadRequest("invalid permission")
}
return nil
}
@@ -121,3 +121,242 @@ func TestValidateOnCreate(t *testing.T) {
})
}
}
func TestValidateOnUpdate(t *testing.T) {
tests := []struct {
name string
requester *identity.StaticRequester
old *iamv0alpha1.TeamBinding
obj *iamv0alpha1.TeamBinding
want error
}{
{
name: "valid update - permission change",
requester: &identity.StaticRequester{
Type: types.TypeUser,
OrgRole: identity.RoleAdmin,
},
old: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionMember,
External: false,
},
},
obj: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
want: nil,
},
{
name: "valid update - no changes",
requester: &identity.StaticRequester{
Type: types.TypeUser,
OrgRole: identity.RoleAdmin,
},
old: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
obj: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
want: nil,
},
{
name: "invalid update - teamRef change",
requester: &identity.StaticRequester{
Type: types.TypeUser,
OrgRole: identity.RoleAdmin,
},
old: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
obj: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team-updated",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
want: apierrors.NewBadRequest("teamRef is immutable"),
},
{
name: "invalid update - subject change",
requester: &identity.StaticRequester{
Type: types.TypeUser,
OrgRole: identity.RoleAdmin,
},
old: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
obj: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user-updated",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
want: apierrors.NewBadRequest("subject is immutable"),
},
{
name: "invalid update - external change",
requester: &identity.StaticRequester{
Type: types.TypeUser,
OrgRole: identity.RoleAdmin,
},
old: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
obj: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: true,
},
},
want: apierrors.NewBadRequest("external is immutable"),
},
{
name: "invalid update - invalid permission",
requester: &identity.StaticRequester{
Type: types.TypeUser,
OrgRole: identity.RoleAdmin,
},
old: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
obj: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: "invalid",
},
},
want: apierrors.NewBadRequest("invalid permission"),
},
{
name: "invalid update - no requester in context",
requester: nil,
old: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
obj: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
External: false,
},
},
want: apierrors.NewUnauthorized("no identity found"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := identity.WithRequester(context.Background(), test.requester)
err := ValidateOnUpdate(ctx, test.obj, test.old)
assert.Equal(t, test.want, err)
})
}
}
+166
View File
@@ -0,0 +1,166 @@
package user
import (
"context"
"fmt"
"log/slog"
"math"
"google.golang.org/grpc"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/user"
res "github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/search"
)
const (
UserResource = "users"
UserResourceGroup = "iam.grafana.com"
)
// UserLegacySearchClient is a client for searching for users in the legacy search engine.
type UserLegacySearchClient struct {
resourcepb.ResourceIndexClient
userService user.Service
log *slog.Logger
}
// NewUserLegacySearchClient creates a new UserLegacySearchClient.
func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient {
return &UserLegacySearchClient{
userService: userService,
log: slog.Default().With("logger", "legacy-user-search-client"),
}
}
// Search searches for users in the legacy search engine.
// It only supports exact matching for title, login, or email.
// FIXME: This implementation only supports a single field query and will be extended in the future.
func (c *UserLegacySearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, _ ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
signedInUser, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
if req.Limit > 100 {
req.Limit = 100
}
if req.Limit <= 0 {
req.Limit = 1
}
if req.Page > math.MaxInt32 || req.Page < 0 {
return nil, fmt.Errorf("invalid page number: %d", req.Page)
}
query := &user.SearchUsersQuery{
SignedInUser: signedInUser,
Limit: int(req.Limit),
Page: int(req.Page),
}
var title, login, email string
for _, field := range req.Options.Fields {
vals := field.GetValues()
if len(vals) != 1 {
c.log.Warn("only single value fields are supported for legacy search, using first value", "field", field.Key, "values", vals)
}
switch field.Key {
case res.SEARCH_FIELD_TITLE:
title = vals[0]
case "fields.login":
login = vals[0]
case "fields.email":
email = vals[0]
}
}
if title == "" && login == "" && email == "" {
return nil, fmt.Errorf("at least one of title, login, or email must be provided for the query")
}
// The user store's Search method combines these into an OR.
// For legacy search we can only supply one.
if title != "" {
query.Query = title
} else if login != "" {
query.Query = login
} else {
query.Query = email
}
columns := getColumns(req.Fields)
list := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: columns,
},
}
res, err := c.userService.Search(ctx, query)
if err != nil {
return nil, err
}
for _, u := range res.Users {
cells := createBaseCells(u, req.Fields)
list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{
Key: getResourceKey(u, req.Options.Key.Namespace),
Cells: cells,
})
}
list.TotalHits = res.TotalCount
return list, nil
}
func getResourceKey(item *user.UserSearchHitDTO, namespace string) *resourcepb.ResourceKey {
return &resourcepb.ResourceKey{
Namespace: namespace,
Group: UserResourceGroup,
Resource: UserResource,
Name: item.UID,
}
}
func getColumns(fields []string) []*resourcepb.ResourceTableColumnDefinition {
columns := defaultColumns()
for _, field := range fields {
switch field {
case "email":
columns = append(columns, search.TableColumnDefinitions[search.USER_EMAIL])
case "login":
columns = append(columns, search.TableColumnDefinitions[search.USER_LOGIN])
}
}
return columns
}
func createBaseCells(u *user.UserSearchHitDTO, fields []string) [][]byte {
cells := createDefaultCells(u)
for _, field := range fields {
switch field {
case "email":
cells = append(cells, []byte(u.Email))
case "login":
cells = append(cells, []byte(u.Login))
}
}
return cells
}
func createDefaultCells(u *user.UserSearchHitDTO) [][]byte {
return [][]byte{
[]byte(u.UID),
[]byte(u.Name),
}
}
func defaultColumns() []*resourcepb.ResourceTableColumnDefinition {
searchFields := res.StandardSearchFields()
return []*resourcepb.ResourceTableColumnDefinition{
searchFields.Field(res.SEARCH_FIELD_NAME),
searchFields.Field(res.SEARCH_FIELD_TITLE),
}
}
@@ -0,0 +1,57 @@
package user
import (
"context"
"google.golang.org/grpc"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
// FakeUserLegacySearchClient is a fake implementation of UserLegacySearchClient for testing.
type FakeUserLegacySearchClient struct {
resourcepb.ResourceIndexClient
SearchFunc func(ctx context.Context, req *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error)
Users []*user.UserSearchHitDTO
}
// Search calls the underlying SearchFunc or simulates a search over the Users slice.
func (c *FakeUserLegacySearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
if c.SearchFunc != nil {
return c.SearchFunc(ctx, req, opts...)
}
// Basic filtering for testing purposes
var filteredUsers []*user.UserSearchHitDTO
var queryValue string
for _, field := range req.Options.Fields {
if len(field.Values) > 0 {
queryValue = field.Values[0]
break
}
}
for _, u := range c.Users {
if u.Login == queryValue || u.Email == queryValue {
filteredUsers = append(filteredUsers, u)
}
}
rows := make([]*resourcepb.ResourceTableRow, 0, len(filteredUsers))
for _, u := range filteredUsers {
rows = append(rows, &resourcepb.ResourceTableRow{
Key: getResourceKey(u, req.Options.Key.Namespace),
Cells: createBaseCells(u, req.Fields),
})
}
return &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: getColumns(req.Fields),
Rows: rows,
},
TotalHits: int64(len(filteredUsers)),
}, nil
}
@@ -0,0 +1,148 @@
package user
import (
"context"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
res "github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
func TestUserLegacySearchClient_Search(t *testing.T) {
t.Run("should return error if no query fields are provided", func(t *testing.T) {
mockUserService := usertest.NewMockService(t)
client := NewUserLegacySearchClient(mockUserService)
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1})
req := &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{Namespace: "default"},
},
}
_, err := client.Search(ctx, req)
require.Error(t, err)
require.Equal(t, "at least one of title, login, or email must be provided for the query", err.Error())
})
testCases := []struct {
name string
fieldKey string
fieldValues []string
expectedQuery string
}{
{
name: "search by title",
fieldKey: res.SEARCH_FIELD_TITLE,
fieldValues: []string{"test user"},
expectedQuery: "test user",
},
{
name: "search by title (multiple values)",
fieldKey: res.SEARCH_FIELD_TITLE,
fieldValues: []string{"user1", "user2"},
expectedQuery: "user1",
},
{
name: "search by login",
fieldKey: "fields.login",
fieldValues: []string{"testlogin"},
expectedQuery: "testlogin",
},
{
name: "search by email",
fieldKey: "fields.email",
fieldValues: []string{"test@example.com"},
expectedQuery: "test@example.com",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockUserService := usertest.NewMockService(t)
client := NewUserLegacySearchClient(mockUserService)
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1})
req := &resourcepb.ResourceSearchRequest{
Limit: 10,
Page: 1,
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{Namespace: "default"},
Fields: []*resourcepb.Requirement{
{Key: tc.fieldKey, Values: tc.fieldValues},
},
},
Fields: []string{"email", "login"},
}
mockUsers := []*user.UserSearchHitDTO{
{ID: 1, UID: "uid1", Name: "Test User 1", Email: "test1@example.com", Login: "testlogin1"},
}
mockUserService.On("Search", mock.Anything, mock.MatchedBy(func(q *user.SearchUsersQuery) bool {
return q.Query == tc.expectedQuery && q.Limit == 10 && q.Page == 1
})).Return(&user.SearchUserQueryResult{
Users: mockUsers,
TotalCount: 1,
}, nil)
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, int64(1), resp.TotalHits)
require.Len(t, resp.Results.Rows, 1)
// Verify columns
expectedColumns := getColumns(req.Fields)
require.Equal(t, len(expectedColumns), len(resp.Results.Columns))
for i, col := range resp.Results.Columns {
require.Equal(t, expectedColumns[i].Name, col.Name)
}
// Verify rows
for i, u := range mockUsers {
row := resp.Results.Rows[i]
require.Equal(t, "default", row.Key.Namespace)
require.Equal(t, UserResourceGroup, row.Key.Group)
require.Equal(t, UserResource, row.Key.Resource)
require.Equal(t, u.UID, row.Key.Name)
expectedCells := createBaseCells(&user.UserSearchHitDTO{
UID: u.UID,
Name: u.Name,
Email: u.Email,
Login: u.Login,
}, req.Fields)
require.Equal(t, expectedCells, row.Cells)
}
})
}
t.Run("title should have precedence over login and email", func(t *testing.T) {
mockUserService := usertest.NewMockService(t)
client := NewUserLegacySearchClient(mockUserService)
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1})
req := &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{Namespace: "default"},
Fields: []*resourcepb.Requirement{
{Key: res.SEARCH_FIELD_TITLE, Values: []string{"title"}},
{Key: "fields.login", Values: []string{"login"}},
{Key: "fields.email", Values: []string{"email"}},
},
},
}
mockUserService.On("Search", mock.Anything, mock.MatchedBy(func(q *user.SearchUsersQuery) bool {
return q.Query == "title"
})).Return(&user.SearchUserQueryResult{Users: []*user.UserSearchHitDTO{}, TotalCount: 0}, nil)
_, err := client.Search(ctx, req)
require.NoError(t, err)
})
}
+3 -3
View File
@@ -82,7 +82,7 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda
UID: name,
Login: userObj.Spec.Login,
Email: userObj.Spec.Email,
Name: userObj.Spec.Name,
Name: userObj.Spec.Title,
IsAdmin: userObj.Spec.GrafanaAdmin,
IsDisabled: userObj.Spec.Disabled,
EmailVerified: userObj.Spec.EmailVerified,
@@ -258,7 +258,7 @@ func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createVali
UID: userObj.Name,
Login: userObj.Spec.Login,
Email: userObj.Spec.Email,
Name: userObj.Spec.Name,
Name: userObj.Spec.Title,
IsAdmin: userObj.Spec.GrafanaAdmin,
IsDisabled: userObj.Spec.Disabled,
EmailVerified: userObj.Spec.EmailVerified,
@@ -284,7 +284,7 @@ func toUserItem(u *common.UserWithRole, ns string) iamv0alpha1.User {
CreationTimestamp: metav1.NewTime(u.Created),
},
Spec: iamv0alpha1.UserSpec{
Name: u.Name,
Title: u.Name,
Login: u.Login,
Email: u.Email,
EmailVerified: u.EmailVerified,
+114 -18
View File
@@ -5,13 +5,15 @@ import (
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/selection"
"github.com/grafana/authlib/types"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error {
func ValidateOnCreate(ctx context.Context, userSearchClient resourcepb.ResourceIndexClient, obj *iamv0alpha1.User) error {
requester, err := identity.GetRequester(ctx)
if err != nil {
return apierrors.NewUnauthorized("no identity found")
@@ -28,27 +30,22 @@ func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error {
return apierrors.NewBadRequest("user must have either login or email")
}
err = validateRole(obj)
if err != nil {
if err := validateRole(obj); err != nil {
return err
}
if err := validateEmail(ctx, userSearchClient, requester.GetNamespace(), obj.Name, obj.Spec.Email); err != nil {
return err
}
if err := validateLogin(ctx, userSearchClient, requester.GetNamespace(), obj.Name, obj.Spec.Login); err != nil {
return err
}
return nil
}
func validateRole(obj *iamv0alpha1.User) error {
if obj.Spec.Role == "" {
return apierrors.NewBadRequest("role is required")
}
if !identity.RoleType(obj.Spec.Role).IsValid() {
return apierrors.NewBadRequest(fmt.Sprintf("invalid role '%s'", obj.Spec.Role))
}
return nil
}
func ValidateOnUpdate(ctx context.Context, oldObj, newObj *iamv0alpha1.User) error {
func ValidateOnUpdate(ctx context.Context, userSearchClient resourcepb.ResourceIndexClient, oldObj, newObj *iamv0alpha1.User) error {
requester, err := identity.GetRequester(ctx)
if err != nil {
return apierrors.NewUnauthorized("no identity found")
@@ -93,10 +90,109 @@ func ValidateOnUpdate(ctx context.Context, oldObj, newObj *iamv0alpha1.User) err
return apierrors.NewBadRequest("user must have either login or email")
}
err = validateRole(newObj)
if err != nil {
if err := validateRole(newObj); err != nil {
return err
}
if newObj.Spec.Email != oldObj.Spec.Email {
if err := validateEmail(ctx, userSearchClient, requester.GetNamespace(), newObj.Name, newObj.Spec.Email); err != nil {
return err
}
}
if newObj.Spec.Login != oldObj.Spec.Login {
if err := validateLogin(ctx, userSearchClient, requester.GetNamespace(), newObj.Name, newObj.Spec.Login); err != nil {
return err
}
}
return nil
}
func validateRole(obj *iamv0alpha1.User) error {
if obj.Spec.Role == "" {
return apierrors.NewBadRequest("role is required")
}
if !identity.RoleType(obj.Spec.Role).IsValid() {
return apierrors.NewBadRequest(fmt.Sprintf("invalid role '%s'", obj.Spec.Role))
}
return nil
}
func validateEmail(ctx context.Context, searchClient resourcepb.ResourceIndexClient, namespace, name, email string) error {
req := createUserSearchRequest(namespace, []*resourcepb.Requirement{
{
Key: "fields.email",
Operator: string(selection.Equals),
Values: []string{email},
},
}, []string{"name", "email", "login"})
resp, err := searchClient.Search(ctx, req)
if err != nil {
return err
}
// FIXME(mgyongyosi): Improve the exact match validation
if resp.TotalHits > 0 {
// If the found user is the same as the one being created/updated, it's not a conflict.
// This is required for Mode 2 when the resource is written to LegacyStorage and UnifiedStorage.
rows := resp.Results.Rows
if len(rows) > 0 && rows[0].Key.Name == name {
return nil
}
return apierrors.NewConflict(iamv0alpha1.UserResourceInfo.GroupResource(),
name,
fmt.Errorf("email '%s' is already taken", email))
}
return nil
}
func validateLogin(ctx context.Context, searchClient resourcepb.ResourceIndexClient, namespace, name, login string) error {
req := createUserSearchRequest(namespace, []*resourcepb.Requirement{
{
Key: "fields.login",
Operator: string(selection.Equals),
Values: []string{login},
},
}, []string{"name", "email", "login"})
resp, err := searchClient.Search(ctx, req)
if err != nil {
return err
}
// FIXME(mgyongyosi): Improve the exact match validation
if resp.TotalHits > 0 {
// If the found user is the same as the one being created/updated, it's not a conflict.
// This is required for Mode 2 when the resource is written to LegacyStorage and UnifiedStorage.
rows := resp.Results.Rows
if len(rows) > 0 && rows[0].Key.Name == name {
return nil
}
return apierrors.NewConflict(iamv0alpha1.UserResourceInfo.GroupResource(),
name,
fmt.Errorf("login '%s' is already taken", login))
}
return nil
}
func createUserSearchRequest(namespace string, requirements []*resourcepb.Requirement, fields []string) *resourcepb.ResourceSearchRequest {
userGvr := iamv0alpha1.UserResourceInfo.GroupResource()
return &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Group: userGvr.Group,
Resource: userGvr.Resource,
Namespace: namespace,
},
Fields: requirements,
},
Fields: fields,
}
}
+146 -12
View File
@@ -9,6 +9,9 @@ import (
"github.com/grafana/authlib/types"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestValidateOnCreate(t *testing.T) {
@@ -16,6 +19,7 @@ func TestValidateOnCreate(t *testing.T) {
name string
user *iamv0alpha1.User
requester *identity.StaticRequester
searchClient resourcepb.ResourceIndexClient
expectError bool
errorContains string
}{
@@ -31,7 +35,8 @@ func TestValidateOnCreate(t *testing.T) {
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: false,
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "grafana admin creating another grafana admin",
@@ -46,7 +51,8 @@ func TestValidateOnCreate(t *testing.T) {
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: false,
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "non-admin trying to create a grafana admin",
@@ -61,6 +67,7 @@ func TestValidateOnCreate(t *testing.T) {
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: true,
errorContains: "only grafana admins can create grafana admins",
},
@@ -75,6 +82,7 @@ func TestValidateOnCreate(t *testing.T) {
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: true,
errorContains: "user must have either login or email",
},
@@ -90,13 +98,14 @@ func TestValidateOnCreate(t *testing.T) {
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
expectError: false,
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "user with only email",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Email: "test@test.com",
Email: "test@example",
Role: "Viewer",
},
},
@@ -104,7 +113,8 @@ func TestValidateOnCreate(t *testing.T) {
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
expectError: false,
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "user with empty role",
@@ -117,6 +127,7 @@ func TestValidateOnCreate(t *testing.T) {
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: true,
errorContains: "role is required",
},
@@ -132,6 +143,7 @@ func TestValidateOnCreate(t *testing.T) {
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: true,
errorContains: "invalid role 'InvalidRole'",
},
@@ -147,7 +159,55 @@ func TestValidateOnCreate(t *testing.T) {
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: false,
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "user with existing email",
user: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{
Email: "existing@example",
Role: "Viewer",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{
Users: []*user.UserSearchHitDTO{
{Email: "existing@example"},
},
},
expectError: true,
errorContains: "email 'existing@example' is already taken",
},
{
name: "user with existing login",
user: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{
Login: "existinguser",
Email: "existinguser@example",
Role: "Viewer",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{
Users: []*user.UserSearchHitDTO{
{Login: "existinguser"},
},
},
expectError: true,
errorContains: "login 'existinguser' is already taken",
},
}
@@ -158,7 +218,7 @@ func TestValidateOnCreate(t *testing.T) {
tt.requester,
)
err := ValidateOnCreate(ctx, tt.user)
err := ValidateOnCreate(ctx, tt.searchClient, tt.user)
if tt.expectError {
require.Error(t, err)
@@ -178,6 +238,7 @@ func TestValidateOnUpdate(t *testing.T) {
oldUser *iamv0alpha1.User
newUser *iamv0alpha1.User
requester *identity.StaticRequester
searchClient resourcepb.ResourceIndexClient
expectError bool
errorContains string
}{
@@ -254,7 +315,7 @@ func TestValidateOnUpdate(t *testing.T) {
{
name: "update with only login",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Email: "test@test.com", Role: "Viewer"},
Spec: iamv0alpha1.UserSpec{Email: "test@example", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Email: "", Role: "Viewer"},
@@ -263,7 +324,8 @@ func TestValidateOnUpdate(t *testing.T) {
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: false,
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "update with only email",
@@ -271,13 +333,14 @@ func TestValidateOnUpdate(t *testing.T) {
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "", Email: "test@test.com", Role: "Viewer"},
Spec: iamv0alpha1.UserSpec{Login: "", Email: "test@example", Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: false,
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "service user verifies email",
@@ -408,6 +471,77 @@ func TestValidateOnUpdate(t *testing.T) {
},
expectError: false,
},
{
name: "update with existing email",
oldUser: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{Email: "one@example", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{Email: "two@example", Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{
Users: []*user.UserSearchHitDTO{
{Email: "two@example"},
},
},
expectError: true,
errorContains: "email 'two@example' is already taken",
},
{
name: "update with existing login",
oldUser: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{Login: "one", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{Login: "two", Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{
Users: []*user.UserSearchHitDTO{
{Name: "other", UID: "uid456", Login: "two"},
},
},
expectError: true,
errorContains: "login 'two' is already taken",
},
{
name: "update with no change to login or email",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Email: "test@example", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Email: "test@example", Role: "Editor"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{
Users: []*user.UserSearchHitDTO{
{Login: "testuser", Email: "test@example"},
},
},
expectError: false,
},
}
for _, tt := range tests {
@@ -417,7 +551,7 @@ func TestValidateOnUpdate(t *testing.T) {
tt.requester,
)
err := ValidateOnUpdate(ctx, tt.oldUser, tt.newUser)
err := ValidateOnUpdate(ctx, tt.searchClient, tt.oldUser, tt.newUser)
if tt.expectError {
require.Error(t, err)
+2 -2
View File
@@ -22,7 +22,7 @@ import (
)
func (b *APIBuilder) proxyAllFlagReq(ctx context.Context, isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(ctx, "ofrep.proxy.evalAllFlags")
ctx, span := tracing.Start(ctx, "ofrep.proxy.evalAllFlags")
defer span.End()
r = r.WithContext(ctx)
@@ -70,7 +70,7 @@ func (b *APIBuilder) proxyAllFlagReq(ctx context.Context, isAuthedUser bool, w h
}
func (b *APIBuilder) proxyFlagReq(ctx context.Context, flagKey string, isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(ctx, "ofrep.proxy.evalFlag")
ctx, span := tracing.Start(ctx, "ofrep.proxy.evalFlag")
defer span.End()
r = r.WithContext(ctx)
+11 -15
View File
@@ -11,7 +11,6 @@ import (
"github.com/gorilla/mux"
"github.com/grafana/grafana/pkg/infra/tracing"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,8 +34,6 @@ var _ builder.APIGroupBuilder = (*APIBuilder)(nil)
var _ builder.APIGroupRouteProvider = (*APIBuilder)(nil)
var _ builder.APIGroupVersionProvider = (*APIBuilder)(nil)
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/registry/apis/ofrep")
const ofrepPath = "/ofrep/v1/evaluate/flags"
const namespaceMismatchMsg = "rejecting request with namespace mismatch"
@@ -246,13 +243,14 @@ func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes {
}
func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "ofrep.handler.evalFlag")
ctx, span := tracing.Start(r.Context(), "ofrep.handler.evalFlag")
defer span.End()
r = r.WithContext(ctx)
b.logger.Debug("validating namespace in oneFlagHandler handler")
if !b.validateNamespace(r) {
valid := b.validateNamespace(r)
b.logger.Debug("validating namespace in oneFlagHandler handler", "valid", valid)
if !valid {
_ = tracing.Errorf(span, namespaceMismatchMsg)
span.SetAttributes(semconv.HTTPStatusCode(http.StatusUnauthorized))
b.logger.Error(namespaceMismatchMsg)
@@ -291,13 +289,15 @@ func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) {
}
func (b *APIBuilder) allFlagsHandler(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "ofrep.handler.evalAllFlags")
ctx, span := tracing.Start(r.Context(), "ofrep.handler.evalAllFlags")
defer span.End()
r = r.WithContext(ctx)
b.logger.Debug("validating namespace in allFlagsHandler handler")
if !b.validateNamespace(r) {
valid := b.validateNamespace(r)
b.logger.Debug("validating namespace in allFlagsHandler handler", "valid", valid)
if !valid {
_ = tracing.Errorf(span, namespaceMismatchMsg)
span.SetAttributes(semconv.HTTPStatusCode(http.StatusUnauthorized))
b.logger.Error(namespaceMismatchMsg)
@@ -327,8 +327,7 @@ func writeResponse(statusCode int, result any, logger log.Logger, w http.Respons
func (b *APIBuilder) namespaceFromEvalCtx(body []byte) string {
// TODO: eval ctx should be added to span attributes, not log
// Adding it temporary for debugging
b.logger.Debug("evaluation context from request", "ctx", body)
b.logger.Debug("evaluation context from request", "ctx", string(body))
var evalCtx struct {
// Extract namespace from request body without consuming it
@@ -342,9 +341,6 @@ func (b *APIBuilder) namespaceFromEvalCtx(body []byte) string {
return ""
}
// Adding it temporary for debugging
b.logger.Debug("evaluation context decoded", "namespace", evalCtx.Context.Namespace)
if evalCtx.Context.Namespace == "" {
b.logger.Debug("namespace missing from evaluation context", "namespace", evalCtx.Context.Namespace)
return ""
@@ -364,7 +360,7 @@ func (b *APIBuilder) isAuthenticatedRequest(r *http.Request) bool {
// validateNamespace checks if the namespace in the evaluation context matches the namespace in the request
func (b *APIBuilder) validateNamespace(r *http.Request) bool {
_, span := tracer.Start(r.Context(), "ofrep.validateNamespace")
_, span := tracing.Start(r.Context(), "ofrep.validateNamespace")
defer span.End()
var namespace string
+2 -2
View File
@@ -10,7 +10,7 @@ import (
)
func (b *APIBuilder) evalAllFlagsStatic(ctx context.Context, isAuthedUser bool, w http.ResponseWriter) {
_, span := tracer.Start(ctx, "ofrep.static.evalAllFlags")
_, span := tracing.Start(ctx, "ofrep.static.evalAllFlags")
defer span.End()
result, err := b.staticEvaluator.EvalAllFlags(ctx)
@@ -40,7 +40,7 @@ func (b *APIBuilder) evalAllFlagsStatic(ctx context.Context, isAuthedUser bool,
}
func (b *APIBuilder) evalFlagStatic(ctx context.Context, flagKey string, w http.ResponseWriter) {
_, span := tracer.Start(ctx, "ofrep.static.evalFlag")
_, span := tracing.Start(ctx, "ofrep.static.evalFlag")
defer span.End()
span.SetAttributes(attribute.String("flag_key", flagKey))
@@ -52,6 +52,7 @@ func RegisterAPIService(
apiregistration builder.APIRegistrar,
) *APIBuilder {
// Requires development settings and clearly experimental
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil
}
@@ -238,6 +238,7 @@ func RegisterAPIService(
extraWorkers []jobs.Worker,
repoFactory repository.Factory,
) (*APIBuilder, error) {
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabledGlobally(featuremgmt.FlagProvisioning) {
return nil, nil
}
+5
View File
@@ -19,6 +19,9 @@ import (
//
// The usage of strings.ToLower is because the server would convert `FromAlert` to `Fromalert`. So the make matching
// easier, we just match all headers in lower case.
//
// the headers X-Real-IP and X-Forwarded-For are used by the HostedGrafanaACHeaderMiddleware at
// https://github.com/grafana/grafana/blob/f191acf8114ab79609fc631e0f01fe2b47371188/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go#L107
var expectedHeaders = map[string]string{
strings.ToLower(models.FromAlertHeaderName): models.FromAlertHeaderName,
strings.ToLower(models.CacheSkipHeaderName): models.CacheSkipHeaderName,
@@ -37,6 +40,8 @@ var expectedHeaders = map[string]string{
strings.ToLower(queryService.HeaderPanelPluginId): queryService.HeaderPanelPluginId,
strings.ToLower(queryService.HeaderDashboardTitle): queryService.HeaderDashboardTitle,
strings.ToLower(queryService.HeaderPanelTitle): queryService.HeaderPanelTitle,
strings.ToLower("X-Real-IP"): "X-Real-IP",
strings.ToLower("X-Forwarded-For"): "X-Forwarded-For",
}
func ExtractKnownHeaders(header http.Header) map[string]string {
+3
View File
@@ -65,6 +65,7 @@ func NewQueryAPIBuilder(
) (*QueryAPIBuilder, error) {
// Include well typed query definitions
var queryTypes *query.QueryTypeDefinitionList
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes) {
// Read the expression query definitions
raw, err := expr.QueryTypeDefinitionListJSON()
@@ -179,6 +180,7 @@ func (b *QueryAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIG
storage := map[string]rest.Storage{}
// Get a list of all datasource instances
//nolint:staticcheck // not yet migrated to OpenFeature
if b.features.IsEnabledGlobally(featuremgmt.FlagQueryServiceWithConnections) {
// Eventually this would be backed either by search or reconciler pattern
storage[query.ConnectionResourceInfo.StoragePath()] = &connectionAccess{
@@ -188,6 +190,7 @@ func (b *QueryAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIG
plugins := newPluginsStorage(b.registry)
storage[plugins.resourceInfo.StoragePath()] = plugins
//nolint:staticcheck // not yet migrated to OpenFeature
if !b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
// The plugin registry is still experimental, and not yet accurate
// For standard k8s api discovery to work, at least one resource must be registered
+1
View File
@@ -26,6 +26,7 @@ func NewServiceAPIBuilder() *ServiceAPIBuilder {
}
func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, registerer prometheus.Registerer) *ServiceAPIBuilder {
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) {
return nil // skip registration unless opting into aggregator mode
}
@@ -28,6 +28,7 @@ func ConvertToK8sResource(orgID int64, r definitions.Route, version string, name
RepeatInterval: optionalPrometheusDurationToString(r.RepeatInterval),
Receiver: r.Receiver,
},
Routes: make([]model.RoutingTreeRoute, 0, len(r.Routes)),
}
for _, route := range r.Routes {
if route == nil {
+6
View File
@@ -43,18 +43,22 @@ func ProvideAppInstallers(
playlistAppInstaller,
pluginsApplInstaller,
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) {
installers = append(installers, shorturlAppInstaller)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesAlertingRules) && rulesAppInstaller != nil {
installers = append(installers, rulesAppInstaller)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesCorrelations) {
installers = append(installers, correlationsAppInstaller)
}
if alertingNotificationAppInstaller != nil {
installers = append(installers, alertingNotificationAppInstaller)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesLogsDrilldown) {
installers = append(installers, logsdrilldownAppInstaller)
}
@@ -97,10 +101,12 @@ func ProvideBuilderRunners(
var apiGroupRunner *runner.APIGroupRunner
var err error
providers := []app.Provider{}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagInvestigationsBackend) {
logger.Debug("Investigations backend is enabled")
providers = append(providers, investigationAppProvider)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagGrafanaAdvisor) &&
!slices.Contains(grafanaCfg.DisablePlugins, "grafana-advisor-app") {
providers = append(providers, advisorAppProvider)
+1
View File
@@ -44,6 +44,7 @@ func RegisterAppInstaller(
service: p,
}
specificConfig := any(&playlistapp.PlaylistConfig{
//nolint:staticcheck // not yet migrated to OpenFeature
EnableReconcilers: features.IsEnabledGlobally(featuremgmt.FlagPlaylistsReconciler),
})
provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, playlistapp.New)
+6 -7
View File
@@ -10,14 +10,13 @@ import (
"github.com/grafana/grafana-app-sdk/app"
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
"github.com/grafana/grafana-app-sdk/simple"
"github.com/grafana/grafana/apps/plugins/pkg/apis"
pluginsappapis "github.com/grafana/grafana/apps/plugins/pkg/apis"
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app"
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app"
)
var (
@@ -38,13 +37,13 @@ func RegisterAppInstaller(
cfg: cfg,
}
specificConfig := any(nil)
provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, pluginsapp.New)
provider := simple.NewAppProvider(pluginsappapis.LocalManifest(), specificConfig, pluginsapp.New)
appConfig := app.Config{
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
ManifestData: *apis.LocalManifest().ManifestData,
ManifestData: *pluginsappapis.LocalManifest().ManifestData,
SpecificConfig: specificConfig,
}
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, &apis.GoTypeAssociator{})
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, pluginsappapis.NewGoTypeAssociator())
if err != nil {
return nil, err
}
+7 -2
View File
@@ -26,6 +26,7 @@ import (
"github.com/grafana/grafana/pkg/services/authz"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/frontend"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
@@ -46,8 +47,9 @@ func NewModule(opts Options,
license licensing.Licensing,
moduleRegisterer ModuleRegisterer,
storageBackend resource.StorageBackend, // Ensures unified storage backend is initialized
hooksService *hooks.HooksService,
) (*ModuleServer, error) {
s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license, moduleRegisterer, storageBackend)
s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license, moduleRegisterer, storageBackend, hooksService)
if err != nil {
return nil, err
}
@@ -70,6 +72,7 @@ func newModuleServer(opts Options,
license licensing.Licensing,
moduleRegisterer ModuleRegisterer,
storageBackend resource.StorageBackend,
hooksService *hooks.HooksService,
) (*ModuleServer, error) {
rootCtx, shutdownFn := context.WithCancel(context.Background())
@@ -93,6 +96,7 @@ func newModuleServer(opts Options,
license: license,
moduleRegisterer: moduleRegisterer,
storageBackend: storageBackend,
hooksService: hooksService,
}
return s, nil
@@ -134,6 +138,7 @@ type ModuleServer struct {
// moduleRegisterer allows registration of modules provided by other builds (e.g. enterprise).
moduleRegisterer ModuleRegisterer
hooksService *hooks.HooksService
}
// init initializes the server and its services.
@@ -205,7 +210,7 @@ func (s *ModuleServer) Run() error {
})
m.RegisterModule(modules.FrontendServer, func() (services.Service, error) {
return frontend.ProvideFrontendService(s.cfg, s.features, s.promGatherer, s.registerer, s.license)
return frontend.ProvideFrontendService(s.cfg, s.features, s.promGatherer, s.registerer, s.license, s.hooksService)
})
m.RegisterModule(modules.OperatorServer, s.initOperatorServer)
+5 -1
View File
@@ -27,6 +27,8 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/modules"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
@@ -330,8 +332,10 @@ func initModuleServerForTest(
apiOpts api.ServerOptions,
) testModuleServer {
tracer := tracing.InitializeTracerForTest()
hooksService := hooks.ProvideService()
license := &licensing.OSSLicensingService{}
ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, nil, ProvideNoopModuleRegisterer(), nil)
ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, license, ProvideNoopModuleRegisterer(), nil, hooksService)
require.NoError(t, err)
conn, err := grpc.NewClient(cfg.GRPCServer.Address,
+1
View File
@@ -128,6 +128,7 @@ func (s *Server) Init() error {
return err
}
//nolint:staticcheck // not yet migrated to OpenFeature
if !s.features.IsEnabledGlobally(featuremgmt.FlagPluginStoreServiceLoading) {
if err := s.roleRegistry.RegisterFixedRoles(s.context); err != nil {
return err
+2
View File
@@ -123,6 +123,7 @@ import (
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
"github.com/grafana/grafana/pkg/services/pluginsintegration"
pluginDashboards "github.com/grafana/grafana/pkg/services/pluginsintegration/dashboards"
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/preference/prefimpl"
promTypeMigration "github.com/grafana/grafana/pkg/services/promtypemigration"
@@ -250,6 +251,7 @@ var wireBasicSet = wire.NewSet(
httpclientprovider.New,
wire.Bind(new(httpclient.Provider), new(*sdkhttpclient.Provider)),
serverlock.ProvideService,
wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)),
annotationsimpl.ProvideCleanupService,
wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)),
cleanup.ProvideService,
+17 -6
View File
File diff suppressed because one or more lines are too long
@@ -108,6 +108,7 @@ func ProvideZanzanaReconciler(cfg *setting.Cfg, features featuremgmt.FeatureTogg
// Run implements registry.BackgroundService
func (r *ZanzanaReconciler) Run(ctx context.Context) error {
//nolint:staticcheck // not yet migrated to OpenFeature
if r.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
return r.Reconcile(ctx)
}
@@ -48,6 +48,7 @@ func (l *FixedRolesLoader) running(ctx context.Context) error {
}
func (l *FixedRolesLoader) IsDisabled() bool {
//nolint:staticcheck // not yet migrated to OpenFeature
return !l.features.IsEnabledGlobally(featuremgmt.FlagPluginStoreServiceLoading)
}
@@ -2,8 +2,10 @@ package ossaccesscontrol
import (
"context"
"errors"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -22,6 +24,8 @@ type FolderPermissionsService struct {
*resourcepermissions.Service
}
var ErrFolderUnhandledError = errutil.Internal("folder.unhandled-error", errutil.WithPublicMessage("Unhandled folder error"))
var FolderViewActions = []string{dashboards.ActionFoldersRead, accesscontrol.ActionAlertingRuleRead, libraryelements.ActionLibraryPanelsRead, accesscontrol.ActionAlertingSilencesRead}
var FolderEditActions = append(FolderViewActions, []string{
dashboards.ActionFoldersWrite,
@@ -106,7 +110,16 @@ func ProvideFolderPermissions(
})
if err != nil {
return err
switch {
case func() bool {
var errUtilErr errutil.Error
return errors.As(err, &errUtilErr)
}():
return err
case errors.Is(err, dashboards.ErrFolderNotFound):
return folder.ErrFolderNotFound.Errorf("folder not found")
}
return ErrFolderUnhandledError.Errorf("unhandled folder error: %w", err)
}
return nil
@@ -361,6 +361,7 @@ func (s *Service) mapPermission(permission string) ([]string, error) {
actions = append(actions, GetActionSetName(s.options.Resource, permission))
// If we only want to store action sets, return now
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabledGlobally(featuremgmt.FlagOnlyStoreActionSets) {
return actions, nil
}
+1
View File
@@ -406,6 +406,7 @@ func InstallAPIs(
}
// if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok {
for name := range resources {
+41
View File
@@ -0,0 +1,41 @@
package apiserver
import (
"context"
"sync"
"github.com/grafana/grafana-app-sdk/k8s"
"github.com/grafana/grafana-app-sdk/resource"
)
// ProvideClientGenerator creates a lazy-initialized ClientGenerator.
func ProvideClientGenerator(restConfigProvider RestConfigProvider) resource.ClientGenerator {
return &lazyClientGenerator{
restConfigProvider: restConfigProvider,
}
}
type lazyClientGenerator struct {
restConfigProvider RestConfigProvider
clientGenerator resource.ClientGenerator
initOnce sync.Once
initError error
}
func (g *lazyClientGenerator) ClientFor(kind resource.Kind) (resource.Client, error) {
g.initOnce.Do(func() {
restConfig, err := g.restConfigProvider.GetRestConfig(context.Background())
if err != nil {
g.initError = err
return
}
restConfig.APIPath = "apis"
g.clientGenerator = k8s.NewClientRegistry(*restConfig, k8s.DefaultClientConfig())
})
if g.initError != nil {
return nil, g.initError
}
return g.clientGenerator.ClientFor(kind)
}
+1
View File
@@ -69,6 +69,7 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o
unifiedStorageCfg := cfg.UnifiedStorage
o.StorageOptions.UnifiedStorageConfig = unifiedStorageCfg
//nolint:staticcheck // not yet migrated to OpenFeature
o.ExtraOptions.DevMode = features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerEnsureKubectlAccess)
o.ExtraOptions.ExternalAddress = host
o.ExtraOptions.APIURL = apiURL
+2
View File
@@ -423,7 +423,9 @@ func (s *service) start(ctx context.Context) error {
delegate := server
var runningServer *genericapiserver.GenericAPIServer
//nolint:staticcheck // not yet migrated to OpenFeature
isKubernetesAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator)
//nolint:staticcheck // not yet migrated to OpenFeature
isDataplaneAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagDataplaneAggregator)
if isKubernetesAggregatorEnabled {
+1
View File
@@ -15,4 +15,5 @@ var WireSet = wire.NewSet(
ProvideService,
wire.Bind(new(Service), new(*service)),
wire.Bind(new(builder.APIRegistrar), new(*service)),
ProvideClientGenerator,
)
+9 -16
View File
@@ -303,7 +303,7 @@ func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateC
log := s.log.FromContext(ctx).New("tokenID", token.Id, "userID", token.UserId, "createdAt", token.CreatedAt, "rotatedAt", token.RotatedAt)
// Avoid multiple instances in HA mode rotating at the same time.
if s.features.IsEnabled(ctx, featuremgmt.FlagSkipTokenRotationIfRecent) && time.Unix(token.RotatedAt, 0).Add(SkipRotationTime).After(getTime()) {
if time.Unix(token.RotatedAt, 0).Add(SkipRotationTime).After(getTime()) {
log.Debug("Token was last rotated very recently, skipping rotation")
span.SetAttributes(attribute.Bool("skipped", true))
return token, nil
@@ -327,16 +327,13 @@ func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateC
}
res, err, _ := s.singleflight.Do(cmd.UnHashedToken, func() (any, error) {
if s.features.IsEnabled(ctx, featuremgmt.FlagSkipTokenRotationIfRecent) {
var token *auth.UserToken
err := s.sqlStore.InTransaction(ctx, func(ctx context.Context) error {
var err error
token, err = rotate(ctx)
return err
})
return token, err
}
return rotate(ctx)
var token *auth.UserToken
err := s.sqlStore.InTransaction(ctx, func(ctx context.Context) error {
var err error
token, err = rotate(ctx)
return err
})
return token, err
})
if err != nil {
@@ -375,11 +372,7 @@ func (s *UserAuthTokenService) rotateToken(ctx context.Context, token *auth.User
now := getTime()
var affected int64
withDbSession := s.sqlStore.WithDbSession
if !s.features.IsEnabled(ctx, featuremgmt.FlagSkipTokenRotationIfRecent) {
withDbSession = s.sqlStore.WithTransactionalDbSession
}
err = withDbSession(ctx, func(dbSession *db.Session) error {
err = s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
res, err := dbSession.Exec(sql, userAgent, clientIPStr, hashedToken, s.sqlStore.GetDialect().BooleanValue(false), now.Unix(), token.Id)
if err != nil {
return err
@@ -20,7 +20,6 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/user"
@@ -721,7 +720,6 @@ func createTestContext(t *testing.T) *testContext {
log: log.New("test-logger"),
singleflight: new(singleflight.Group),
externalSessionStore: extSessionStore,
features: featuremgmt.WithFeatures(featuremgmt.FlagSkipTokenRotationIfRecent),
tracer: tracer,
}
+1
View File
@@ -281,6 +281,7 @@ func handleLogin(r *http.Request, w http.ResponseWriter, cfg *setting.Cfg, ident
WriteSessionCookie(w, cfg, identity.SessionToken)
redirectURL := cfg.AppSubURL + "/"
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
if redirectToCookieName != "" {
scopedRedirectToCookie, err := r.Cookie(redirectToCookieName)
@@ -58,6 +58,7 @@ func ProvideRegistration(
var passwordClients []authn.PasswordClient
// always register LDAP if LDAP is enabled in SSO settings
//nolint:staticcheck // not yet migrated to OpenFeature
if cfg.LDAPAuthEnabled || features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsLDAP) {
ldap := clients.ProvideLDAP(cfg, ldapService, userService, authInfoService, tracer)
proxyClients = append(proxyClients, ldap)
@@ -125,6 +126,7 @@ func ProvideRegistration(
authnSvc.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthTokenService, socialService, settingsProviderService, features, tracer))
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagProvisioning) {
authnSvc.RegisterClient(clients.ProvideProvisioning())
}
@@ -140,11 +142,13 @@ func ProvideRegistration(
authnSvc.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService, tracer, features).SyncOauthTokenHook, 60)
authnSvc.RegisterPostAuthHook(userSync.FetchSyncedUserHook, 100)
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagEnableSCIM) {
authnSvc.RegisterPostAuthHook(userSync.ValidateUserProvisioningHook, 30)
}
rbacSync := sync.ProvideRBACSync(accessControlService, tracer, permRegistry)
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagCloudRBACRoles) {
authnSvc.RegisterPostAuthHook(rbacSync.SyncCloudRoles, 110)
authnSvc.RegisterPreLogoutHook(gcomsso.ProvideGComSSOService(cfg).LogoutHook, 50)

Some files were not shown because too many files have changed in this diff Show More