merge upstream

This commit is contained in:
Ryan McKinley
2024-07-03 20:16:34 -07:00
293 changed files with 3309 additions and 3315 deletions
+2 -2
View File
@@ -250,12 +250,12 @@ func setupScenarioContextSamlLogout(t *testing.T, url string) *scenarioContext {
// FIXME: This user should not be anonymous
func authedUserWithPermissions(userID, orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
return &user.SignedInUser{UserID: userID, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}}
return &user.SignedInUser{UserID: userID, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), permissions)}}
}
// FIXME: This user should not be anonymous
func userWithPermissions(orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
return &user.SignedInUser{IsAnonymous: true, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}}
return &user.SignedInUser{IsAnonymous: true, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), permissions)}}
}
func setupSimpleHTTPServer(features featuremgmt.FeatureToggles) *HTTPServer {
+2
View File
@@ -18,6 +18,7 @@ type UpdatePrefsCmd struct {
QueryHistory *pref.QueryHistoryPreference `json:"queryHistory,omitempty"`
Language string `json:"language"`
Cookies []pref.CookieType `json:"cookies,omitempty"`
Navbar *pref.NavbarPreference `json:"navbar,omitempty"`
}
// swagger:model
@@ -34,4 +35,5 @@ type PatchPrefsCmd struct {
QueryHistory *pref.QueryHistoryPreference `json:"queryHistory,omitempty"`
HomeDashboardUID *string `json:"homeDashboardUID,omitempty"`
Cookies []pref.CookieType `json:"cookies,omitempty"`
Navbar *pref.NavbarPreference `json:"navbar,omitempty"`
}
+4 -3
View File
@@ -1,6 +1,7 @@
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
@@ -282,7 +283,7 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
req := server.NewGetRequest("/api/folders/folderUid?accesscontrol=true")
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("folderUid")},
}),
@@ -311,7 +312,7 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
req := server.NewGetRequest("/api/folders/folderUid?accesscontrol=true")
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("parentUid")},
{Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("folderUid")},
@@ -336,7 +337,7 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
req := server.NewGetRequest("/api/folders/folderUid")
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("folderUid")},
}),
+4 -3
View File
@@ -1,6 +1,7 @@
package api
import (
"context"
"net/http"
"strings"
"testing"
@@ -220,7 +221,7 @@ func TestAPIEndpoint_DeleteOrgs(t *testing.T) {
expectedIdentity := &authn.Identity{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction(tt.permission),
1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permission),
},
}
@@ -269,8 +270,8 @@ func TestAPIEndpoint_GetOrg(t *testing.T) {
ID: authn.MustParseNamespaceID("user:1"),
OrgID: 1,
Permissions: map[int64]map[string][]string{
0: accesscontrol.GroupScopesByAction(tt.permissions),
1: accesscontrol.GroupScopesByAction(tt.permissions),
0: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions),
1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions),
},
}
+4 -4
View File
@@ -70,7 +70,7 @@ func TestCallResource(t *testing.T) {
t.Run("Test successful response is received for valid request", func(t *testing.T) {
req := srv.NewPostRequest("/api/plugins/grafana-testdata-datasource/resources/test", strings.NewReader(`{"test": "true"}`))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
{Action: pluginaccesscontrol.ActionAppAccess, Scope: pluginaccesscontrol.ScopeProvider.GetResourceAllScope()},
}),
}})
@@ -92,7 +92,7 @@ func TestCallResource(t *testing.T) {
t.Run("Test successful response is received for valid request with the colon character", func(t *testing.T) {
req := srv.NewPostRequest("/api/plugins/grafana-testdata-datasource/resources/test-*,*:test-*/_mapping", strings.NewReader(`{"test": "true"}`))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
{Action: pluginaccesscontrol.ActionAppAccess, Scope: pluginaccesscontrol.ScopeProvider.GetResourceAllScope()},
}),
}})
@@ -146,7 +146,7 @@ func TestCallResource(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
req := srv.NewPostRequest(tc.url, strings.NewReader(`{"test": "true"}`))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
{Action: pluginaccesscontrol.ActionAppAccess, Scope: pluginaccesscontrol.ScopeProvider.GetResourceAllScope()},
}),
}})
@@ -192,7 +192,7 @@ func TestCallResource(t *testing.T) {
t.Run("Test error is properly propagated to API response", func(t *testing.T) {
req := srv.NewGetRequest("/api/plugins/grafana-testdata-datasource/resources/scenarios")
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
{Action: pluginaccesscontrol.ActionAppAccess, Scope: pluginaccesscontrol.ScopeProvider.GetResourceAllScope()},
}),
}})
+1 -1
View File
@@ -104,7 +104,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
Permissions: map[int64]map[string][]string{},
OrgRoles: map[int64]org.RoleType{},
}
expectedIdentity.Permissions[tc.permissionOrg] = ac.GroupScopesByAction(tc.permissions)
expectedIdentity.Permissions[tc.permissionOrg] = ac.GroupScopesByActionContext(context.Background(), tc.permissions)
hs.authnService = &authntest.FakeService{
ExpectedIdentity: expectedIdentity,
}
+1
View File
@@ -155,6 +155,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te
Language: dtoCmd.Language,
QueryHistory: dtoCmd.QueryHistory,
CookiePreferences: dtoCmd.Cookies,
Navbar: dtoCmd.Navbar,
}
if err := hs.preferenceService.Patch(ctx, &patchCmd); err != nil {
+2 -1
View File
@@ -1,6 +1,7 @@
package api
import (
"context"
"fmt"
"net/http"
"strings"
@@ -156,7 +157,7 @@ func TestAPIEndpoint_PutOrgQuotas(t *testing.T) {
Permissions: map[int64]map[string][]string{},
}
for orgID, permissions := range tt.permissions {
expectedIdentity.Permissions[orgID] = accesscontrol.GroupScopesByAction(permissions)
expectedIdentity.Permissions[orgID] = accesscontrol.GroupScopesByActionContext(context.Background(), permissions)
}
server := SetupAPITestServer(t, func(hs *HTTPServer) {
+3
View File
@@ -46,6 +46,9 @@ type FeatureSpec struct {
// Do not show the value in docs
HideFromDocs bool `json:"hideFromDocs,omitempty"`
// Expression to determine if the flag is enabled by default
Expression string `json:"expression,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+52 -40
View File
@@ -12,63 +12,75 @@ import (
)
type Key struct {
Group string
Resource string
Namespace string
Name string
Group string `json:"group,omitempty"`
Resource string `json:"resource"`
Namespace string `json:"namespace,omitempty"`
Name string `json:"name,omitempty"`
}
func ParseKey(key string) (*Key, error) {
// /<group>/<resource>[/namespaces/<namespace>][/<name>]
parts := strings.Split(key, "/")
if len(parts) < 3 {
return nil, fmt.Errorf("invalid key (expecting at least 2 parts): %s", key)
// ParseKey parses a key string into a Key.
// Format: [/group/<group>]/resource/<resource>[/namespace/<namespace>][/name/<name>]
func ParseKey(raw string) (*Key, error) {
parts := strings.Split(raw, "/")
key := &Key{}
// Skip the first empty string
if parts[0] == "" {
parts = parts[1:]
}
if parts[0] != "" {
return nil, fmt.Errorf("invalid key (expecting leading slash): %s", key)
for i := 0; i < len(parts); i += 2 {
k := parts[i]
if i+1 >= len(parts) {
return nil, fmt.Errorf("invalid key: %s", raw)
}
v := parts[i+1]
switch k {
case "group":
key.Group = v
case "resource":
key.Resource = v
case "namespace":
key.Namespace = v
case "name":
key.Name = v
default:
return nil, fmt.Errorf("invalid key name: %s", key)
}
}
k := &Key{
Group: parts[1],
Resource: parts[2],
if len(key.Resource) == 0 {
return nil, fmt.Errorf("missing resource: %s", raw)
}
if len(parts) == 3 {
return k, nil
}
if parts[3] != "namespaces" {
k.Name = parts[3]
return k, nil
}
if len(parts) < 5 {
return nil, fmt.Errorf("invalid key (expecting namespace after 'namespaces'): %s", key)
}
k.Namespace = parts[4]
if len(parts) == 5 {
return k, nil
}
k.Name = parts[5]
return k, nil
return key, nil
}
// String returns the string representation of the Key.
func (k *Key) String() string {
s := "/" + k.Group + "/" + k.Resource
var builder strings.Builder
if len(k.Group) > 0 {
builder.WriteString("/group/")
builder.WriteString(k.Group)
}
if len(k.Resource) > 0 {
builder.WriteString("/resource/")
builder.WriteString(k.Resource)
}
if len(k.Namespace) > 0 {
s += "/namespaces/" + k.Namespace
builder.WriteString("/namespace/")
builder.WriteString(k.Namespace)
}
if len(k.Name) > 0 {
s += "/" + k.Name
builder.WriteString("/name/")
builder.WriteString(k.Name)
}
return s
return builder.String()
}
// IsEqual returns true if the keys are equal.
func (k *Key) IsEqual(other *Key) bool {
return k.Group == other.Group &&
k.Resource == other.Resource &&
+120
View File
@@ -0,0 +1,120 @@
package generic
import (
"reflect"
"testing"
)
func TestParseKey(t *testing.T) {
tests := []struct {
name string
raw string
expected *Key
wantErr bool
}{
{
name: "All keys",
raw: "/group/test-group/resource/test-resource/namespace/test-namespace/name/test-name",
expected: &Key{Group: "test-group", Resource: "test-resource", Namespace: "test-namespace", Name: "test-name"},
wantErr: false,
},
{
name: "Missing group",
raw: "/resource/test-resource/namespace/test-namespace/name/test-name",
expected: &Key{Group: "", Resource: "test-resource", Namespace: "test-namespace", Name: "test-name"},
wantErr: false,
},
{
name: "Missing namespace",
raw: "/group/test-group/resource/test-resource/name/test-name",
expected: &Key{Group: "test-group", Resource: "test-resource", Namespace: "", Name: "test-name"},
wantErr: false,
},
{
name: "Missing name",
raw: "/group/test-group/resource/test-resource/namespace/test-namespace",
expected: &Key{Group: "test-group", Resource: "test-resource", Namespace: "test-namespace", Name: ""},
wantErr: false,
},
{
name: "Missing resource",
raw: "/group/test-group/namespace/test-namespace/name/test-name",
expected: nil,
wantErr: true,
},
{
name: "Empty string",
raw: "",
expected: nil,
wantErr: true,
},
{
name: "Invalid key",
raw: "/",
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseKey(tt.raw)
if (err != nil) != tt.wantErr {
t.Errorf("ParseKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("ParseKey() = %v, expected %v", got, tt.expected)
}
})
}
}
func BenchmarkKey_String(b *testing.B) {
key := &Key{Group: "test-group", Resource: "test-resource", Namespace: "test-namespace", Name: "test-name"}
for i := 0; i < b.N; i++ {
_ = key.String()
}
}
func TestKey_String(t *testing.T) {
tests := []struct {
name string
key *Key
expected string
}{
{
name: "All fields",
key: &Key{Group: "test-group", Resource: "test-resource", Namespace: "test-namespace", Name: "test-name"},
expected: "/group/test-group/resource/test-resource/namespace/test-namespace/name/test-name",
},
{
name: "Missing group",
key: &Key{Resource: "test-resource", Namespace: "test-namespace", Name: "test-name"},
expected: "/resource/test-resource/namespace/test-namespace/name/test-name",
},
{
name: "Missing namespace",
key: &Key{Group: "test-group", Resource: "test-resource", Name: "test-name"},
expected: "/group/test-group/resource/test-resource/name/test-name",
},
{
name: "Missing name",
key: &Key{Group: "test-group", Resource: "test-resource", Namespace: "test-namespace"},
expected: "/group/test-group/resource/test-resource/namespace/test-namespace",
},
{
name: "Missing resource",
key: &Key{Group: "test-group", Namespace: "test-namespace", Name: "test-name"},
expected: "/group/test-group/namespace/test-namespace/name/test-name",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.key.String()
if got != tt.expected {
t.Errorf("Key.String() = %s, expected %s", got, tt.expected)
}
})
}
}
@@ -753,6 +753,9 @@ type Spec struct {
// List of dashboard panels
Panels []any `json:"panels,omitempty"`
// When set to true, the dashboard will load all panels in the dashboard when it's loaded.
Preload *bool `json:"preload,omitempty"`
// Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d".
Refresh *string `json:"refresh,omitempty"`
@@ -16,6 +16,11 @@ type CookiePreferences struct {
Performance map[string]any `json:"performance,omitempty"`
}
// NavbarPreference defines model for NavbarPreference.
type NavbarPreference struct {
SavedItemIds []string `json:"savedItemIds"`
}
// QueryHistoryPreference defines model for QueryHistoryPreference.
type QueryHistoryPreference struct {
// HomeTab one of: '' | 'query' | 'starred';
@@ -32,6 +37,7 @@ type Spec struct {
// Selected language (beta)
Language *string `json:"language,omitempty"`
Navbar *NavbarPreference `json:"navbar,omitempty"`
QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"`
// Theme light, dark, empty is default
+22 -14
View File
@@ -74,32 +74,40 @@ func RouteOperationName(req *http.Request) (string, bool) {
func RequestTracing(tracer tracing.Tracer) web.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.URL.Path, "/public/") || req.URL.Path == "/robots.txt" || req.URL.Path == "/favicon.ico" {
// skip tracing for a few endpoints
if strings.HasPrefix(req.URL.Path, "/public/") ||
req.URL.Path == "/robots.txt" ||
req.URL.Path == "/favicon.ico" {
next.ServeHTTP(w, req)
return
}
rw := web.Rw(w, req)
// Extract the parent span context from the incoming request.
ctx := otel.GetTextMapPropagator().Extract(req.Context(), propagation.HeaderCarrier(req.Header))
wireContext := otel.GetTextMapPropagator().Extract(req.Context(), propagation.HeaderCarrier(req.Header))
ctx, span := tracer.Start(wireContext, fmt.Sprintf("HTTP %s %s", req.Method, req.URL.Path), trace.WithLinks(trace.LinkFromContext(wireContext)))
req = req.WithContext(ctx)
next.ServeHTTP(w, req)
// Only call span.Finish when a route operation name have been set,
// meaning that not set the span would not be reported.
// generic span name for requests where there's no route operation name
spanName := fmt.Sprintf("HTTP %s <unknown>", req.Method)
// TODO: do not depend on web.Context from the future
if routeOperation, exists := RouteOperationName(web.FromContext(req.Context()).Req); exists {
defer span.End()
span.SetName(fmt.Sprintf("HTTP %s %s", req.Method, routeOperation))
spanName = fmt.Sprintf("HTTP %s %s", req.Method, routeOperation)
}
ctx, span := tracer.Start(ctx, spanName, trace.WithAttributes(
semconv.HTTPURLKey.String(req.RequestURI),
semconv.HTTPMethodKey.String(req.Method),
), trace.WithSpanKind(trace.SpanKindServer))
defer span.End()
req = req.WithContext(ctx)
// Ensure the response writer's status can be captured.
rw := web.Rw(w, req)
next.ServeHTTP(rw, req)
status := rw.Status()
span.SetAttributes(semconv.HTTPStatusCode(status))
span.SetAttributes(semconv.HTTPURL(req.RequestURI))
span.SetAttributes(semconv.HTTPMethod(req.Method))
if status >= 400 {
span.SetStatus(codes.Error, fmt.Sprintf("error with HTTP status code %s", strconv.Itoa(status)))
}
+77
View File
@@ -0,0 +1,77 @@
package promlib
import (
"context"
"fmt"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var (
_ backend.AdmissionHandler = (*Service)(nil)
)
// ValidateAdmission implements backend.AdmissionHandler.
func (s *Service) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
rsp, err := s.MutateAdmission(ctx, req)
if rsp != nil {
return &backend.ValidationResponse{
Allowed: rsp.Allowed,
Result: rsp.Result,
Warnings: rsp.Warnings,
}, err
}
return nil, err
}
// MutateAdmission implements backend.AdmissionHandler.
func (s *Service) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
expected := (&backend.DataSourceInstanceSettings{}).GVK()
if req.Kind.Kind != expected.Kind && req.Kind.Group != expected.Group {
return getBadRequest("expected DataSourceInstanceSettings protobuf payload"), nil
}
// Convert the payload from protobuf to an SDK struct
settings, err := backend.DataSourceInstanceSettingsFromProto(req.ObjectBytes, "")
if err != nil {
return nil, err
}
if settings == nil {
return getBadRequest("missing datasource settings"), nil
}
switch settings.APIVersion {
case "", "v0alpha1":
// OK!
default:
return getBadRequest(fmt.Sprintf("expected apiVersion: v0alpha1, found: %s", settings.APIVersion)), nil
}
if settings.URL != "" {
return getBadRequest("unsupported URL value"), nil
}
pb, err := backend.DataSourceInstanceSettingsToProtoBytes(settings)
return &backend.MutationResponse{
Allowed: true,
ObjectBytes: pb,
}, err
}
// ConvertObject implements backend.AdmissionHandler.
func (s *Service) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
return nil, fmt.Errorf("not implemented")
}
func getBadRequest(msg string) *backend.MutationResponse {
return &backend.MutationResponse{
Allowed: false,
Result: &backend.StatusResult{
Status: "Failure",
Message: msg,
Reason: string(metav1.StatusReasonBadRequest),
Code: http.StatusBadRequest,
},
}
}
+7
View File
@@ -13,6 +13,7 @@ require (
go.opentelemetry.io/otel v1.26.0
go.opentelemetry.io/otel/trace v1.26.0
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f
k8s.io/apimachinery v0.29.3
)
require (
@@ -118,7 +119,13 @@ require (
google.golang.org/grpc v1.64.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
+6
View File
@@ -187,6 +187,7 @@ github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -289,14 +290,19 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+2
View File
@@ -67,6 +67,8 @@ func RegisterAPIService(
var err error
var builder *DataSourceAPIBuilder
all := pluginStore.Plugins(context.Background(), plugins.TypeDataSource)
// ATTENTION: Adding a datasource here requires the plugin to implement
// an AdmissionHandler to validate the datasource settings.
ids := []string{
"grafana-testdata-datasource",
"prometheus",
+20 -21
View File
@@ -12,8 +12,13 @@ import (
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/accesscontrol")
type AccessControl interface {
// Evaluate evaluates access to the given resources.
Evaluate(ctx context.Context, user identity.Requester, evaluator Evaluator) (bool, error)
@@ -232,30 +237,24 @@ func BuildPermissionsMap(permissions []Permission) map[string]bool {
}
// GroupScopesByAction will group scopes on action
//
// Deprecated: use GroupScopesByActionContext instead
func GroupScopesByAction(permissions []Permission) map[string][]string {
// Use a map to deduplicate scopes.
// User can have the same permission from multiple sources (e.g. team, basic role, directly assigned etc).
// User will also have duplicate permissions if action sets are used, as we will be double writing permissions for a while.
m := make(map[string]map[string]struct{})
return GroupScopesByActionContext(context.Background(), permissions)
}
// GroupScopesByAction will group scopes on action
func GroupScopesByActionContext(ctx context.Context, permissions []Permission) map[string][]string {
_, span := tracer.Start(ctx, "accesscontrol.GroupScopesByActionContext", trace.WithAttributes(
attribute.Int("permissions_count", len(permissions)),
))
defer span.End()
m := make(map[string][]string)
for i := range permissions {
if _, ok := m[permissions[i].Action]; !ok {
m[permissions[i].Action] = make(map[string]struct{})
}
m[permissions[i].Action][permissions[i].Scope] = struct{}{}
m[permissions[i].Action] = append(m[permissions[i].Action], permissions[i].Scope)
}
res := make(map[string][]string, len(m))
for action, scopes := range m {
scopeList := make([]string, len(scopes))
i := 0
for scope := range scopes {
scopeList[i] = scope
i++
}
res[action] = scopeList
}
return res
return m
}
// Reduce will reduce a list of permissions to its minimal form, grouping scopes by action
@@ -1,8 +1,11 @@
package accesscontrol
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
// this import is needed for github.com/grafana/grafana/pkg/web hack_wrap to work
@@ -125,3 +128,54 @@ func TestReduce(t *testing.T) {
})
}
}
func TestGroupScopesByActionContext(t *testing.T) {
// test data = 3 actions with 2+i scopes each, including a duplicate
permissions := []Permission{}
for i := 0; i < 3; i++ {
for j := 0; j < 2+i; j++ {
permissions = append(permissions, Permission{
Action: fmt.Sprintf("action:%d", i),
Scope: fmt.Sprintf("scope:%d_%d", i, j),
})
}
}
expected := map[string][]string{}
for i := 0; i < 3; i++ {
action := fmt.Sprintf("action:%d", i)
scopes := []string{}
for j := 0; j < 2+i; j++ {
scopes = append(scopes, fmt.Sprintf("scope:%d_%d", i, j))
}
expected[action] = scopes
}
assert.EqualValues(t, expected, GroupScopesByActionContext(context.Background(), permissions))
}
func BenchmarkGroupScopesByAction(b *testing.B) {
// create a big list of permissions with a bunch of duplicates
permissions := []Permission{}
for i := 0; i < 100; i++ {
for j := 0; j < 500+i; j++ {
permissions = append(permissions, Permission{
Action: fmt.Sprintf("action:%d", i),
Scope: fmt.Sprintf("scope:%d_%d", i, j),
})
}
// add duplicate scopes
for j := 0; j < 10; j++ {
permissions = append(permissions, Permission{
Action: fmt.Sprintf("action:%d", i),
Scope: fmt.Sprintf("scope:%d_%d", i, 0),
})
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
GroupScopesByActionContext(context.Background(), permissions)
}
}
+46 -3
View File
@@ -114,6 +114,7 @@ func (s *Service) GetUsageStats(_ context.Context) map[string]any {
func (s *Service) GetUserPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
ctx, span := s.tracer.Start(ctx, "authz.GetUserPermissionsOSS")
defer span.End()
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
defer timer.ObserveDuration()
@@ -125,6 +126,9 @@ func (s *Service) GetUserPermissions(ctx context.Context, user identity.Requeste
}
func (s *Service) getUserPermissions(ctx context.Context, user identity.Requester, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
ctx, span := s.tracer.Start(ctx, "authz.getUserPermissions")
defer span.End()
permissions := make([]accesscontrol.Permission, 0)
for _, builtin := range accesscontrol.GetOrgRoles(user) {
if basicRole, ok := s.roles[builtin]; ok {
@@ -265,8 +269,10 @@ func (s *Service) getCachedBasicRolesPermissions(ctx context.Context, user ident
defer span.End()
basicRoles := accesscontrol.GetOrgRoles(user)
span.SetAttributes(attribute.Int("roles", len(basicRoles)))
for _, role := range basicRoles {
perms, err := s.getCachedBasicRolePermissions(ctx, role, user.GetOrgID(), options)
span.SetAttributes(attribute.Int(fmt.Sprintf("role_%s_permissions", role), len(perms)))
if err != nil {
return nil, err
}
@@ -301,12 +307,13 @@ type getPermissionsFunc = func(ctx context.Context) ([]accesscontrol.Permission,
// Generic method for getting various permissions from cache
func (s *Service) getCachedPermissions(ctx context.Context, key string, getPermissionsFn getPermissionsFunc, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
_, span := s.tracer.Start(ctx, "authz.getCachedPermissions")
ctx, span := s.tracer.Start(ctx, "authz.getCachedPermissions")
defer span.End()
if !options.ReloadCache {
permissions, ok := s.cache.Get(key)
if ok {
span.SetAttributes(attribute.Int("num_permissions_cached", len(permissions.([]accesscontrol.Permission))))
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheHit).Inc()
return permissions.([]accesscontrol.Permission), nil
}
@@ -315,6 +322,7 @@ func (s *Service) getCachedPermissions(ctx context.Context, key string, getPermi
span.AddEvent("cache miss")
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheMiss).Inc()
permissions, err := getPermissionsFn(ctx)
span.SetAttributes(attribute.Int("num_permissions_fetched", len(permissions)))
if err != nil {
return nil, err
}
@@ -338,6 +346,7 @@ func (s *Service) getCachedTeamsPermissions(ctx context.Context, user identity.R
teamPermissions, ok := s.cache.Get(key)
if ok {
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheHit).Inc()
span.SetAttributes(attribute.Int("num_permissions_cached", len(teamPermissions.([]accesscontrol.Permission))))
permissions = append(permissions, teamPermissions.([]accesscontrol.Permission)...)
} else {
miss = append(miss, teamID)
@@ -349,6 +358,7 @@ func (s *Service) getCachedTeamsPermissions(ctx context.Context, user identity.R
span.AddEvent("cache miss")
metrics.MAccessPermissionsCacheUsage.WithLabelValues(accesscontrol.CacheMiss).Inc()
teamsPermissions, err := s.getTeamsPermissions(ctx, miss, orgID)
span.SetAttributes(attribute.Int("num_permissions_fetched", len(teamsPermissions)))
if err != nil {
return nil, err
}
@@ -369,10 +379,16 @@ func (s *Service) ClearUserPermissionCache(user identity.Requester) {
}
func (s *Service) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error {
ctx, span := s.tracer.Start(ctx, "authz.DeleteUserPermissions")
defer span.End()
return s.store.DeleteUserPermissions(ctx, orgID, userID)
}
func (s *Service) DeleteTeamPermissions(ctx context.Context, orgID int64, teamID int64) error {
ctx, span := s.tracer.Start(ctx, "authz.DeleteTeamPermissions")
defer span.End()
return s.store.DeleteTeamPermissions(ctx, orgID, teamID)
}
@@ -398,6 +414,9 @@ func (s *Service) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistrat
// RegisterFixedRoles registers all declared roles in RAM
func (s *Service) RegisterFixedRoles(ctx context.Context) error {
_, span := s.tracer.Start(ctx, "authz.RegisterFixedRoles")
defer span.End()
s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) {
if basicRole, ok := s.roles[br]; ok {
@@ -421,6 +440,9 @@ func (s *Service) RegisterFixedRoles(ctx context.Context) error {
// DeclarePluginRoles allow the caller to declare, to the service, plugin roles and their assignments
// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs []plugins.RoleRegistration) error {
ctx, span := s.tracer.Start(ctx, "authz.DeclarePluginRoles")
defer span.End()
// Protect behind feature toggle
if !s.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) {
return nil
@@ -455,6 +477,9 @@ func GetActionFilter(options accesscontrol.SearchOptions) func(action string) bo
// SearchUsersPermissions returns all users' permissions filtered by action prefixes
func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Requester, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
ctx, span := s.tracer.Start(ctx, "authz.SearchUsersPermissions")
defer span.End()
// Limit roles to available in OSS
options.RolePrefixes = OSSRolesPrefixes
if options.NamespacedID != "" {
@@ -566,6 +591,9 @@ func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Reque
}
func (s *Service) SearchUserPermissions(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) {
ctx, span := s.tracer.Start(ctx, "authz.SearchUserPermissions")
defer span.End()
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
defer timer.ObserveDuration()
@@ -573,13 +601,16 @@ func (s *Service) SearchUserPermissions(ctx context.Context, orgID int64, search
return nil, fmt.Errorf("expected namespaced ID to be specified")
}
if permissions, success := s.searchUserPermissionsFromCache(orgID, searchOptions); success {
if permissions, success := s.searchUserPermissionsFromCache(ctx, orgID, searchOptions); success {
return permissions, nil
}
return s.searchUserPermissions(ctx, orgID, searchOptions)
}
func (s *Service) searchUserPermissions(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) {
ctx, span := s.tracer.Start(ctx, "authz.searchUserPermissions")
defer span.End()
userID, err := searchOptions.ComputeUserID()
if err != nil {
return nil, err
@@ -629,7 +660,10 @@ func (s *Service) searchUserPermissions(ctx context.Context, orgID int64, search
return permissions, nil
}
func (s *Service) searchUserPermissionsFromCache(orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, bool) {
func (s *Service) searchUserPermissionsFromCache(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, bool) {
_, span := s.tracer.Start(ctx, "authz.searchUserPermissionsFromCache")
defer span.End()
userID, err := searchOptions.ComputeUserID()
if err != nil {
return nil, false
@@ -669,6 +703,9 @@ func PermissionMatchesSearchOptions(permission accesscontrol.Permission, searchO
}
func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol.SaveExternalServiceRoleCommand) error {
ctx, span := s.tracer.Start(ctx, "authz.SaveExternalServiceRole")
defer span.End()
if !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
s.log.Debug("Registering an external service role is behind a feature flag, enable it to use this feature.")
return nil
@@ -682,6 +719,9 @@ func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol
}
func (s *Service) DeleteExternalServiceRole(ctx context.Context, externalServiceID string) error {
ctx, span := s.tracer.Start(ctx, "authz.DeleteExternalServiceRole")
defer span.End()
if !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) {
s.log.Debug("Deleting an external service role is behind a feature flag, enable it to use this feature.")
return nil
@@ -697,6 +737,9 @@ func (*Service) SyncUserRoles(ctx context.Context, orgID int64, cmd accesscontro
}
func (s *Service) GetRoleByName(ctx context.Context, orgID int64, roleName string) (*accesscontrol.RoleDTO, error) {
_, span := s.tracer.Start(ctx, "authz.GetRoleByName")
defer span.End()
err := accesscontrol.ErrRoleNotFound
if _, ok := s.roles[roleName]; ok {
return nil, err
@@ -42,6 +42,7 @@ func setupTestEnv(t testing.TB) *Service {
registrations: accesscontrol.RegistrationList{},
roles: accesscontrol.BuildBasicRoleDefinitions(),
store: database.ProvideService(db.InitTestDB(t)),
tracer: tracing.InitializeTracerForTest(),
}
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
return ac
+1 -1
View File
@@ -62,7 +62,7 @@ func (api *AccessControlAPI) getUserPermissions(c *contextmodel.ReqContext) resp
return response.JSON(http.StatusInternalServerError, err)
}
return response.JSON(http.StatusOK, ac.GroupScopesByAction(permissions))
return response.JSON(http.StatusOK, ac.GroupScopesByActionContext(c.Req.Context(), permissions))
}
// GET /api/access-control/users/permissions/search
+67 -15
View File
@@ -3,7 +3,6 @@ package migrator
import (
"context"
"fmt"
"strconv"
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
@@ -30,9 +29,14 @@ type ZanzanaSynchroniser struct {
func NewZanzanaSynchroniser(client zanzana.Client, store db.DB, collectors ...TupleCollector) *ZanzanaSynchroniser {
// Append shared collectors that is used by both enterprise and oss
collectors = append(collectors, managedPermissionsCollector(store))
collectors = append(
collectors,
teamMembershipCollector(store),
managedPermissionsCollector(store),
)
return &ZanzanaSynchroniser{
client: client,
log: log.New("zanzana.sync"),
collectors: collectors,
}
@@ -75,21 +79,24 @@ func managedPermissionsCollector(store db.DB) TupleCollector {
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
const collectorID = "managed"
const query = `
SELECT ur.user_id, p.action, p.kind, p.identifier, r.org_id FROM permission p
INNER JOIN role r on p.role_id = r.id
LEFT JOIN user_role ur on r.id = ur.role_id
LEFT JOIN team_role tr on r.id = tr.role_id
LEFT JOIN builtin_role br on r.id = br.role_id
WHERE r.name LIKE 'managed:%'
`
SELECT u.uid as user_uid, t.uid as team_uid, p.action, p.kind, p.identifier, r.org_id
FROM permission p
INNER JOIN role r ON p.role_id = r.id
LEFT JOIN user_role ur ON r.id = ur.role_id
LEFT JOIN user u ON u.id = ur.user_id
LEFT JOIN team_role tr ON r.id = tr.role_id
LEFT JOIN team t ON tr.team_id = t.id
LEFT JOIN builtin_role br ON r.id = br.role_id
WHERE r.name LIKE 'managed:%'
`
type Permission struct {
RoleName string `xorm:"role_name"`
OrgID int64 `xorm:"org_id"`
Action string `xorm:"action"`
Kind string
Identifier string
UserID int64 `xorm:"user_id"`
TeamID int64 `xorm:"user_id"`
UserUID string `xorm:"user_uid"`
TeamUID string `xorm:"team_uid"`
}
var permissions []Permission
@@ -103,10 +110,10 @@ func managedPermissionsCollector(store db.DB) TupleCollector {
for _, p := range permissions {
var subject string
if p.UserID > 0 {
subject = zanzana.NewObject(zanzana.TypeUser, strconv.FormatInt(p.UserID, 10))
} else if p.TeamID > 0 {
subject = zanzana.NewObject(zanzana.TypeTeam, strconv.FormatInt(p.TeamID, 10))
if len(p.UserUID) > 0 {
subject = zanzana.NewObject(zanzana.TypeUser, p.UserUID)
} else if len(p.TeamUID) > 0 {
subject = zanzana.NewObject(zanzana.TypeTeam, p.TeamUID)
} else {
// FIXME(kalleep): Unsuported role binding (org role). We need to have basic roles in place
continue
@@ -126,3 +133,48 @@ func managedPermissionsCollector(store db.DB) TupleCollector {
return nil
}
}
func teamMembershipCollector(store db.DB) TupleCollector {
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
const collectorID = "team_membership"
const query = `
SELECT t.uid as team_uid, u.uid as user_uid, tm.permission
FROM team_member tm
INNER JOIN team t ON tm.team_id = t.id
INNER JOIN user u ON tm.user_id = u.id
`
type membership struct {
TeamUID string `xorm:"team_uid"`
UserUID string `xorm:"user_uid"`
Permission int
}
var memberships []membership
err := store.WithDbSession(ctx, func(sess *db.Session) error {
return sess.SQL(query).Find(&memberships)
})
if err != nil {
return err
}
for _, m := range memberships {
tuple := &openfgav1.TupleKey{
User: zanzana.NewObject(zanzana.TypeUser, m.UserUID),
Object: zanzana.NewObject(zanzana.TypeTeam, m.TeamUID),
}
// Admin permission is 4 and member 0
if m.Permission == 4 {
tuple.Relation = zanzana.RelationTeamAdmin
} else {
tuple.Relation = zanzana.RelationTeamMember
}
tuples[collectorID] = append(tuples[collectorID], tuple)
}
return nil
}
}
+1 -1
View File
@@ -120,7 +120,7 @@ func (m *Mock) Evaluate(ctx context.Context, usr identity.Requester, evaluator a
if err != nil {
return false, err
}
permissions = accesscontrol.GroupScopesByAction(userPermissions)
permissions = accesscontrol.GroupScopesByActionContext(ctx, userPermissions)
}
if evaluator.Evaluate(permissions) {
@@ -111,7 +111,7 @@ func TestApi_getDescription(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
service, _, _ := setupTestEnvironment(t, tt.options)
server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service)
server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}}, service)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/access-control/%s/description", tt.options.Resource), nil)
require.NoError(t, err)
@@ -158,7 +158,7 @@ func TestApi_getPermissions(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
service, usrSvc, teamSvc := setupTestEnvironment(t, testOptions)
server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service)
server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}}, service)
seedPermissions(t, tt.resourceID, usrSvc, teamSvc, service)
@@ -235,7 +235,7 @@ func TestApi_setBuiltinRolePermission(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
service, _, _ := setupTestEnvironment(t, testOptions)
server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service)
server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}}, service)
recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "builtInRoles", tt.builtInRole)
assert.Equal(t, tt.expectedStatus, recorder.Code)
@@ -313,7 +313,7 @@ func TestApi_setTeamPermission(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
service, _, teamSvc := setupTestEnvironment(t, testOptions)
server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, service)
server := setupTestServer(t, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}}, service)
// seed team
_, err := teamSvc.CreateTeam(context.Background(), "test", "test@test.com", 1)
@@ -398,7 +398,7 @@ func TestApi_setUserPermission(t *testing.T) {
service, usrSvc, _ := setupTestEnvironment(t, testOptions)
server := setupTestServer(t, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)},
Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)},
}, service)
_, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "test", OrgID: 1})
@@ -195,7 +195,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
usr := &user.SignedInUser{
UserID: 1,
OrgID: orgID,
Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)},
Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), permissions)},
}
var role *accesscontrol.Role
@@ -86,7 +86,7 @@ func TestResourceToEntity(t *testing.T) {
},
},
},
expectedKey: "/playlist.grafana.app/playlists/namespaces/default/test-name",
expectedKey: "/group/playlist.grafana.app/resource/playlists/namespace/default/name/test-name",
expectedGroupVersion: "v0alpha1",
expectedName: "test-name",
expectedNamespace: "default",
@@ -157,7 +157,7 @@ func TestEntityToResource(t *testing.T) {
}{
{
entity: &entityStore.Entity{
Key: "/playlist.grafana.app/playlists/namespaces/default/test-uid",
Key: "/group/playlist.grafana.app/resource/playlists/namespaces/default/name/test-uid",
GroupVersion: "v0alpha1",
Name: "test-uid",
Title: "A playlist",
+6 -4
View File
@@ -3,6 +3,7 @@ package authnimpl
import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
@@ -36,6 +37,7 @@ func ProvideRegistration(
features *featuremgmt.FeatureManager, oauthTokenService oauthtoken.OAuthTokenService,
socialService social.Service, cache *remotecache.RemoteCache,
ldapService service.LDAP, settingsProviderService setting.Provider,
tracer tracing.Tracer,
) Registration {
logger := log.New("authn.registration")
@@ -95,16 +97,16 @@ func ProvideRegistration(
}
// FIXME (jguer): move to User package
userSync := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService)
orgSync := sync.ProvideOrgSync(userService, orgService, accessControlService, cfg)
userSync := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService, tracer)
orgSync := sync.ProvideOrgSync(userService, orgService, accessControlService, cfg, tracer)
authnSvc.RegisterPostAuthHook(userSync.SyncUserHook, 10)
authnSvc.RegisterPostAuthHook(userSync.EnableUserHook, 20)
authnSvc.RegisterPostAuthHook(orgSync.SyncOrgRolesHook, 30)
authnSvc.RegisterPostAuthHook(userSync.SyncLastSeenHook, 130)
authnSvc.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService).SyncOauthTokenHook, 60)
authnSvc.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService, tracer).SyncOauthTokenHook, 60)
authnSvc.RegisterPostAuthHook(userSync.FetchSyncedUserHook, 100)
rbacSync := sync.ProvideRBACSync(accessControlService)
rbacSync := sync.ProvideRBACSync(accessControlService, tracer)
if features.IsEnabledGlobally(featuremgmt.FlagCloudRBACRoles) {
authnSvc.RegisterPostAuthHook(rbacSync.SyncCloudRoles, 110)
authnSvc.RegisterPreLogoutHook(gcomsso.ProvideGComSSOService(cfg).LogoutHook, 50)
+9
View File
@@ -323,6 +323,9 @@ Default:
}
func (s *Service) ResolveIdentity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) {
ctx, span := s.tracer.Start(ctx, "authn.ResolveIdentity")
defer span.End()
r := &authn.Request{}
r.OrgID = orgID
// hack to not update last seen
@@ -358,6 +361,9 @@ func (s *Service) IsClientEnabled(name string) bool {
}
func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) error {
ctx, span := s.tracer.Start(ctx, "authn.SyncIdentity")
defer span.End()
r := &authn.Request{OrgID: identity.OrgID}
// hack to not update last seen on external syncs
r.SetMeta(authn.MetaKeyIsLogin, "true")
@@ -365,6 +371,9 @@ func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) er
}
func (s *Service) resolveIdenity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) {
ctx, span := s.tracer.Start(ctx, "authn.resolveIdentity")
defer span.End()
if namespaceID.IsNamespace(authn.NamespaceUser) {
return &authn.Identity{
OrgID: orgID,
@@ -9,19 +9,21 @@ import (
"golang.org/x/sync/singleflight"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/oauthtoken"
)
func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService auth.UserTokenService, socialService social.Service) *OAuthTokenSync {
func ProvideOAuthTokenSync(service oauthtoken.OAuthTokenService, sessionService auth.UserTokenService, socialService social.Service, tracer tracing.Tracer) *OAuthTokenSync {
return &OAuthTokenSync{
log.New("oauth_token.sync"),
service,
sessionService,
socialService,
new(singleflight.Group),
tracer,
}
}
@@ -31,9 +33,13 @@ type OAuthTokenSync struct {
sessionService auth.UserTokenService
socialService social.Service
singleflightGroup *singleflight.Group
tracer tracing.Tracer
}
func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
ctx, span := s.tracer.Start(ctx, "oauth.sync.SyncOauthTokenHook")
defer span.End()
// only perform oauth token check if identity is a user
if !identity.ID.IsNamespace(authn.NamespaceUser) {
return nil
@@ -10,6 +10,7 @@ import (
"golang.org/x/sync/singleflight"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/social/socialtest"
"github.com/grafana/grafana/pkg/services/auth"
@@ -128,6 +129,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) {
sessionService: sessionService,
socialService: socialService,
singleflightGroup: new(singleflight.Group),
tracer: tracing.InitializeTracerForTest(),
}
err := sync.SyncOauthTokenHook(context.Background(), tt.identity, nil)
+14 -4
View File
@@ -7,6 +7,7 @@ import (
"sort"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/org"
@@ -14,8 +15,8 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
func ProvideOrgSync(userService user.Service, orgService org.Service, accessControl accesscontrol.Service, cfg *setting.Cfg) *OrgSync {
return &OrgSync{userService, orgService, accessControl, cfg, log.New("org.sync")}
func ProvideOrgSync(userService user.Service, orgService org.Service, accessControl accesscontrol.Service, cfg *setting.Cfg, tracer tracing.Tracer) *OrgSync {
return &OrgSync{userService, orgService, accessControl, cfg, log.New("org.sync"), tracer}
}
type OrgSync struct {
@@ -23,11 +24,14 @@ type OrgSync struct {
orgService org.Service
accessControl accesscontrol.Service
cfg *setting.Cfg
log log.Logger
log log.Logger
tracer tracing.Tracer
}
func (s *OrgSync) SyncOrgRolesHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error {
ctx, span := s.tracer.Start(ctx, "org.sync.SyncOrgRolesHook")
defer span.End()
if !id.ClientParams.SyncOrgRoles {
return nil
}
@@ -131,6 +135,9 @@ func (s *OrgSync) SyncOrgRolesHook(ctx context.Context, id *authn.Identity, _ *a
}
func (s *OrgSync) SetDefaultOrgHook(ctx context.Context, currentIdentity *authn.Identity, r *authn.Request, err error) {
ctx, span := s.tracer.Start(ctx, "org.sync.SetDefaultOrgHook")
defer span.End()
if s.cfg.LoginDefaultOrgId < 1 || currentIdentity == nil || err != nil {
return
}
@@ -166,6 +173,9 @@ func (s *OrgSync) SetDefaultOrgHook(ctx context.Context, currentIdentity *authn.
}
func (s *OrgSync) validateUsingOrg(ctx context.Context, userID int64, orgID int64) (bool, error) {
ctx, span := s.tracer.Start(ctx, "org.sync.validateUsingOrg")
defer span.End()
query := org.GetUserOrgListQuery{UserID: userID}
result, err := s.orgService.GetUserOrgList(ctx, &query)
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/authn"
@@ -116,6 +117,7 @@ func TestOrgSync_SyncOrgRolesHook(t *testing.T) {
orgService: tt.fields.orgService,
accessControl: tt.fields.accessControl,
log: tt.fields.log,
tracer: tracing.InitializeTracerForTest(),
}
if err := s.SyncOrgRolesHook(tt.args.ctx, tt.args.id, nil); (err != nil) != tt.wantErr {
t.Errorf("OrgSync.SyncOrgRolesHook() error = %v, wantErr %v", err, tt.wantErr)
@@ -214,6 +216,7 @@ func TestOrgSync_SetDefaultOrgHook(t *testing.T) {
accessControl: actest.FakeService{},
log: log.NewNopLogger(),
cfg: cfg,
tracer: tracing.InitializeTracerForTest(),
}
s.SetDefaultOrgHook(context.Background(), tt.identity, nil, tt.inputErr)
+20 -7
View File
@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
@@ -17,19 +18,24 @@ var (
errSyncPermissionsForbidden = errutil.Forbidden("permissions.sync.forbidden")
)
func ProvideRBACSync(acService accesscontrol.Service) *RBACSync {
func ProvideRBACSync(acService accesscontrol.Service, tracer tracing.Tracer) *RBACSync {
return &RBACSync{
ac: acService,
log: log.New("permissions.sync"),
ac: acService,
log: log.New("permissions.sync"),
tracer: tracer,
}
}
type RBACSync struct {
ac accesscontrol.Service
log log.Logger
ac accesscontrol.Service
log log.Logger
tracer tracing.Tracer
}
func (s *RBACSync) SyncPermissionsHook(ctx context.Context, ident *authn.Identity, _ *authn.Request) error {
ctx, span := s.tracer.Start(ctx, "rbac.sync.SyncPermissionsHook")
defer span.End()
if !ident.ClientParams.SyncPermissions {
return nil
}
@@ -43,7 +49,8 @@ func (s *RBACSync) SyncPermissionsHook(ctx context.Context, ident *authn.Identit
if ident.Permissions == nil {
ident.Permissions = make(map[int64]map[string][]string, 1)
}
grouped := accesscontrol.GroupScopesByAction(permissions)
grouped := accesscontrol.GroupScopesByActionContext(ctx, permissions)
// Restrict access to the list of actions
actionsLookup := ident.ClientParams.FetchPermissionsParams.ActionsLookup
@@ -56,12 +63,15 @@ func (s *RBACSync) SyncPermissionsHook(ctx context.Context, ident *authn.Identit
}
grouped = filtered
}
ident.Permissions[ident.OrgID] = grouped
return nil
}
func (s *RBACSync) fetchPermissions(ctx context.Context, ident *authn.Identity) ([]accesscontrol.Permission, error) {
ctx, span := s.tracer.Start(ctx, "rbac.sync.fetchPermissions")
defer span.End()
permissions := make([]accesscontrol.Permission, 0, 8)
roles := ident.ClientParams.FetchPermissionsParams.Roles
if len(roles) > 0 {
@@ -94,6 +104,9 @@ var fixedCloudRoles = map[org.RoleType]string{
}
func (s *RBACSync) SyncCloudRoles(ctx context.Context, ident *authn.Identity, r *authn.Request) error {
ctx, span := s.tracer.Start(ctx, "rbac.sync.SyncCloudRoles")
defer span.End()
// we only want to run this hook during login and if the module used is grafana com
if r.GetMeta(authn.MetaKeyAuthModule) != login.GrafanaComAuthModule {
return nil
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/authn"
@@ -45,7 +46,7 @@ func TestRBACSync_SyncPermission(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 1, len(tt.identity.Permissions))
assert.Equal(t, accesscontrol.GroupScopesByAction(tt.expectedPermissions), tt.identity.Permissions[tt.identity.OrgID])
assert.Equal(t, accesscontrol.GroupScopesByActionContext(context.Background(), tt.expectedPermissions), tt.identity.Permissions[tt.identity.OrgID])
})
}
}
@@ -127,7 +128,8 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) {
return nil
},
},
log: log.NewNopLogger(),
log: log.NewNopLogger(),
tracer: tracing.InitializeTracerForTest(),
}
req := &authn.Request{}
@@ -149,8 +151,9 @@ func setupTestEnv() *RBACSync {
},
}
s := &RBACSync{
ac: acMock,
log: log.NewNopLogger(),
ac: acMock,
log: log.NewNopLogger(),
tracer: tracing.InitializeTracerForTest(),
}
return s
}
+30 -3
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
@@ -47,15 +48,14 @@ var (
errSignupNotAllowed = errors.New("system administrator has disabled signup")
)
func ProvideUserSync(userService user.Service,
userProtectionService login.UserProtectionService,
authInfoService login.AuthInfoService, quotaService quota.Service) *UserSync {
func ProvideUserSync(userService user.Service, userProtectionService login.UserProtectionService, authInfoService login.AuthInfoService, quotaService quota.Service, tracer tracing.Tracer) *UserSync {
return &UserSync{
userService: userService,
authInfoService: authInfoService,
userProtectionService: userProtectionService,
quotaService: quotaService,
log: log.New("user.sync"),
tracer: tracer,
}
}
@@ -65,10 +65,14 @@ type UserSync struct {
userProtectionService login.UserProtectionService
quotaService quota.Service
log log.Logger
tracer tracing.Tracer
}
// SyncUserHook syncs a user with the database
func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error {
ctx, span := s.tracer.Start(ctx, "user.sync.SyncUserHook")
defer span.End()
if !id.ClientParams.SyncUser {
return nil
}
@@ -106,6 +110,9 @@ func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *auth
}
func (s *UserSync) FetchSyncedUserHook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
ctx, span := s.tracer.Start(ctx, "user.sync.FetchSyncedUserHook")
defer span.End()
if !identity.ClientParams.FetchSyncedUser {
return nil
}
@@ -143,6 +150,9 @@ func (s *UserSync) FetchSyncedUserHook(ctx context.Context, identity *authn.Iden
}
func (s *UserSync) SyncLastSeenHook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
ctx, span := s.tracer.Start(ctx, "user.sync.SyncLastSeenHook")
defer span.End()
if r.GetMeta(authn.MetaKeyIsLogin) != "" {
// Do not sync last seen for login requests
return nil
@@ -177,6 +187,9 @@ func (s *UserSync) SyncLastSeenHook(ctx context.Context, identity *authn.Identit
}
func (s *UserSync) EnableUserHook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
ctx, span := s.tracer.Start(ctx, "user.sync.EnableUserHook")
defer span.End()
if !identity.ClientParams.EnableUser {
return nil
}
@@ -196,6 +209,9 @@ func (s *UserSync) EnableUserHook(ctx context.Context, identity *authn.Identity,
}
func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, identity *authn.Identity, createConnection bool) error {
ctx, span := s.tracer.Start(ctx, "user.sync.upsertAuthConnection")
defer span.End()
if identity.AuthenticatedBy == "" {
return nil
}
@@ -222,6 +238,9 @@ func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, ident
}
func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id *authn.Identity, userAuth *login.UserAuth) error {
ctx, span := s.tracer.Start(ctx, "user.sync.updateUserAttributes")
defer span.End()
if errProtection := s.userProtectionService.AllowUserMapping(usr, id.AuthenticatedBy); errProtection != nil {
return errUserProtection.Errorf("user mapping not allowed: %w", errProtection)
}
@@ -273,6 +292,8 @@ func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id
}
func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.User, error) {
ctx, span := s.tracer.Start(ctx, "user.sync.createUser")
defer span.End()
// FIXME(jguer): this should be done in the user service
// quota check: we can have quotas on both global and org level
// therefore we need to query check quota for both user and org services
@@ -312,6 +333,9 @@ func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.Us
}
func (s *UserSync) getUser(ctx context.Context, identity *authn.Identity) (*user.User, *login.UserAuth, error) {
ctx, span := s.tracer.Start(ctx, "user.sync.getUser")
defer span.End()
// Check auth info fist
if identity.AuthID != "" && identity.AuthenticatedBy != "" {
query := &login.GetAuthInfoQuery{AuthId: identity.AuthID, AuthModule: identity.AuthenticatedBy}
@@ -361,6 +385,9 @@ func (s *UserSync) getUser(ctx context.Context, identity *authn.Identity) (*user
}
func (s *UserSync) lookupByOneOf(ctx context.Context, params login.UserLookupParams) (*user.User, error) {
ctx, span := s.tracer.Start(ctx, "user.sync.lookupByOneOf")
defer span.End()
var usr *user.User
var err error
@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/authinfoimpl"
@@ -426,7 +427,7 @@ func TestUserSync_SyncUserHook(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := ProvideUserSync(tt.fields.userService, userProtection, tt.fields.authInfoService, tt.fields.quotaService)
s := ProvideUserSync(tt.fields.userService, userProtection, tt.fields.authInfoService, tt.fields.quotaService, tracing.InitializeTracerForTest())
err := s.SyncUserHook(tt.args.ctx, tt.args.id, nil)
if tt.wantErr {
require.Error(t, err)
@@ -462,7 +463,9 @@ func TestUserSync_FetchSyncedUserHook(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
s := UserSync{}
s := UserSync{
tracer: tracing.InitializeTracerForTest(),
}
err := s.FetchSyncedUserHook(context.Background(), tt.identity, tt.req)
require.ErrorIs(t, err, tt.expectedErr)
})
@@ -515,7 +518,7 @@ func TestUserSync_EnableDisabledUserHook(t *testing.T) {
return nil
}
s := UserSync{userService: userSvc}
s := UserSync{userService: userSvc, tracer: tracing.InitializeTracerForTest()}
err := s.EnableUserHook(context.Background(), tt.identity, nil)
require.NoError(t, err)
assert.Equal(t, tt.enableUser, called)
+6 -1
View File
@@ -12,6 +12,11 @@ const (
TypeTeam string = "team"
)
const (
RelationTeamMember string = "member"
RelationTeamAdmin string = "admin"
)
func NewObject(typ, id string) string {
return fmt.Sprintf("%s:%s", typ, id)
}
@@ -49,7 +54,7 @@ func TranslateToTuple(user string, action, kind, identifier string, orgID int64)
tuple.User = user
tuple.Relation = relation
// UID in grafana are not guarantee to be unique across orgs so we need to scope them.
// Some uid:s in grafana are not guarantee to be unique across orgs so we need to scope them.
if t.orgScoped {
tuple.Object = NewScopedObject(t.typ, identifier, strconv.FormatInt(orgID, 10))
} else {
+2 -1
View File
@@ -376,11 +376,12 @@ func (cma *CloudMigrationAPI) CreateSnapshot(c *contextmodel.ReqContext) respons
defer span.End()
uid := web.Params(c.Req)[":uid"]
if err := util.ValidateUID(uid); err != nil {
return response.ErrOrFallback(http.StatusBadRequest, "invalid session uid", err)
}
ss, err := cma.cloudMigrationService.CreateSnapshot(ctx, uid)
ss, err := cma.cloudMigrationService.CreateSnapshot(ctx, c.SignedInUser, uid)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "error creating snapshot", err)
}
@@ -1,7 +1,7 @@
[sample token] // NOT A REAL TOKEN
eyJUb2tlbiI6ImNvbXBsZXRlbHlfZmFrZV90b2tlbl9jZG9peTFhYzdwdXlwZCIsIkluc3RhbmNlIjp7IlN0YWNrSUQiOjEyMzQ1LCJTbHVnIjoic3R1Ymluc3RhbmNlIiwiUmVnaW9uU2x1ZyI6ImZha2UtcmVnaW9uIiwiQ2x1c3RlclNsdWciOiJmYWtlLWNsdXNlciJ9fQ==
[create session}
[create session]
curl -X POST -H "Content-Type: application/json" \
http://admin:admin@localhost:3000/api/cloudmigration/migration \
-d '{"AuthToken":"eyJUb2tlbiI6ImNvbXBsZXRlbHlfZmFrZV90b2tlbl9jZG9peTFhYzdwdXlwZCIsIkluc3RhbmNlIjp7IlN0YWNrSUQiOjEyMzQ1LCJTbHVnIjoic3R1Ymluc3RhbmNlIiwiUmVnaW9uU2x1ZyI6ImZha2UtcmVnaW9uIiwiQ2x1c3RlclNsdWciOiJmYWtlLWNsdXNlciJ9fQ=="}'
@@ -4,6 +4,7 @@ import (
"context"
"github.com/grafana/grafana/pkg/services/gcom"
"github.com/grafana/grafana/pkg/services/user"
)
type Service interface {
@@ -24,7 +25,7 @@ type Service interface {
GetMigrationStatus(ctx context.Context, runUID string) (*CloudMigrationSnapshot, error)
GetMigrationRunList(ctx context.Context, migUID string) (*CloudMigrationRunList, error)
CreateSnapshot(ctx context.Context, sessionUid string) (*CloudMigrationSnapshot, error)
CreateSnapshot(ctx context.Context, signedInUser *user.SignedInUser, sessionUid string) (*CloudMigrationSnapshot, error)
GetSnapshot(ctx context.Context, query GetSnapshotsQuery) (*CloudMigrationSnapshot, error)
GetSnapshotList(ctx context.Context, query ListSnapshotsQuery) ([]CloudMigrationSnapshot, error)
UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error
@@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"sync"
"time"
@@ -26,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/gcom"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/prometheus/client_golang/prometheus"
@@ -41,7 +41,6 @@ type Service struct {
cfg *setting.Cfg
buildSnapshotMutex sync.Mutex
buildSnapshotError bool
features featuremgmt.FeatureToggles
gmsClient gmsclient.Client
@@ -391,7 +390,7 @@ func (s *Service) RunMigration(ctx context.Context, uid string) (*cloudmigration
}
// Get migration data JSON
request, err := s.getMigrationDataJSON(ctx)
request, err := s.getMigrationDataJSON(ctx, &user.SignedInUser{})
if err != nil {
s.log.Error("error getting the json request body for migration run", "err", err.Error())
return nil, fmt.Errorf("migration data get error: %w", err)
@@ -459,8 +458,10 @@ func (s *Service) DeleteSession(ctx context.Context, uid string) (*cloudmigratio
return c, nil
}
func (s *Service) CreateSnapshot(ctx context.Context, sessionUid string) (*cloudmigration.CloudMigrationSnapshot, error) {
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.CreateSnapshot")
func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedInUser, sessionUid string) (*cloudmigration.CloudMigrationSnapshot, error) {
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.CreateSnapshot", trace.WithAttributes(
attribute.String("sessionUid", sessionUid),
))
defer span.End()
// fetch session for the gms auth token
@@ -470,28 +471,25 @@ func (s *Service) CreateSnapshot(ctx context.Context, sessionUid string) (*cloud
}
// query gms to establish new snapshot
initResp, err := s.gmsClient.InitializeSnapshot(ctx, *session)
timeoutCtx, cancel := context.WithTimeout(ctx, s.cfg.CloudMigration.StartSnapshotTimeout)
defer cancel()
initResp, err := s.gmsClient.StartSnapshot(timeoutCtx, *session)
if err != nil {
return nil, fmt.Errorf("initializing snapshot with GMS for session %s: %w", sessionUid, err)
}
// create new directory for snapshot writing
snapshotUid := util.GenerateShortUID()
dir := filepath.Join("cloudmigration.snapshots", fmt.Sprintf("snapshot-%s-%s", snapshotUid, initResp.GMSSnapshotUID))
err = os.MkdirAll(dir, 0750)
if err != nil {
return nil, fmt.Errorf("creating snapshot directory: %w", err)
if s.cfg.CloudMigration.SnapshotFolder == "" {
return nil, fmt.Errorf("snapshot folder is not set")
}
// save snapshot to the db
snapshot := cloudmigration.CloudMigrationSnapshot{
UID: snapshotUid,
UID: util.GenerateShortUID(),
SessionUID: sessionUid,
Status: cloudmigration.SnapshotStatusInitializing,
EncryptionKey: initResp.EncryptionKey,
UploadURL: initResp.UploadURL,
GMSSnapshotUID: initResp.GMSSnapshotUID,
LocalDir: dir,
GMSSnapshotUID: initResp.SnapshotID,
LocalDir: filepath.Join(s.cfg.CloudMigration.SnapshotFolder, "grafana", "snapshots", initResp.SnapshotID),
}
uid, err := s.store.CreateSnapshot(ctx, snapshot)
@@ -501,7 +499,11 @@ func (s *Service) CreateSnapshot(ctx context.Context, sessionUid string) (*cloud
snapshot.UID = uid
// start building the snapshot asynchronously while we return a success response to the client
go s.buildSnapshot(context.Background(), snapshot)
go func() {
if err := s.buildSnapshot(context.Background(), signedInUser, initResp.MaxItemsPerPartition, snapshot); err != nil {
s.log.Error("building snapshot", "err", err.Error())
}
}()
return &snapshot, nil
}
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/services/cloudmigration"
"github.com/grafana/grafana/pkg/services/gcom"
"github.com/grafana/grafana/pkg/services/user"
)
// NoopServiceImpl Define the Service Implementation.
@@ -60,7 +61,7 @@ func (s *NoopServiceImpl) RunMigration(context.Context, string) (*cloudmigration
return nil, cloudmigration.ErrFeatureDisabledError
}
func (s *NoopServiceImpl) CreateSnapshot(ctx context.Context, sessionUid string) (*cloudmigration.CloudMigrationSnapshot, error) {
func (s *NoopServiceImpl) CreateSnapshot(ctx context.Context, user *user.SignedInUser, sessionUid string) (*cloudmigration.CloudMigrationSnapshot, error) {
return nil, cloudmigration.ErrFeatureDisabledError
}
@@ -2,8 +2,11 @@ package cloudmigrationimpl
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
@@ -109,73 +112,6 @@ func Test_CreateGetRunMigrationsAndRuns(t *testing.T) {
require.NotNil(t, createResp.UID, delMigResp.UID)
}
func Test_ExecuteAsyncWorkflow(t *testing.T) {
s := setUpServiceTest(t, false)
createTokenResp, err := s.CreateToken(context.Background())
assert.NoError(t, err)
assert.NotEmpty(t, createTokenResp.Token)
cmd := cloudmigration.CloudMigrationSessionRequest{
AuthToken: createTokenResp.Token,
}
createResp, err := s.CreateSession(context.Background(), cmd)
require.NoError(t, err)
require.NotEmpty(t, createResp.UID)
require.NotEmpty(t, createResp.Slug)
getSessionResp, err := s.GetSession(context.Background(), createResp.UID)
require.NoError(t, err)
require.NotNil(t, getSessionResp)
require.Equal(t, createResp.UID, getSessionResp.UID)
require.Equal(t, createResp.Slug, getSessionResp.Slug)
listResp, err := s.GetSessionList(context.Background())
require.NoError(t, err)
require.NotNil(t, listResp)
require.Equal(t, 1, len(listResp.Sessions))
require.Equal(t, createResp.UID, listResp.Sessions[0].UID)
require.Equal(t, createResp.Slug, listResp.Sessions[0].Slug)
sessionUid := createResp.UID
snapshotResp, err := s.CreateSnapshot(ctxWithSignedInUser(), sessionUid)
require.NoError(t, err)
require.NotEmpty(t, snapshotResp.UID)
require.Equal(t, sessionUid, snapshotResp.SessionUID)
snapshotUid := snapshotResp.UID
// Service doesn't currently expose updating a snapshot externally, so we will just manually add a resource
err = (s.(*Service)).store.CreateUpdateSnapshotResources(context.Background(), snapshotUid, []cloudmigration.CloudMigrationResource{{Type: cloudmigration.DashboardDataType, RefID: "qwerty", Status: cloudmigration.ItemStatusOK}})
assert.NoError(t, err)
snapshot, err := s.GetSnapshot(ctxWithSignedInUser(), cloudmigration.GetSnapshotsQuery{
SnapshotUID: snapshotUid,
SessionUID: sessionUid,
ResultPage: 1,
ResultLimit: 100,
})
require.NoError(t, err)
assert.Equal(t, snapshotResp.UID, snapshot.UID)
assert.Equal(t, snapshotResp.EncryptionKey, snapshot.EncryptionKey)
assert.Len(t, snapshot.Resources, 1)
assert.Equal(t, "qwerty", snapshot.Resources[0].RefID)
snapshots, err := s.GetSnapshotList(ctxWithSignedInUser(), cloudmigration.ListSnapshotsQuery{SessionUID: sessionUid, Page: 1, Limit: 100})
require.NoError(t, err)
assert.Len(t, snapshots, 1)
assert.Equal(t, snapshotResp.UID, snapshots[0].UID)
assert.Equal(t, snapshotResp.EncryptionKey, snapshots[0].EncryptionKey)
assert.Empty(t, snapshots[0].Resources)
err = s.UploadSnapshot(ctxWithSignedInUser(), sessionUid, snapshotUid)
require.NoError(t, err)
assert.Panics(t, func() {
err = s.CancelSnapshot(ctxWithSignedInUser(), sessionUid, snapshotUid)
})
}
func ctxWithSignedInUser() context.Context {
c := &contextmodel.ReqContext{
SignedInUser: &user.SignedInUser{OrgID: 1},
@@ -202,6 +138,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi
require.NoError(t, err)
// dont know if this is the best, but dont want to refactor at the moment
cfg.CloudMigration.IsDeveloperMode = true
cfg.CloudMigration.SnapshotFolder = filepath.Join(os.TempDir(), uuid.NewString())
dashboardService := dashboards.NewFakeDashboardService(t)
if withDashboardMock {
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/services/cloudmigration"
"github.com/grafana/grafana/pkg/services/gcom"
"github.com/grafana/grafana/pkg/services/user"
)
var fixedDate = time.Date(2024, 6, 5, 17, 30, 40, 0, time.UTC)
@@ -129,7 +130,7 @@ func (m FakeServiceImpl) GetMigrationRunList(_ context.Context, _ string) (*clou
}, nil
}
func (m FakeServiceImpl) CreateSnapshot(ctx context.Context, sessionUid string) (*cloudmigration.CloudMigrationSnapshot, error) {
func (m FakeServiceImpl) CreateSnapshot(ctx context.Context, user *user.SignedInUser, sessionUid string) (*cloudmigration.CloudMigrationSnapshot, error) {
if m.ReturnError {
return nil, fmt.Errorf("mock error")
}
@@ -2,17 +2,24 @@ package cloudmigrationimpl
import (
"context"
cryptoRand "crypto/rand"
"fmt"
"time"
snapshot "github.com/grafana/grafana-cloud-migration-snapshot/src"
"github.com/grafana/grafana-cloud-migration-snapshot/src/contracts"
"github.com/grafana/grafana-cloud-migration-snapshot/src/infra/crypto"
"github.com/grafana/grafana/pkg/services/cloudmigration"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/cloudmigration/slicesext"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util/retryer"
"golang.org/x/crypto/nacl/box"
)
func (s *Service) getMigrationDataJSON(ctx context.Context) (*cloudmigration.MigrateDataRequest, error) {
func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) {
// Data sources
dataSources, err := s.getDataSources(ctx)
if err != nil {
@@ -28,7 +35,7 @@ func (s *Service) getMigrationDataJSON(ctx context.Context) (*cloudmigration.Mig
}
// Folders
folders, err := s.getFolders(ctx)
folders, err := s.getFolders(ctx, signedInUser)
if err != nil {
s.log.Error("Failed to get folders", "err", err)
return nil, err
@@ -111,10 +118,9 @@ func (s *Service) getDataSources(ctx context.Context) ([]datasources.AddDataSour
return result, err
}
func (s *Service) getFolders(ctx context.Context) ([]folder.Folder, error) {
reqCtx := contexthandler.FromContext(ctx)
func (s *Service) getFolders(ctx context.Context, signedInUser *user.SignedInUser) ([]folder.Folder, error) {
folders, err := s.folderService.GetFolders(ctx, folder.GetFoldersQuery{
SignedInUser: reqCtx.SignedInUser,
SignedInUser: signedInUser,
})
if err != nil {
return nil, err
@@ -143,11 +149,10 @@ func (s *Service) getDashboards(ctx context.Context) ([]dashboards.Dashboard, er
}
// asynchronous process for writing the snapshot to the filesystem and updating the snapshot status
func (s *Service) buildSnapshot(ctx context.Context, snapshotMeta cloudmigration.CloudMigrationSnapshot) {
func (s *Service) buildSnapshot(ctx context.Context, signedInUser *user.SignedInUser, maxItemsPerPartition uint32, snapshotMeta cloudmigration.CloudMigrationSnapshot) error {
// TODO -- make sure we can only build one snapshot at a time
s.buildSnapshotMutex.Lock()
defer s.buildSnapshotMutex.Unlock()
s.buildSnapshotError = false
// update snapshot status to creating, add some retries since this is a background task
if err := retryer.Retry(func() (retryer.RetrySignal, error) {
@@ -158,18 +163,60 @@ func (s *Service) buildSnapshot(ctx context.Context, snapshotMeta cloudmigration
return retryer.FuncComplete, err
}, 10, time.Millisecond*100, time.Second*10); err != nil {
s.log.Error("failed to set snapshot status to 'creating'", "err", err)
s.buildSnapshotError = true
return
return fmt.Errorf("setting snapshot status to creating: snapshotUID=%s %w", snapshotMeta.UID, err)
}
// build snapshot
// just sleep for now to simulate snapshot creation happening
// need to do a couple of fancy things when we implement this:
// - some sort of regular check-in so we know we haven't timed out
// - a channel to listen for cancel events
// - retries baked into the snapshot writing process?
s.log.Debug("snapshot meta", "snapshot", snapshotMeta)
time.Sleep(3 * time.Second)
publicKey, privateKey, err := box.GenerateKey(cryptoRand.Reader)
if err != nil {
return fmt.Errorf("nacl: generating public and private key: %w", err)
}
// Use GMS public key + the grafana generated private private key to encrypt snapshot files.
snapshotWriter, err := snapshot.NewSnapshotWriter(contracts.AssymetricKeys{
Public: []byte(snapshotMeta.EncryptionKey),
Private: privateKey[:],
},
crypto.NewNacl(),
snapshotMeta.LocalDir,
)
if err != nil {
return fmt.Errorf("instantiating snapshot writer: %w", err)
}
migrationData, err := s.getMigrationDataJSON(ctx, signedInUser)
if err != nil {
return fmt.Errorf("fetching migration data: %w", err)
}
resourcesGroupedByType := make(map[cloudmigration.MigrateDataType][]snapshot.MigrateDataRequestItemDTO, 0)
for _, item := range migrationData.Items {
resourcesGroupedByType[item.Type] = append(resourcesGroupedByType[item.Type], snapshot.MigrateDataRequestItemDTO{
Type: snapshot.MigrateDataType(item.Type),
RefID: item.RefID,
Name: item.Name,
Data: item.Data,
})
}
for _, resourceType := range []cloudmigration.MigrateDataType{
cloudmigration.DatasourceDataType,
cloudmigration.FolderDataType,
cloudmigration.DashboardDataType,
} {
for _, chunk := range slicesext.Chunks(int(maxItemsPerPartition), resourcesGroupedByType[resourceType]) {
if err := snapshotWriter.Write(string(resourceType), chunk); err != nil {
return fmt.Errorf("writing resources to snapshot writer: resourceType=%s %w", resourceType, err)
}
}
}
// Add the grafana generated public key to the index file so gms can use it to decrypt the snapshot files later.
// This works because the snapshot files are being encrypted with
// the grafana generated private key + the gms public key.
_, err = snapshotWriter.Finish(publicKey[:])
if err != nil {
return fmt.Errorf("finishing writing snapshot files and generating index file: %w", err)
}
// update snapshot status to pending upload with retry
if err := retryer.Retry(func() (retryer.RetrySignal, error) {
@@ -180,8 +227,10 @@ func (s *Service) buildSnapshot(ctx context.Context, snapshotMeta cloudmigration
return retryer.FuncComplete, err
}, 10, time.Millisecond*100, time.Second*10); err != nil {
s.log.Error("failed to set snapshot status to 'pending upload'", "err", err)
s.buildSnapshotError = true
return fmt.Errorf("setting snapshot status to pending upload: snapshotID=%s %w", snapshotMeta.UID, err)
}
return nil
}
// asynchronous process for and updating the snapshot status
@@ -189,7 +238,6 @@ func (s *Service) uploadSnapshot(ctx context.Context, snapshotMeta cloudmigratio
// TODO -- make sure we can only upload one snapshot at a time
s.buildSnapshotMutex.Lock()
defer s.buildSnapshotMutex.Unlock()
s.buildSnapshotError = false
// update snapshot status to uploading, add some retries since this is a background task
if err := retryer.Retry(func() (retryer.RetrySignal, error) {
@@ -200,7 +248,6 @@ func (s *Service) uploadSnapshot(ctx context.Context, snapshotMeta cloudmigratio
return retryer.FuncComplete, err
}, 10, time.Millisecond*100, time.Second*10); err != nil {
s.log.Error("failed to set snapshot status to 'creating'", "err", err)
s.buildSnapshotError = true
return
}
@@ -218,7 +265,6 @@ func (s *Service) uploadSnapshot(ctx context.Context, snapshotMeta cloudmigratio
return retryer.FuncComplete, err
}, 10, time.Millisecond*100, time.Second*10); err != nil {
s.log.Error("failed to set snapshot status to 'pending upload'", "err", err)
s.buildSnapshotError = true
}
// simulate the rest
@@ -9,7 +9,7 @@ import (
type Client interface {
ValidateKey(context.Context, cloudmigration.CloudMigrationSession) error
MigrateData(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.MigrateDataRequest) (*cloudmigration.MigrateDataResponse, error)
InitializeSnapshot(context.Context, cloudmigration.CloudMigrationSession) (*cloudmigration.InitializeSnapshotResponse, error)
StartSnapshot(context.Context, cloudmigration.CloudMigrationSession) (*cloudmigration.StartSnapshotResponse, error)
GetSnapshotStatus(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.CloudMigrationSnapshot) (*cloudmigration.CloudMigrationSnapshot, error)
}
@@ -111,8 +111,43 @@ func (c *gmsClientImpl) MigrateData(ctx context.Context, cm cloudmigration.Cloud
return &result, nil
}
func (c *gmsClientImpl) InitializeSnapshot(context.Context, cloudmigration.CloudMigrationSession) (*cloudmigration.InitializeSnapshotResponse, error) {
panic("not implemented")
func (c *gmsClientImpl) StartSnapshot(ctx context.Context, session cloudmigration.CloudMigrationSession) (*cloudmigration.StartSnapshotResponse, error) {
logger := c.log.FromContext(ctx)
path := fmt.Sprintf("https://cms-%s.%s/cloud-migrations/api/v1/start-snapshot", session.ClusterSlug, c.domain)
// Send the request to cms with the associated auth token
req, err := http.NewRequest(http.MethodPost, path, nil)
if err != nil {
c.log.Error("error creating http request to start snapshot", "err", err.Error())
return nil, fmt.Errorf("http request error: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %d:%s", session.StackID, session.AuthToken))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
c.log.Error("error sending http request to start snapshot", "err", err.Error())
return nil, fmt.Errorf("http request error: %w", err)
} else if resp.StatusCode >= 400 {
c.log.Error("received error response to start snapshot", "statusCode", resp.StatusCode)
return nil, fmt.Errorf("http request error: %w", err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
logger.Error("closing request body: %w", err)
}
}()
var result cloudmigration.StartSnapshotResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
logger.Error("unmarshalling response body: %w", err)
return nil, fmt.Errorf("unmarshalling start snapshot response: %w", err)
}
return &result, nil
}
func (c *gmsClientImpl) GetSnapshotStatus(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.CloudMigrationSnapshot) (*cloudmigration.CloudMigrationSnapshot, error) {
@@ -15,7 +15,7 @@ func NewInMemoryClient() Client {
}
type memoryClientImpl struct {
snapshot *cloudmigration.InitializeSnapshotResponse
snapshot *cloudmigration.StartSnapshotResponse
}
func (c *memoryClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.CloudMigrationSession) error {
@@ -48,11 +48,11 @@ func (c *memoryClientImpl) MigrateData(
return &result, nil
}
func (c *memoryClientImpl) InitializeSnapshot(context.Context, cloudmigration.CloudMigrationSession) (*cloudmigration.InitializeSnapshotResponse, error) {
c.snapshot = &cloudmigration.InitializeSnapshotResponse{
EncryptionKey: util.GenerateShortUID(),
GMSSnapshotUID: util.GenerateShortUID(),
UploadURL: "localhost:3000",
func (c *memoryClientImpl) StartSnapshot(context.Context, cloudmigration.CloudMigrationSession) (*cloudmigration.StartSnapshotResponse, error) {
c.snapshot = &cloudmigration.StartSnapshotResponse{
EncryptionKey: util.GenerateShortUID(),
SnapshotID: util.GenerateShortUID(),
UploadURL: "localhost:3000",
}
return c.snapshot, nil
+8 -4
View File
@@ -195,8 +195,12 @@ type CreateSessionResponse struct {
SnapshotUid string
}
type InitializeSnapshotResponse struct {
EncryptionKey string
UploadURL string
GMSSnapshotUID string
type StartSnapshotResponse struct {
SnapshotID string `json:"snapshotID"`
MaxItemsPerPartition uint32 `json:"maxItemsPerPartition"`
Algo string `json:"algo"`
UploadURL string `json:"uploadURL"`
PresignedURLFormData map[string]string `json:"presignedURLFormData"`
EncryptionKey string `json:"encryptionKey"`
Nonce string `json:"nonce"`
}
@@ -0,0 +1,33 @@
package slicesext
import "math"
// Partitions the input into slices where the length is <= chunkSize.
//
// Example:
//
// Chunks(2, []int{1, 2, 3, 4})
// => [][]int{{1, 2}, {3, 4}}
func Chunks[T any](chunkSize int, xs []T) [][]T {
if chunkSize < 0 {
panic("chunk size must be greater than or equal to 0")
}
if chunkSize == 0 {
return [][]T{}
}
out := make([][]T, 0, int(math.Ceil(float64(len(xs))/float64(chunkSize))))
for i := 0; i < len(xs); i += chunkSize {
var chunk []T
if i+chunkSize < len(xs) {
chunk = xs[i : i+chunkSize]
} else {
chunk = xs[i:]
}
out = append(out, chunk)
}
return out
}
@@ -0,0 +1,80 @@
package slicesext
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestChunks(t *testing.T) {
t.Parallel()
t.Run("chunkSize must be greater than 0", func(t *testing.T) {
t.Parallel()
assert.PanicsWithValue(t, "chunk size must be greater than or equal to 0", func() {
Chunks(-1, []string{})
})
})
t.Run("basic", func(t *testing.T) {
t.Parallel()
cases := []struct {
description string
chunkSize int
input []int
expected [][]int
}{
{
description: "empty slice",
chunkSize: 2,
input: []int{},
expected: [][]int{},
},
{
description: "nil slice",
chunkSize: 2,
input: nil,
expected: [][]int{},
},
{
description: "chunk size is 0",
chunkSize: 0,
input: []int{1, 2, 3},
expected: [][]int{},
},
{
description: "chunk size is greater than slice length",
chunkSize: 3,
input: []int{1},
expected: [][]int{{1}},
},
{
description: "chunk size is 1",
chunkSize: 1,
input: []int{1, 2, 3},
expected: [][]int{{1}, {2}, {3}},
},
{
description: "chunk size is 2 and slice length is 3",
chunkSize: 2,
input: []int{1, 2, 3},
expected: [][]int{{1, 2}, {3}},
},
{
description: "chunk size is 2 and slice length is 6",
chunkSize: 2,
input: []int{1, 2, 3, 4, 5, 6},
expected: [][]int{{1, 2}, {3, 4}, {5, 6}},
},
}
for _, tt := range cases {
t.Run(tt.description, func(t *testing.T) {
result := Chunks(tt.chunkSize, tt.input)
assert.Equal(t, tt.expected, result)
})
}
})
}
@@ -288,7 +288,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) {
UserID: u.ID,
OrgID: u.OrgID,
OrgRole: org.RoleAdmin,
Permissions: map[int64]map[string][]string{u.OrgID: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
Permissions: map[int64]map[string][]string{u.OrgID: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
{
Action: dashboards.ActionFoldersCreate,
}, {
+44 -30
View File
@@ -1,24 +1,24 @@
#name,created,deleted,hash,author
newNavigation,2022-01-26T17:44:20Z,2022-06-16T09:48:38Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
trimDefaults,2022-01-26T17:44:20Z,2023-11-02T15:35:14Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
dashboardPreviews,2022-01-26T17:44:20Z,2023-04-13T17:42:24Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
live-config,2022-01-26T17:44:20Z,2023-02-03T21:21:48Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
accesscontrol,2022-01-26T17:44:20Z,2022-05-16T10:45:41Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
tempoServiceGraph,2022-01-26T17:44:20Z,2022-07-19T07:00:58Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
httpclientprovider_azure_auth,2022-01-26T17:44:20Z,2022-05-30T15:43:32Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
tempoBackendSearch,2022-01-26T17:44:20Z,2022-06-01T17:32:10Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
showFeatureFlagsInUI,2022-01-26T17:44:20Z,2023-02-09T00:01:34Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
queryOverLive,2022-01-26T17:44:20Z,,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
validatedQueries,2022-01-26T17:44:20Z,2022-05-16T21:17:05Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
fullRangeLogsVolume,2022-01-26T17:44:20Z,2022-02-15T08:05:03Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
prometheus_azure_auth,2022-01-26T17:44:20Z,2022-08-11T14:12:57Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
live-service-web-worker,2022-01-26T17:44:20Z,,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
envelopeEncryption,2022-01-26T17:44:20Z,2022-05-24T08:34:47Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
dashboardPreviews,2022-01-26T17:44:20Z,2023-04-13T17:42:24Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
database_metrics,2022-01-26T17:44:20Z,2023-04-28T13:19:06Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
live-pipeline,2022-01-26T17:44:20Z,2023-03-22T18:09:44Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
service-accounts,2022-01-26T17:44:20Z,2022-04-21T09:41:37Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
tempoSearch,2022-01-26T17:44:20Z,2022-06-01T17:32:10Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
disable_http_request_histogram,2022-01-26T17:44:20Z,2022-06-01T12:33:59Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
envelopeEncryption,2022-01-26T17:44:20Z,2022-05-24T08:34:47Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
fullRangeLogsVolume,2022-01-26T17:44:20Z,2022-02-15T08:05:03Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
httpclientprovider_azure_auth,2022-01-26T17:44:20Z,2022-05-30T15:43:32Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
live-config,2022-01-26T17:44:20Z,2023-02-03T21:21:48Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
live-pipeline,2022-01-26T17:44:20Z,2023-03-22T18:09:44Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
live-service-web-worker,2022-01-26T17:44:20Z,,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
newNavigation,2022-01-26T17:44:20Z,2022-06-16T09:48:38Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
prometheus_azure_auth,2022-01-26T17:44:20Z,2022-08-11T14:12:57Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
queryOverLive,2022-01-26T17:44:20Z,,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
service-accounts,2022-01-26T17:44:20Z,2022-04-21T09:41:37Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
showFeatureFlagsInUI,2022-01-26T17:44:20Z,2023-02-09T00:01:34Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
tempoBackendSearch,2022-01-26T17:44:20Z,2022-06-01T17:32:10Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
tempoSearch,2022-01-26T17:44:20Z,2022-06-01T17:32:10Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
tempoServiceGraph,2022-01-26T17:44:20Z,2022-07-19T07:00:58Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
trimDefaults,2022-01-26T17:44:20Z,2023-11-02T15:35:14Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
validatedQueries,2022-01-26T17:44:20Z,2022-05-16T21:17:05Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley
featureHighlights,2022-02-03T11:53:23Z,,a79c048344bddff7a868b040d9a08953917480f9,Alex Khomenko
lokiBackendMode,2022-02-07T07:43:48Z,2022-06-08T06:14:34Z,560c77390550e12e8c0be507c00f27cde0aa31e5,Gábor Farkas
swaggerUi,2022-02-08T12:38:43Z,2023-03-01T14:36:37Z,35fe58de374003bb4b077a878cc47ffc0a9d27b5,Sofia Papagiannaki
@@ -32,8 +32,8 @@ dashboardComments,2022-02-22T07:47:42Z,2023-03-11T12:28:12Z,28c30a34adbe94d0f08a
lokiLive,2022-03-01T22:46:52Z,2023-06-19T10:03:51Z,796bc27f75d52148d5b15cc8c4901276c344df2d,Ryan McKinley
fileStoreApi,2022-03-03T06:53:26Z,2022-03-11T18:08:19Z,a8b90d9a2524765c49923c48a7fcf0025c85b733,Artur Wierzbicki
azureMonitorResourcePickerForMetrics,2022-03-14T19:07:45Z,2023-01-30T16:19:03Z,275f33cf37bb4221ef120310a87c17d2e0ff5f35,Sarah Zinger
storageLocalUpload,2022-03-17T17:19:23Z,2022-07-18T17:44:42Z,1cfb9a4a1916d3eac473e5ccd0cb77ce281f044f,Ryan McKinley
storage,2022-03-17T17:19:23Z,,1cfb9a4a1916d3eac473e5ccd0cb77ce281f044f,Ryan McKinley
storageLocalUpload,2022-03-17T17:19:23Z,2022-07-18T17:44:42Z,1cfb9a4a1916d3eac473e5ccd0cb77ce281f044f,Ryan McKinley
saveDashboardDrawer,2022-03-30T17:07:41Z,2022-05-02T16:29:22Z,edf384c730a448a5e90b21bbbce49bc0e70e8197,Ryan McKinley
accesscontrol-builtins,2022-03-31T09:40:57Z,2022-05-19T07:29:36Z,0d87de153a2b8406d03efe0cf43b5a926ce7acab,Gabriel MABILLE
alertProvisioning,2022-04-01T06:32:00Z,2022-06-05T05:45:36Z,b8e277ee4c070b64ba4cf024b18c460626d1a2d3,Alexander Weaver
@@ -65,7 +65,7 @@ lokiDataframeApi,2022-06-13T06:33:46Z,2023-04-13T13:22:09Z,8fd9cb48548313cf43ebc
topnav,2022-06-20T14:25:43Z,,3c3293df78344a782fb65e5f977fd148daa597ce,Torkel Ödegaard
customBranding,2022-06-22T15:05:52Z,2022-10-05T12:07:35Z,405df77e3e6abcf5264ba192abe33630bd64da20,Tania
useLegacyHeatmapPanel,2022-06-23T18:48:28Z,2022-11-23T18:46:21Z,dd5a3b77472884035de13ddd441f41d0dd007e4b,Ryan McKinley
scenes,2022-07-07T06:53:02Z,,935334cbdabef8b0516bfe6c8ed45dd063cce7e9,Torkel Ödegaard
scenes,2022-07-07T06:53:02Z,2024-06-27T07:03:46Z,935334cbdabef8b0516bfe6c8ed45dd063cce7e9,Torkel Ödegaard
disableSecretsCompatibility,2022-07-12T20:27:37Z,,2d8a91a8461098f83aa512c867aff5af17b7893e,Guilherme Caulada
dashboardsFromStorage,2022-07-14T22:36:17Z,2023-03-20T16:36:49Z,da1701ce576ab26506d6256567b73a597043623a,Ryan McKinley
exploreMixedDatasource,2022-07-27T14:40:59Z,2022-07-27T15:17:31Z,e2258120e742b31ecde50e8de93544220a0762a3,Kristina
@@ -169,7 +169,7 @@ elasticToggleableFilters,2023-06-27T08:38:20Z,2023-07-28T12:49:02Z,c1ce24c90f75d
vizAndWidgetSplit,2023-06-27T10:22:13Z,,2785ed80d999b9ee1770e1209284d4fccd985401,Alexa V
nestedFolderPicker,2023-06-28T09:40:29Z,2024-06-04T09:16:12Z,f18a7f7d9696a6e80606dc49d5ac0064384f111d,Josh Hunt
frontendSandboxMonitorOnly,2023-07-05T11:48:25Z,,72f6793344fb3a63f5a8a86570b786b518264366,Esteban Beltran
prometheusIncrementalQueryInstrumentation,2023-07-05T19:39:49Z,,daf9f9cd199e0bbc110222a3f005dc05dab1681a,Galen Kistler
prometheusIncrementalQueryInstrumentation,2023-07-05T19:39:49Z,2024-06-20T13:04:22Z,daf9f9cd199e0bbc110222a3f005dc05dab1681a,Galen Kistler
dashboardEmbed,2023-07-06T14:43:20Z,2024-04-19T10:48:08Z,420b19e0e4bbdb97ae707cc1360bef7f79839d02,Alex Khomenko
awsDatasourcesTempCredentials,2023-07-06T15:06:11Z,,d33508453f6f1f7aab262b54c36ef471ed3eab87,Ida Štambuk
logsExploreTableVisualisation,2023-07-12T13:52:42Z,,7e4e743a42052183f549f7fa88cd0cc362844ad1,Sven Grossmann
@@ -222,7 +222,7 @@ libraryPanelRBAC,2023-10-11T23:30:50Z,,a12cb8cbf3a9b33841b2f2cb1522be11de78c86a,
awsDatasourcesNewFormStyling,2023-10-12T08:59:10Z,,2771fb940342aa152377b26b9554eb15082f90ac,Ida Štambuk
cachingOptimizeSerializationMemoryUsage,2023-10-12T16:56:49Z,,94ce87571ddfcede0fb7a229a65502b385d5bca3,Michael Mandrus
panelTitleSearchInV1,2023-10-13T12:04:24Z,,bf2f2540da7a4e4b8d80e1fa4ae3d05868cf7b69,Arati R
exploreContentOutline,2023-10-13T16:57:13Z,,4ec54bc2c39ba43843c693fdb2a4529b6a4703f2,Haris Rozajac
exploreContentOutline,2023-10-13T16:57:13Z,2024-06-24T15:45:42Z,4ec54bc2c39ba43843c693fdb2a4529b6a4703f2,Haris Rozajac
formatString,2023-10-13T18:17:12Z,,889576ac1d9278b1c6e3e278e8195968646a2db0,Sol
pluginsInstrumentationStatusSource,2023-10-17T08:27:45Z,2024-02-21T11:57:40Z,f5076d1868caa14ce44a70e812315541b4199d9f,Giuseppe Guerra
teamHttpHeaders,2023-10-17T10:23:54Z,,be5ba6813209b5b24e955e0f761032cb5826b578,Eric Leijonmarck
@@ -232,8 +232,8 @@ prometheusPromQAIL,2023-10-19T15:45:32Z,,5580d061019bee46ea2e69c94041f3da14585ce
cloudWatchBatchQueries,2023-10-20T19:09:41Z,,ecbc52f51529e1f35e26895db1a10f8a1c2f4244,Isabella Siu
alertingContactPointsV2,2023-10-25T13:57:53Z,2023-11-30T12:37:14Z,e12e40fc2493160338237b0b94e72fa530a78ef4,Gilles De Mey
alertmanagerRemoteOnly,2023-10-30T16:27:08Z,,363830883cb1f5de30f7015df5cba419df47468e,Santiago
alertmanagerRemoteSecondary,2023-10-30T16:27:08Z,,363830883cb1f5de30f7015df5cba419df47468e,Santiago
alertmanagerRemotePrimary,2023-10-30T16:27:08Z,,363830883cb1f5de30f7015df5cba419df47468e,Santiago
alertmanagerRemoteSecondary,2023-10-30T16:27:08Z,,363830883cb1f5de30f7015df5cba419df47468e,Santiago
annotationPermissionUpdate,2023-10-31T13:30:13Z,,c51c51458e4dd103aa0c099aa48b0d41f9375f86,Ieva
kubernetesPlaylistsAPI,2023-10-31T17:26:39Z,2023-11-08T19:14:05Z,dd773e74f120ba908cafd596a6875c2d7fa199bf,Ryan McKinley
traceToProfiles,2023-11-01T10:14:24Z,2024-01-22T14:21:14Z,c39e9a8f527b79881b95f64fd1c413b4bff42983,Joey
@@ -271,8 +271,8 @@ alertingQueryOptimization,2024-01-10T20:52:58Z,,afa33f12b2cf50d2c7e438f498d27b06
newFolderPicker,2024-01-15T11:43:19Z,,ec53487c995777b314f566f5a1054e3f8e29ec05,Ashley Harrison
kubernetesFeatureToggles,2024-01-18T05:32:44Z,,41e523bde7db5706f339d418c68d019039a8062e,Ryan McKinley
returnToPrevious,2024-01-18T17:12:14Z,2024-05-27T15:47:57Z,5800e40fba2accf96d81328f000e35bbb7c7acf1,Laura Fernández
jitterAlertRulesWithinGroups,2024-01-18T18:48:11Z,,00a260effab802edc8f72df50bfb6447aac343f0,Alexander Weaver
jitterAlertRules,2024-01-18T18:48:11Z,2024-02-09T21:53:58Z,00a260effab802edc8f72df50bfb6447aac343f0,Alexander Weaver
jitterAlertRulesWithinGroups,2024-01-18T18:48:11Z,,00a260effab802edc8f72df50bfb6447aac343f0,Alexander Weaver
onPremToCloudMigrations,2024-01-22T16:09:08Z,,cf13cb9f70c2230f17450667ce59440304fb023c,Michael Mandrus
alertingSaveStatePeriodic,2024-01-23T16:03:30Z,,aa25776f813926cb4f1947d4ae5a014f4e7728ff,Jean-Philippe Quéméner
promQLScope,2024-01-29T20:22:17Z,,43d0664340f3e3af219d7b5c747f486a073f5ce3,Kyle Brandt
@@ -285,17 +285,17 @@ newPDFRendering,2024-02-08T12:09:34Z,,28e66b4ad82ecebb551374325f0be4332412341c,A
autoMigrateGraphPanel,2024-02-08T22:00:48Z,,829672759c12b27f849c92c3a2aee4a6b0037920,Nathan Marrs
dashboardSceneSolo,2024-02-11T08:08:47Z,,fe6d1460b09b403fc74fd20e95f61513eede2555,Torkel Ödegaard
kubernetesAggregator,2024-02-12T20:59:35Z,,d6e6298103d5d6a4efd1c21a3d74f429b506f3a7,Todd Treece
autoMigrateStatPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs
autoMigrateWorldmapPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs
autoMigratePiechartPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs
autoMigrateStatPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs
autoMigrateTablePanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs
autoMigrateWorldmapPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs
groupByVariable,2024-02-14T17:18:04Z,,f016f95298fe490a865612864520f3622f8e804a,Dominik Prokop
alertingUpgradeDryrunOnStart,2024-02-16T16:29:54Z,2024-03-14T14:36:35Z,dfaf6d1e2e13b2bd11dc8f0cd4432bfcad819aa9,Matthew Jacobson
expressionParser,2024-02-17T00:59:11Z,,f23f50f58d7ab5cb1fd88b42b6c58ec09c1a159d,Ryan McKinley
sqlExpressions,2024-02-27T21:16:00Z,,70009201d44c2d0ab39cc77081808a69a6c4fd63,Scott Lepper
aiGeneratedDashboardChanges,2024-03-05T12:01:31Z,,a7c06d26f14b2a9fa8faa929a6c9a0c355018429,Ivan Ortega Alba
scopeFilters,2024-03-05T15:41:19Z,,b3efb4217e48656f24aacdffd5737595d7361afe,Carl Bergquist
betterPageScrolling,2024-03-06T15:06:47Z,,6a4e0c692ab26f4d4cb99ae615013e0b6e23f90b,Josh Hunt
betterPageScrolling,2024-03-06T15:06:47Z,2024-06-18T13:33:08Z,6a4e0c692ab26f4d4cb99ae615013e0b6e23f90b,Josh Hunt
emailVerificationEnforcement,2024-03-11T14:09:44Z,2024-03-22T13:30:58Z,0b55d72fb5698e1ea2cf73eaceae166cd5619daa,Karl Persson
ssoSettingsSAML,2024-03-14T11:04:45Z,,831ee9ee1696c0aa7a6e4ca022ddfc0ab28b86dc,linoman
publicDashboardsScene,2024-03-22T14:48:21Z,,8d4ca72f2a0e66c446d58d8bf13fadbc988fce11,Juan Cabanas
@@ -309,17 +309,17 @@ cloudWatchNewLabelParsing,2024-04-05T15:57:56Z,,58f32150c262605d188874b88f44a7de
exploreMetrics,2024-04-09T18:15:18Z,,66c0fd4dcc3202e11f41b302d27894dc162fb288,Darren Janeczek
accessActionSets,2024-04-12T16:19:25Z,,56f4664875047d6861ea3facbc94cd921e263950,Ieva
disableNumericMetricsSortingInExpressions,2024-04-16T14:52:47Z,,d3fee607e2818747ad02daf6b8c58cc801075076,Nick Richmond
queryServiceRewrite,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904,Ryan McKinley
queryServiceFromUI,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904,Ryan McKinley
queryService,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904,Ryan McKinley
queryServiceFromUI,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904,Ryan McKinley
queryServiceRewrite,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904,Ryan McKinley
grafanaManagedRecordingRules,2024-04-22T17:53:16Z,,c32953e52cf9fd46a462711bc23fde15a5c3b6bf,Alexander Weaver
logsExploreTableDefaultVisualization,2024-05-02T15:28:15Z,,840aeddbd1957117d9b62074c155e87bb4afd4b4,Galen Kistler
autofixDSUID,2024-05-03T11:32:07Z,,b6f899d953a0924760dc5fd5a3d40669c86d475d,Andres Martinez Gotor
autofixDSUID,2024-05-03T11:32:07Z,2024-06-20T10:56:39Z,b6f899d953a0924760dc5fd5a3d40669c86d475d,Andres Martinez Gotor
newDashboardSharingComponent,2024-05-03T15:02:18Z,,d1434fad3a68bc1d2b49725be31e45526e6ad349,Juan Cabanas
tlsMemcached,2024-05-09T19:12:08Z,,b009536329d110afd807ef2f27f2b7dcc7d310ba,lean.dev
notificationBanner,2024-05-13T09:32:34Z,,f3953b4955c218cc4678e842faa3d3380fd0f8f7,Alex Khomenko
dualWritePlaylistsMode3,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R
dualWritePlaylistsMode2,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R
dualWritePlaylistsMode3,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R
dashboardRestore,2024-05-16T17:36:26Z,,42d75ac737d7ac001a6d53376e25512408a01db5,Ezequiel Victorero
datasourceProxyDisableRBAC,2024-05-21T13:05:16Z,,0072e4a92d896df343d9586522d6f7533773da78,Aaron Godin
alertingDisableSendAlertsExternal,2024-05-23T12:29:19Z,,8421919cb552b9e8dbd4ebba20bcc67bbc5e6b4f,Steve Simpson
@@ -330,3 +330,17 @@ alertingCentralAlertHistory,2024-05-29T15:01:38Z,,289ce6185574df99acea37b86c2e08
pluginProxyPreserveTrailingSlash,2024-06-05T11:36:14Z,,fe3e5917f1bc83ab29b6a57578316e054718aa4a,Marcus Efraimsson
kubernetesDashboards,2024-06-05T14:34:23Z,,41e0430f83bf7db50c4caaa1472afa3bf3d5c2dc,Ryan McKinley
azureMonitorPrometheusExemplars,2024-06-06T16:53:17Z,,c9778c3332aa93e5dd8bc3b894264e1955f0d593,Andreas Christou
pinNavItems,2024-06-10T11:40:03Z,,84b638fb26cecf856374bb3d09b123061b4b8a6b,Laura Fernández
authZGRPCServer,2024-06-13T09:41:35Z,,afcb5a855c26e985e43861bff6fab36b1b008109,Gabriel MABILLE
openSearchBackendFlowEnabled,2024-06-17T09:41:50Z,,ab2af9b8f75cd13595f4d487c1168e849768a518,Ida Štambuk
ssoSettingsLDAP,2024-06-18T11:31:27Z,,d074cc7892b96a1333bd07011baff146ea71e21d,Mihai Doarna
databaseReadReplica,2024-06-18T15:07:15Z,,50244ed4a1435cbf3e3c87d4af34fd7937f7c259,Kristin Laemmert
disableClassicHTTPHistogram,2024-06-18T19:37:44Z,,3bbc821131f1b10ace139dbb4a6880fb77686646,Dave Henderson
zanzana,2024-06-19T13:59:47Z,,3fe29809bec39239c45d672d686392725773f2e1,Karl Persson
failWrongDSUID,2024-06-20T10:56:39Z,,44fd13c742e606b8409e23eb62cab8bab24310f1,Andres Martinez Gotor
passScopeToDashboardApi,2024-06-20T15:49:19Z,,543e71eb2862187d12e8ee7742badb06e4c913e8,Bogdan Matei
alertingApiServer,2024-06-20T20:52:03Z,,b07592620279f16b0353444e7aba3c457c50d7ec,Yuri Tseretyan
dashboardRestoreUI,2024-06-25T14:43:13Z,,a3879e02bb3b7e8e917ba1bb4163bb230f917f2c,Laura Fernández
cloudWatchRoundUpEndTime,2024-06-27T15:10:28Z,,ba5b33227c343cb2c7dad15ff85a745869c68da9,Ida Štambuk
bodyScrolling,2024-07-01T10:28:39Z,,c0058f9c7e390d8a196f5b375382334287633ea9,Ashley Harrison
cloudwatchMetricInsightsCrossAccount,2024-07-02T10:34:12Z,,36ff0fe63a7710eb496f2f048feb71e6eb6e3c56,Ida Štambuk
1 #name created deleted hash author
newNavigation 2022-01-26T17:44:20Z 2022-06-16T09:48:38Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
trimDefaults 2022-01-26T17:44:20Z 2023-11-02T15:35:14Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
dashboardPreviews 2022-01-26T17:44:20Z 2023-04-13T17:42:24Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
live-config 2022-01-26T17:44:20Z 2023-02-03T21:21:48Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
2 accesscontrol 2022-01-26T17:44:20Z 2022-05-16T10:45:41Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
3 tempoServiceGraph dashboardPreviews 2022-01-26T17:44:20Z 2022-07-19T07:00:58Z 2023-04-13T17:42:24Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
httpclientprovider_azure_auth 2022-01-26T17:44:20Z 2022-05-30T15:43:32Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
tempoBackendSearch 2022-01-26T17:44:20Z 2022-06-01T17:32:10Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
showFeatureFlagsInUI 2022-01-26T17:44:20Z 2023-02-09T00:01:34Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
queryOverLive 2022-01-26T17:44:20Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
validatedQueries 2022-01-26T17:44:20Z 2022-05-16T21:17:05Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
fullRangeLogsVolume 2022-01-26T17:44:20Z 2022-02-15T08:05:03Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
prometheus_azure_auth 2022-01-26T17:44:20Z 2022-08-11T14:12:57Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
live-service-web-worker 2022-01-26T17:44:20Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
envelopeEncryption 2022-01-26T17:44:20Z 2022-05-24T08:34:47Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
4 database_metrics 2022-01-26T17:44:20Z 2023-04-28T13:19:06Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
live-pipeline 2022-01-26T17:44:20Z 2023-03-22T18:09:44Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
service-accounts 2022-01-26T17:44:20Z 2022-04-21T09:41:37Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
tempoSearch 2022-01-26T17:44:20Z 2022-06-01T17:32:10Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
5 disable_http_request_histogram 2022-01-26T17:44:20Z 2022-06-01T12:33:59Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
6 envelopeEncryption 2022-01-26T17:44:20Z 2022-05-24T08:34:47Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
7 fullRangeLogsVolume 2022-01-26T17:44:20Z 2022-02-15T08:05:03Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
8 httpclientprovider_azure_auth 2022-01-26T17:44:20Z 2022-05-30T15:43:32Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
9 live-config 2022-01-26T17:44:20Z 2023-02-03T21:21:48Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
10 live-pipeline 2022-01-26T17:44:20Z 2023-03-22T18:09:44Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
11 live-service-web-worker 2022-01-26T17:44:20Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
12 newNavigation 2022-01-26T17:44:20Z 2022-06-16T09:48:38Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
13 prometheus_azure_auth 2022-01-26T17:44:20Z 2022-08-11T14:12:57Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
14 queryOverLive 2022-01-26T17:44:20Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
15 service-accounts 2022-01-26T17:44:20Z 2022-04-21T09:41:37Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
16 showFeatureFlagsInUI 2022-01-26T17:44:20Z 2023-02-09T00:01:34Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
17 tempoBackendSearch 2022-01-26T17:44:20Z 2022-06-01T17:32:10Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
18 tempoSearch 2022-01-26T17:44:20Z 2022-06-01T17:32:10Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
19 tempoServiceGraph 2022-01-26T17:44:20Z 2022-07-19T07:00:58Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
20 trimDefaults 2022-01-26T17:44:20Z 2023-11-02T15:35:14Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
21 validatedQueries 2022-01-26T17:44:20Z 2022-05-16T21:17:05Z 5d66194ec5d8a7c174a075b6023dcbf58d17b861 Ryan McKinley
22 featureHighlights 2022-02-03T11:53:23Z a79c048344bddff7a868b040d9a08953917480f9 Alex Khomenko
23 lokiBackendMode 2022-02-07T07:43:48Z 2022-06-08T06:14:34Z 560c77390550e12e8c0be507c00f27cde0aa31e5 Gábor Farkas
24 swaggerUi 2022-02-08T12:38:43Z 2023-03-01T14:36:37Z 35fe58de374003bb4b077a878cc47ffc0a9d27b5 Sofia Papagiannaki
32 lokiLive 2022-03-01T22:46:52Z 2023-06-19T10:03:51Z 796bc27f75d52148d5b15cc8c4901276c344df2d Ryan McKinley
33 fileStoreApi 2022-03-03T06:53:26Z 2022-03-11T18:08:19Z a8b90d9a2524765c49923c48a7fcf0025c85b733 Artur Wierzbicki
34 azureMonitorResourcePickerForMetrics 2022-03-14T19:07:45Z 2023-01-30T16:19:03Z 275f33cf37bb4221ef120310a87c17d2e0ff5f35 Sarah Zinger
storageLocalUpload 2022-03-17T17:19:23Z 2022-07-18T17:44:42Z 1cfb9a4a1916d3eac473e5ccd0cb77ce281f044f Ryan McKinley
35 storage 2022-03-17T17:19:23Z 1cfb9a4a1916d3eac473e5ccd0cb77ce281f044f Ryan McKinley
36 storageLocalUpload 2022-03-17T17:19:23Z 2022-07-18T17:44:42Z 1cfb9a4a1916d3eac473e5ccd0cb77ce281f044f Ryan McKinley
37 saveDashboardDrawer 2022-03-30T17:07:41Z 2022-05-02T16:29:22Z edf384c730a448a5e90b21bbbce49bc0e70e8197 Ryan McKinley
38 accesscontrol-builtins 2022-03-31T09:40:57Z 2022-05-19T07:29:36Z 0d87de153a2b8406d03efe0cf43b5a926ce7acab Gabriel MABILLE
39 alertProvisioning 2022-04-01T06:32:00Z 2022-06-05T05:45:36Z b8e277ee4c070b64ba4cf024b18c460626d1a2d3 Alexander Weaver
65 topnav 2022-06-20T14:25:43Z 3c3293df78344a782fb65e5f977fd148daa597ce Torkel Ödegaard
66 customBranding 2022-06-22T15:05:52Z 2022-10-05T12:07:35Z 405df77e3e6abcf5264ba192abe33630bd64da20 Tania
67 useLegacyHeatmapPanel 2022-06-23T18:48:28Z 2022-11-23T18:46:21Z dd5a3b77472884035de13ddd441f41d0dd007e4b Ryan McKinley
68 scenes 2022-07-07T06:53:02Z 2024-06-27T07:03:46Z 935334cbdabef8b0516bfe6c8ed45dd063cce7e9 Torkel Ödegaard
69 disableSecretsCompatibility 2022-07-12T20:27:37Z 2d8a91a8461098f83aa512c867aff5af17b7893e Guilherme Caulada
70 dashboardsFromStorage 2022-07-14T22:36:17Z 2023-03-20T16:36:49Z da1701ce576ab26506d6256567b73a597043623a Ryan McKinley
71 exploreMixedDatasource 2022-07-27T14:40:59Z 2022-07-27T15:17:31Z e2258120e742b31ecde50e8de93544220a0762a3 Kristina
169 vizAndWidgetSplit 2023-06-27T10:22:13Z 2785ed80d999b9ee1770e1209284d4fccd985401 Alexa V
170 nestedFolderPicker 2023-06-28T09:40:29Z 2024-06-04T09:16:12Z f18a7f7d9696a6e80606dc49d5ac0064384f111d Josh Hunt
171 frontendSandboxMonitorOnly 2023-07-05T11:48:25Z 72f6793344fb3a63f5a8a86570b786b518264366 Esteban Beltran
172 prometheusIncrementalQueryInstrumentation 2023-07-05T19:39:49Z 2024-06-20T13:04:22Z daf9f9cd199e0bbc110222a3f005dc05dab1681a Galen Kistler
173 dashboardEmbed 2023-07-06T14:43:20Z 2024-04-19T10:48:08Z 420b19e0e4bbdb97ae707cc1360bef7f79839d02 Alex Khomenko
174 awsDatasourcesTempCredentials 2023-07-06T15:06:11Z d33508453f6f1f7aab262b54c36ef471ed3eab87 Ida Štambuk
175 logsExploreTableVisualisation 2023-07-12T13:52:42Z 7e4e743a42052183f549f7fa88cd0cc362844ad1 Sven Grossmann
222 awsDatasourcesNewFormStyling 2023-10-12T08:59:10Z 2771fb940342aa152377b26b9554eb15082f90ac Ida Štambuk
223 cachingOptimizeSerializationMemoryUsage 2023-10-12T16:56:49Z 94ce87571ddfcede0fb7a229a65502b385d5bca3 Michael Mandrus
224 panelTitleSearchInV1 2023-10-13T12:04:24Z bf2f2540da7a4e4b8d80e1fa4ae3d05868cf7b69 Arati R
225 exploreContentOutline 2023-10-13T16:57:13Z 2024-06-24T15:45:42Z 4ec54bc2c39ba43843c693fdb2a4529b6a4703f2 Haris Rozajac
226 formatString 2023-10-13T18:17:12Z 889576ac1d9278b1c6e3e278e8195968646a2db0 Sol
227 pluginsInstrumentationStatusSource 2023-10-17T08:27:45Z 2024-02-21T11:57:40Z f5076d1868caa14ce44a70e812315541b4199d9f Giuseppe Guerra
228 teamHttpHeaders 2023-10-17T10:23:54Z be5ba6813209b5b24e955e0f761032cb5826b578 Eric Leijonmarck
232 cloudWatchBatchQueries 2023-10-20T19:09:41Z ecbc52f51529e1f35e26895db1a10f8a1c2f4244 Isabella Siu
233 alertingContactPointsV2 2023-10-25T13:57:53Z 2023-11-30T12:37:14Z e12e40fc2493160338237b0b94e72fa530a78ef4 Gilles De Mey
234 alertmanagerRemoteOnly 2023-10-30T16:27:08Z 363830883cb1f5de30f7015df5cba419df47468e Santiago
alertmanagerRemoteSecondary 2023-10-30T16:27:08Z 363830883cb1f5de30f7015df5cba419df47468e Santiago
235 alertmanagerRemotePrimary 2023-10-30T16:27:08Z 363830883cb1f5de30f7015df5cba419df47468e Santiago
236 alertmanagerRemoteSecondary 2023-10-30T16:27:08Z 363830883cb1f5de30f7015df5cba419df47468e Santiago
237 annotationPermissionUpdate 2023-10-31T13:30:13Z c51c51458e4dd103aa0c099aa48b0d41f9375f86 Ieva
238 kubernetesPlaylistsAPI 2023-10-31T17:26:39Z 2023-11-08T19:14:05Z dd773e74f120ba908cafd596a6875c2d7fa199bf Ryan McKinley
239 traceToProfiles 2023-11-01T10:14:24Z 2024-01-22T14:21:14Z c39e9a8f527b79881b95f64fd1c413b4bff42983 Joey
271 newFolderPicker 2024-01-15T11:43:19Z ec53487c995777b314f566f5a1054e3f8e29ec05 Ashley Harrison
272 kubernetesFeatureToggles 2024-01-18T05:32:44Z 41e523bde7db5706f339d418c68d019039a8062e Ryan McKinley
273 returnToPrevious 2024-01-18T17:12:14Z 2024-05-27T15:47:57Z 5800e40fba2accf96d81328f000e35bbb7c7acf1 Laura Fernández
jitterAlertRulesWithinGroups 2024-01-18T18:48:11Z 00a260effab802edc8f72df50bfb6447aac343f0 Alexander Weaver
274 jitterAlertRules 2024-01-18T18:48:11Z 2024-02-09T21:53:58Z 00a260effab802edc8f72df50bfb6447aac343f0 Alexander Weaver
275 jitterAlertRulesWithinGroups 2024-01-18T18:48:11Z 00a260effab802edc8f72df50bfb6447aac343f0 Alexander Weaver
276 onPremToCloudMigrations 2024-01-22T16:09:08Z cf13cb9f70c2230f17450667ce59440304fb023c Michael Mandrus
277 alertingSaveStatePeriodic 2024-01-23T16:03:30Z aa25776f813926cb4f1947d4ae5a014f4e7728ff Jean-Philippe Quéméner
278 promQLScope 2024-01-29T20:22:17Z 43d0664340f3e3af219d7b5c747f486a073f5ce3 Kyle Brandt
285 autoMigrateGraphPanel 2024-02-08T22:00:48Z 829672759c12b27f849c92c3a2aee4a6b0037920 Nathan Marrs
286 dashboardSceneSolo 2024-02-11T08:08:47Z fe6d1460b09b403fc74fd20e95f61513eede2555 Torkel Ödegaard
287 kubernetesAggregator 2024-02-12T20:59:35Z d6e6298103d5d6a4efd1c21a3d74f429b506f3a7 Todd Treece
autoMigrateStatPanel 2024-02-14T16:06:25Z ce750e06187599da6b9c0a91ef95c7a62fe0d069 Nathan Marrs
autoMigrateWorldmapPanel 2024-02-14T16:06:25Z ce750e06187599da6b9c0a91ef95c7a62fe0d069 Nathan Marrs
288 autoMigratePiechartPanel 2024-02-14T16:06:25Z ce750e06187599da6b9c0a91ef95c7a62fe0d069 Nathan Marrs
289 autoMigrateStatPanel 2024-02-14T16:06:25Z ce750e06187599da6b9c0a91ef95c7a62fe0d069 Nathan Marrs
290 autoMigrateTablePanel 2024-02-14T16:06:25Z ce750e06187599da6b9c0a91ef95c7a62fe0d069 Nathan Marrs
291 autoMigrateWorldmapPanel 2024-02-14T16:06:25Z ce750e06187599da6b9c0a91ef95c7a62fe0d069 Nathan Marrs
292 groupByVariable 2024-02-14T17:18:04Z f016f95298fe490a865612864520f3622f8e804a Dominik Prokop
293 alertingUpgradeDryrunOnStart 2024-02-16T16:29:54Z 2024-03-14T14:36:35Z dfaf6d1e2e13b2bd11dc8f0cd4432bfcad819aa9 Matthew Jacobson
294 expressionParser 2024-02-17T00:59:11Z f23f50f58d7ab5cb1fd88b42b6c58ec09c1a159d Ryan McKinley
295 sqlExpressions 2024-02-27T21:16:00Z 70009201d44c2d0ab39cc77081808a69a6c4fd63 Scott Lepper
296 aiGeneratedDashboardChanges 2024-03-05T12:01:31Z a7c06d26f14b2a9fa8faa929a6c9a0c355018429 Ivan Ortega Alba
297 scopeFilters 2024-03-05T15:41:19Z b3efb4217e48656f24aacdffd5737595d7361afe Carl Bergquist
298 betterPageScrolling 2024-03-06T15:06:47Z 2024-06-18T13:33:08Z 6a4e0c692ab26f4d4cb99ae615013e0b6e23f90b Josh Hunt
299 emailVerificationEnforcement 2024-03-11T14:09:44Z 2024-03-22T13:30:58Z 0b55d72fb5698e1ea2cf73eaceae166cd5619daa Karl Persson
300 ssoSettingsSAML 2024-03-14T11:04:45Z 831ee9ee1696c0aa7a6e4ca022ddfc0ab28b86dc linoman
301 publicDashboardsScene 2024-03-22T14:48:21Z 8d4ca72f2a0e66c446d58d8bf13fadbc988fce11 Juan Cabanas
309 exploreMetrics 2024-04-09T18:15:18Z 66c0fd4dcc3202e11f41b302d27894dc162fb288 Darren Janeczek
310 accessActionSets 2024-04-12T16:19:25Z 56f4664875047d6861ea3facbc94cd921e263950 Ieva
311 disableNumericMetricsSortingInExpressions 2024-04-16T14:52:47Z d3fee607e2818747ad02daf6b8c58cc801075076 Nick Richmond
queryServiceRewrite 2024-04-19T09:26:21Z 5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904 Ryan McKinley
queryServiceFromUI 2024-04-19T09:26:21Z 5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904 Ryan McKinley
312 queryService 2024-04-19T09:26:21Z 5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904 Ryan McKinley
313 queryServiceFromUI 2024-04-19T09:26:21Z 5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904 Ryan McKinley
314 queryServiceRewrite 2024-04-19T09:26:21Z 5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904 Ryan McKinley
315 grafanaManagedRecordingRules 2024-04-22T17:53:16Z c32953e52cf9fd46a462711bc23fde15a5c3b6bf Alexander Weaver
316 logsExploreTableDefaultVisualization 2024-05-02T15:28:15Z 840aeddbd1957117d9b62074c155e87bb4afd4b4 Galen Kistler
317 autofixDSUID 2024-05-03T11:32:07Z 2024-06-20T10:56:39Z b6f899d953a0924760dc5fd5a3d40669c86d475d Andres Martinez Gotor
318 newDashboardSharingComponent 2024-05-03T15:02:18Z d1434fad3a68bc1d2b49725be31e45526e6ad349 Juan Cabanas
319 tlsMemcached 2024-05-09T19:12:08Z b009536329d110afd807ef2f27f2b7dcc7d310ba lean.dev
320 notificationBanner 2024-05-13T09:32:34Z f3953b4955c218cc4678e842faa3d3380fd0f8f7 Alex Khomenko
dualWritePlaylistsMode3 2024-05-14T12:11:56Z 2024-05-31T18:18:09Z 6836bfe1ea1bf62f4eb66dc1328a456183874f81 Arati R
321 dualWritePlaylistsMode2 2024-05-14T12:11:56Z 2024-05-31T18:18:09Z 6836bfe1ea1bf62f4eb66dc1328a456183874f81 Arati R
322 dualWritePlaylistsMode3 2024-05-14T12:11:56Z 2024-05-31T18:18:09Z 6836bfe1ea1bf62f4eb66dc1328a456183874f81 Arati R
323 dashboardRestore 2024-05-16T17:36:26Z 42d75ac737d7ac001a6d53376e25512408a01db5 Ezequiel Victorero
324 datasourceProxyDisableRBAC 2024-05-21T13:05:16Z 0072e4a92d896df343d9586522d6f7533773da78 Aaron Godin
325 alertingDisableSendAlertsExternal 2024-05-23T12:29:19Z 8421919cb552b9e8dbd4ebba20bcc67bbc5e6b4f Steve Simpson
330 pluginProxyPreserveTrailingSlash 2024-06-05T11:36:14Z fe3e5917f1bc83ab29b6a57578316e054718aa4a Marcus Efraimsson
331 kubernetesDashboards 2024-06-05T14:34:23Z 41e0430f83bf7db50c4caaa1472afa3bf3d5c2dc Ryan McKinley
332 azureMonitorPrometheusExemplars 2024-06-06T16:53:17Z c9778c3332aa93e5dd8bc3b894264e1955f0d593 Andreas Christou
333 pinNavItems 2024-06-10T11:40:03Z 84b638fb26cecf856374bb3d09b123061b4b8a6b Laura Fernández
334 authZGRPCServer 2024-06-13T09:41:35Z afcb5a855c26e985e43861bff6fab36b1b008109 Gabriel MABILLE
335 openSearchBackendFlowEnabled 2024-06-17T09:41:50Z ab2af9b8f75cd13595f4d487c1168e849768a518 Ida Štambuk
336 ssoSettingsLDAP 2024-06-18T11:31:27Z d074cc7892b96a1333bd07011baff146ea71e21d Mihai Doarna
337 databaseReadReplica 2024-06-18T15:07:15Z 50244ed4a1435cbf3e3c87d4af34fd7937f7c259 Kristin Laemmert
338 disableClassicHTTPHistogram 2024-06-18T19:37:44Z 3bbc821131f1b10ace139dbb4a6880fb77686646 Dave Henderson
339 zanzana 2024-06-19T13:59:47Z 3fe29809bec39239c45d672d686392725773f2e1 Karl Persson
340 failWrongDSUID 2024-06-20T10:56:39Z 44fd13c742e606b8409e23eb62cab8bab24310f1 Andres Martinez Gotor
341 passScopeToDashboardApi 2024-06-20T15:49:19Z 543e71eb2862187d12e8ee7742badb06e4c913e8 Bogdan Matei
342 alertingApiServer 2024-06-20T20:52:03Z b07592620279f16b0353444e7aba3c457c50d7ec Yuri Tseretyan
343 dashboardRestoreUI 2024-06-25T14:43:13Z a3879e02bb3b7e8e917ba1bb4163bb230f917f2c Laura Fernández
344 cloudWatchRoundUpEndTime 2024-06-27T15:10:28Z ba5b33227c343cb2c7dad15ff85a745869c68da9 Ida Štambuk
345 bodyScrolling 2024-07-01T10:28:39Z c0058f9c7e390d8a196f5b375382334287633ea9 Ashley Harrison
346 cloudwatchMetricInsightsCrossAccount 2024-07-02T10:34:12Z 36ff0fe63a7710eb496f2f048feb71e6eb6e3c56 Ida Štambuk
File diff suppressed because it is too large Load Diff
@@ -59,6 +59,7 @@ func TestFeatureToggleFiles(t *testing.T) {
AllowSelfServe: flag.AllowSelfServe,
HideFromAdminPage: flag.HideFromAdminPage,
HideFromDocs: flag.HideFromDocs,
Expression: flag.Expression,
// EnabledVersion: ???,
}
+1 -1
View File
@@ -126,7 +126,7 @@ func (a *authenticator) getSignedInUser(ctx context.Context, token string) (*use
if err != nil {
a.logger.Error("failed fetching permissions for user", "userID", signedInUser.UserID, "error", err)
}
signedInUser.Permissions[signedInUser.OrgID] = accesscontrol.GroupScopesByAction(permissions)
signedInUser.Permissions[signedInUser.OrgID] = accesscontrol.GroupScopesByActionContext(context.Background(), permissions)
}
return signedInUser, nil
+2 -1
View File
@@ -1,6 +1,7 @@
package api
import (
"context"
"encoding/json"
"errors"
"io"
@@ -663,5 +664,5 @@ search_base_dns = ["dc=grafana,dc=org"]`)
}
func userWithPermissions(orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
return &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}}
return &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), permissions)}}
}
+6
View File
@@ -140,6 +140,7 @@ func (root *NavTreeRoot) ApplyCostManagementIA() {
orgAdminNode := root.FindById(NavIDCfg)
var costManagementApp *NavLink
var adaptiveMetricsApp *NavLink
var adaptiveLogsApp *NavLink
var attributionsApp *NavLink
var logVolumeExplorerApp *NavLink
@@ -151,6 +152,8 @@ func (root *NavTreeRoot) ApplyCostManagementIA() {
costManagementApp = element
case "plugin-page-grafana-adaptive-metrics-app":
adaptiveMetricsApp = element
case "plugin-page-grafana-adaptivelogs-app":
adaptiveLogsApp = element
case "plugin-page-grafana-attributions-app":
attributionsApp = element
case "plugin-page-grafana-logvolumeexplorer-app":
@@ -173,6 +176,9 @@ func (root *NavTreeRoot) ApplyCostManagementIA() {
costManagementLogsNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/logs")
if costManagementLogsNode != nil {
if adaptiveLogsApp != nil {
costManagementLogsNode.Children = append(costManagementLogsNode.Children, adaptiveLogsApp)
}
if logVolumeExplorerApp != nil {
costManagementLogsNode.Children = append(costManagementLogsNode.Children, logVolumeExplorerApp)
}
@@ -298,6 +298,7 @@ func (s *ServiceImpl) readNavigationSettings() {
"grafana-cloud-link-app": {SectionID: navtree.NavIDCfgPlugins, SortWeight: 3},
"grafana-costmanagementui-app": {SectionID: navtree.NavIDCfg, Text: "Cost management"},
"grafana-adaptive-metrics-app": {SectionID: navtree.NavIDCfg, Text: "Adaptive Metrics"},
"grafana-adaptivelogs-app": {SectionID: navtree.NavIDCfg, Text: "Adaptive Logs"},
"grafana-attributions-app": {SectionID: navtree.NavIDCfg, Text: "Attributions"},
"grafana-logvolumeexplorer-app": {SectionID: navtree.NavIDCfg, Text: "Log Volume Explorer"},
"grafana-easystart-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApps + 1, Text: "Connections", Icon: "adjust-circle"},
+2 -2
View File
@@ -183,7 +183,7 @@ func calculateState(ctx context.Context, log log.Logger, alertRule *ngModels.Ale
}
}
if len(dupes) > 0 {
log.Warn("Rule declares one or many reserved labels. Those rules labels will be ignored", "labels", dupes)
log.Debug("Rule declares one or many reserved labels. Those rules labels will be ignored", "labels", dupes)
}
dupes = make(data.Labels)
for key, val := range resultLabels {
@@ -196,7 +196,7 @@ func calculateState(ctx context.Context, log log.Logger, alertRule *ngModels.Ale
}
}
if len(dupes) > 0 {
log.Warn("Evaluation result contains either reserved labels or labels declared in the rules. Those labels from the result will be ignored", "labels", dupes)
log.Debug("Evaluation result contains either reserved labels or labels declared in the rules. Those labels from the result will be ignored", "labels", dupes)
}
cacheID := lbs.Fingerprint()
+7
View File
@@ -67,6 +67,7 @@ type SavePreferenceCommand struct {
Language string `json:"language,omitempty"`
QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"`
CookiePreferences []CookieType `json:"cookiePreferences,omitempty"`
Navbar *NavbarPreference `json:"navbar,omitempty"`
}
type PatchPreferenceCommand struct {
@@ -82,18 +83,24 @@ type PatchPreferenceCommand struct {
Language *string `json:"language,omitempty"`
QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"`
CookiePreferences []CookieType `json:"cookiePreferences,omitempty"`
Navbar *NavbarPreference `json:"navbar,omitempty"`
}
type PreferenceJSONData struct {
Language string `json:"language"`
QueryHistory QueryHistoryPreference `json:"queryHistory"`
CookiePreferences map[string]struct{} `json:"cookiePreferences"`
Navbar NavbarPreference `json:"navbar"`
}
type QueryHistoryPreference struct {
HomeTab string `json:"homeTab"`
}
type NavbarPreference struct {
SavedItemIds []string `json:"savedItemIds"`
}
func (j *PreferenceJSONData) FromDB(data []byte) error {
dec := json.NewDecoder(bytes.NewBuffer(data))
dec.UseNumber()
+8
View File
@@ -46,6 +46,7 @@ func UpdatePreferencesFor(ctx context.Context,
HomeDashboardID: dtoCmd.HomeDashboardID,
QueryHistory: dtoCmd.QueryHistory,
CookiePreferences: dtoCmd.Cookies,
Navbar: dtoCmd.Navbar,
}
if err := preferenceService.Save(ctx, &saveCmd); err != nil {
@@ -96,6 +97,13 @@ func GetPreferencesFor(ctx context.Context,
dto.Language = &preference.JSONData.Language
}
if preference.JSONData.Navbar.SavedItemIds != nil {
dto.Navbar = &preferences.NavbarPreference{
SavedItemIds: []string{},
}
dto.Navbar.SavedItemIds = preference.JSONData.Navbar.SavedItemIds
}
if preference.JSONData.QueryHistory.HomeTab != "" {
dto.QueryHistory = &preferences.QueryHistoryPreference{
HomeTab: &preference.JSONData.QueryHistory.HomeTab,
+14
View File
@@ -71,6 +71,10 @@ func (s *Service) GetWithDefaults(ctx context.Context, query *pref.GetPreference
res.JSONData.QueryHistory.HomeTab = p.JSONData.QueryHistory.HomeTab
}
if p.JSONData.Navbar.SavedItemIds != nil {
res.JSONData.Navbar.SavedItemIds = p.JSONData.Navbar.SavedItemIds
}
if p.JSONData.CookiePreferences != nil {
res.JSONData.CookiePreferences = p.JSONData.CookiePreferences
}
@@ -170,6 +174,13 @@ func (s *Service) Patch(ctx context.Context, cmd *pref.PatchPreferenceCommand) e
preference.JSONData.Language = *cmd.Language
}
if cmd.Navbar != nil && cmd.Navbar.SavedItemIds != nil {
if preference.JSONData == nil {
preference.JSONData = &pref.PreferenceJSONData{}
}
preference.JSONData.Navbar.SavedItemIds = cmd.Navbar.SavedItemIds
}
if cmd.QueryHistory != nil {
if preference.JSONData == nil {
preference.JSONData = &pref.PreferenceJSONData{}
@@ -257,6 +268,9 @@ func preferenceData(cmd *pref.SavePreferenceCommand) (*pref.PreferenceJSONData,
Language: cmd.Language,
}
if cmd.Navbar != nil {
jsonData.Navbar = *cmd.Navbar
}
if cmd.QueryHistory != nil {
jsonData.QueryHistory = *cmd.QueryHistory
}
@@ -91,7 +91,7 @@ func TestIntegrationListPublicDashboard(t *testing.T) {
{Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", cDash.UID)},
}
usr := &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByAction(permissions)}}
usr := &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByActionContext(context.Background(), permissions)}}
actest.AddUserPermissionToDB(t, sqlStore, usr)
@@ -120,7 +120,7 @@ func TestIntegrationListPublicDashboard(t *testing.T) {
{Action: dashboards.ActionDashboardsRead, Scope: fmt.Sprintf("dashboards:uid:%s", cDash.UID)},
}
usr := &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByAction(permissions)}}
usr := &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByActionContext(context.Background(), permissions)}}
actest.AddUserPermissionToDB(t, sqlStore, usr)
@@ -148,7 +148,7 @@ func TestIntegrationListPublicDashboard(t *testing.T) {
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:another-dashboard-2-uid"},
}
usr := &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByAction(permissions)}}
usr := &user.SignedInUser{UserID: 1, OrgID: orgId, Permissions: map[int64]map[string][]string{orgId: accesscontrol.GroupScopesByActionContext(context.Background(), permissions)}}
actest.AddUserPermissionToDB(t, sqlStore, usr)
+1 -1
View File
@@ -199,7 +199,7 @@ func (s *StandardSearchService) getUser(ctx context.Context, backendUser *backen
return nil, errors.New("auth error")
}
usr.Permissions[orgId] = accesscontrol.GroupScopesByAction(permissions)
usr.Permissions[orgId] = accesscontrol.GroupScopesByActionContext(ctx, permissions)
return usr, nil
}
+6 -5
View File
@@ -1,6 +1,7 @@
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
@@ -87,7 +88,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
req := server.NewRequest(http.MethodPost, "/api/serviceaccounts/", strings.NewReader(tt.body))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgRole: tt.basicRole, OrgID: 1, IsAnonymous: true,
Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}})
Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}})
res, err := server.SendJSON(req)
require.NoError(t, err)
@@ -124,7 +125,7 @@ func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) {
t.Run(tt.desc, func(t *testing.T) {
server := setupTests(t)
req := server.NewRequest(http.MethodDelete, fmt.Sprintf("/api/serviceaccounts/%d", tt.id), nil)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}})
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}})
res, err := server.Send(req)
require.NoError(t, err)
@@ -165,7 +166,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
a.service = &satests.FakeServiceAccountService{ExpectedServiceAccountProfile: tt.expectedSA}
})
req := server.NewGetRequest(fmt.Sprintf("/api/serviceaccounts/%d", tt.id))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}})
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}})
res, err := server.Send(req)
require.NoError(t, err)
assert.Equal(t, tt.expectedCode, res.StatusCode)
@@ -228,7 +229,7 @@ func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) {
})
req := server.NewRequest(http.MethodPatch, fmt.Sprintf("/api/serviceaccounts/%d", tt.id), strings.NewReader(tt.body))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgRole: tt.basicRole, OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}})
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgRole: tt.basicRole, OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}})
res, err := server.SendJSON(req)
require.NoError(t, err)
@@ -282,7 +283,7 @@ func TestServiceAccountsAPI_MigrateApiKeysToServiceAccounts(t *testing.T) {
})
req := server.NewRequest(http.MethodPost, "/api/serviceaccounts/migrate", nil)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgRole: tt.basicRole, OrgID: tt.orgId, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}})
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgRole: tt.basicRole, OrgID: tt.orgId, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}})
res, err := server.SendJSON(req)
require.NoError(t, err)
@@ -1,6 +1,7 @@
package api
import (
"context"
"fmt"
"net/http"
"strings"
@@ -47,7 +48,7 @@ func TestServiceAccountsAPI_ListTokens(t *testing.T) {
a.service = &satests.FakeServiceAccountService{}
})
req := server.NewGetRequest(fmt.Sprintf("/api/serviceaccounts/%d/tokens", tt.id))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}})
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}})
res, err := server.Send(req)
require.NoError(t, err)
@@ -116,7 +117,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) {
}
})
req := server.NewRequest(http.MethodPost, fmt.Sprintf("/api/serviceaccounts/%d/tokens", tt.id), strings.NewReader(tt.body))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}})
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}})
res, err := server.SendJSON(req)
require.NoError(t, err)
@@ -168,7 +169,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) {
})
req := server.NewRequest(http.MethodDelete, fmt.Sprintf("/api/serviceaccounts/%d/tokens/%d", tt.saID, tt.apikeyID), nil)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}})
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}})
res, err := server.SendJSON(req)
require.NoError(t, err)
@@ -173,7 +173,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) {
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
require.NoError(t, err)
usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}
usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
@@ -345,7 +345,7 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
require.NoError(t, err)
usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.signedInUserPermissions)}}
usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.signedInUserPermissions)}}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
@@ -456,7 +456,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
Action: dashboards.ActionFoldersWrite,
Scope: dashboards.ScopeFoldersAll,
})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.permissions)}}
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), tc.permissions)}}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagAccessActionSets)...), featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
m := features.GetEnabled(context.Background())
@@ -564,7 +564,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
for _, tc := range testCases {
helperUser := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule,
Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
{
Action: dashboards.ActionFoldersCreate,
},
@@ -583,7 +583,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
}
t.Run(tc.desc+" with features "+strings.Join(keys, ","), func(t *testing.T) {
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.signedInUserPermissions)}}
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, AuthenticatedBy: login.ExtendedJWTModule, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), tc.signedInUserPermissions)}}
db := setupNestedTest(t, helperUser, []accesscontrol.Permission{}, orgID, features)
recursiveQueriesAreSupported, err := db.RecursiveQueriesAreSupported()
require.NoError(t, err)
@@ -693,7 +693,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
Scope: "folders:uid:unrelated"}, accesscontrol.Permission{
Action: dashboards.ActionDashboardsCreate,
Scope: "folders:uid:unrelated"})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.signedInUserPermissions)}}
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), tc.signedInUserPermissions)}}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
m := features.GetEnabled(context.Background())
@@ -34,7 +34,7 @@ import (
func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards, numFolders, nestingLevel int) {
usr := user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
{
Action: dashboards.ActionFoldersCreate,
},
@@ -320,7 +320,7 @@ func TestBuilder_RBAC(t *testing.T) {
for _, tc := range testsCases {
t.Run(tc.desc, func(t *testing.T) {
if len(tc.userPermissions) > 0 {
user.Permissions = map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tc.userPermissions)}
user.Permissions = map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tc.userPermissions)}
}
builder := &searchstore.Builder{
+1
View File
@@ -64,6 +64,7 @@ func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args user.C
// create user
usr = user.User{
UID: util.GenerateShortUID(),
Email: args.Email,
Login: args.Login,
IsAdmin: args.IsAdmin,
+2 -1
View File
@@ -2,6 +2,7 @@ package api
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@@ -550,7 +551,7 @@ func TestSSOSettingsAPI_List(t *testing.T) {
func getPermissionsForActionAndScope(action, scope string) map[int64]map[string][]string {
return map[int64]map[string][]string{
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{{
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{{
Action: action, Scope: scope,
}}),
}
+2 -2
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.33.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: entity.proto
@@ -1401,7 +1401,7 @@ type EntityListRequest struct {
WithStatus bool `protobuf:"varint,10,opt,name=with_status,json=withStatus,proto3" json:"with_status,omitempty"`
// list deleted entities instead of active ones
Deleted bool `protobuf:"varint,12,opt,name=deleted,proto3" json:"deleted,omitempty"`
// Limit to a set of origin keys (empty is all)
// Deprecated: Limit to a set of origin keys (empty is all)
OriginKeys []string `protobuf:"bytes,13,rep,name=origin_keys,json=originKeys,proto3" json:"origin_keys,omitempty"`
}
+25 -13
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc-gen-go-grpc v1.4.0
// - protoc (unknown)
// source: entity.proto
@@ -15,8 +15,8 @@ import (
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// Requires gRPC-Go v1.62.0 or later.
const _ = grpc.SupportPackageIsVersion8
const (
EntityStore_Read_FullMethodName = "/entity.EntityStore/Read"
@@ -32,6 +32,8 @@ const (
// EntityStoreClient is the client API for EntityStore service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// The entity store provides a basic CRUD (+watch eventually) interface for generic entities
type EntityStoreClient interface {
Read(ctx context.Context, in *ReadEntityRequest, opts ...grpc.CallOption) (*Entity, error)
Create(ctx context.Context, in *CreateEntityRequest, opts ...grpc.CallOption) (*CreateEntityResponse, error)
@@ -52,8 +54,9 @@ func NewEntityStoreClient(cc grpc.ClientConnInterface) EntityStoreClient {
}
func (c *entityStoreClient) Read(ctx context.Context, in *ReadEntityRequest, opts ...grpc.CallOption) (*Entity, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Entity)
err := c.cc.Invoke(ctx, EntityStore_Read_FullMethodName, in, out, opts...)
err := c.cc.Invoke(ctx, EntityStore_Read_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -61,8 +64,9 @@ func (c *entityStoreClient) Read(ctx context.Context, in *ReadEntityRequest, opt
}
func (c *entityStoreClient) Create(ctx context.Context, in *CreateEntityRequest, opts ...grpc.CallOption) (*CreateEntityResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateEntityResponse)
err := c.cc.Invoke(ctx, EntityStore_Create_FullMethodName, in, out, opts...)
err := c.cc.Invoke(ctx, EntityStore_Create_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -70,8 +74,9 @@ func (c *entityStoreClient) Create(ctx context.Context, in *CreateEntityRequest,
}
func (c *entityStoreClient) Update(ctx context.Context, in *UpdateEntityRequest, opts ...grpc.CallOption) (*UpdateEntityResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateEntityResponse)
err := c.cc.Invoke(ctx, EntityStore_Update_FullMethodName, in, out, opts...)
err := c.cc.Invoke(ctx, EntityStore_Update_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -79,8 +84,9 @@ func (c *entityStoreClient) Update(ctx context.Context, in *UpdateEntityRequest,
}
func (c *entityStoreClient) Delete(ctx context.Context, in *DeleteEntityRequest, opts ...grpc.CallOption) (*DeleteEntityResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeleteEntityResponse)
err := c.cc.Invoke(ctx, EntityStore_Delete_FullMethodName, in, out, opts...)
err := c.cc.Invoke(ctx, EntityStore_Delete_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -88,8 +94,9 @@ func (c *entityStoreClient) Delete(ctx context.Context, in *DeleteEntityRequest,
}
func (c *entityStoreClient) History(ctx context.Context, in *EntityHistoryRequest, opts ...grpc.CallOption) (*EntityHistoryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(EntityHistoryResponse)
err := c.cc.Invoke(ctx, EntityStore_History_FullMethodName, in, out, opts...)
err := c.cc.Invoke(ctx, EntityStore_History_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -97,8 +104,9 @@ func (c *entityStoreClient) History(ctx context.Context, in *EntityHistoryReques
}
func (c *entityStoreClient) List(ctx context.Context, in *EntityListRequest, opts ...grpc.CallOption) (*EntityListResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(EntityListResponse)
err := c.cc.Invoke(ctx, EntityStore_List_FullMethodName, in, out, opts...)
err := c.cc.Invoke(ctx, EntityStore_List_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -106,11 +114,12 @@ func (c *entityStoreClient) List(ctx context.Context, in *EntityListRequest, opt
}
func (c *entityStoreClient) Watch(ctx context.Context, opts ...grpc.CallOption) (EntityStore_WatchClient, error) {
stream, err := c.cc.NewStream(ctx, &EntityStore_ServiceDesc.Streams[0], EntityStore_Watch_FullMethodName, opts...)
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &EntityStore_ServiceDesc.Streams[0], EntityStore_Watch_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &entityStoreWatchClient{stream}
x := &entityStoreWatchClient{ClientStream: stream}
return x, nil
}
@@ -137,8 +146,9 @@ func (x *entityStoreWatchClient) Recv() (*EntityWatchResponse, error) {
}
func (c *entityStoreClient) IsHealthy(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(HealthCheckResponse)
err := c.cc.Invoke(ctx, EntityStore_IsHealthy_FullMethodName, in, out, opts...)
err := c.cc.Invoke(ctx, EntityStore_IsHealthy_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -148,6 +158,8 @@ func (c *entityStoreClient) IsHealthy(ctx context.Context, in *HealthCheckReques
// EntityStoreServer is the server API for EntityStore service.
// All implementations should embed UnimplementedEntityStoreServer
// for forward compatibility
//
// The entity store provides a basic CRUD (+watch eventually) interface for generic entities
type EntityStoreServer interface {
Read(context.Context, *ReadEntityRequest) (*Entity, error)
Create(context.Context, *CreateEntityRequest) (*CreateEntityResponse, error)
@@ -308,7 +320,7 @@ func _EntityStore_List_Handler(srv interface{}, ctx context.Context, dec func(in
}
func _EntityStore_Watch_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(EntityStoreServer).Watch(&entityStoreWatchServer{stream})
return srv.(EntityStoreServer).Watch(&entityStoreWatchServer{ServerStream: stream})
}
type EntityStore_WatchServer interface {
@@ -655,6 +655,7 @@ func (s *sqlEntityServer) List(ctx context.Context, r *entity.EntityListRequest)
rvSubQuery.AddWhere("("+strings.Join(where, " OR ")+")", args...)
}
// nolint:staticcheck
if len(r.OriginKeys) > 0 {
entityQuery.AddWhereIn("origin_key", ToAnyList(r.OriginKeys))
rvMaxQuery.AddWhereIn("origin_key", ToAnyList(r.OriginKeys))
@@ -6,7 +6,7 @@
"namespace": "default",
"name": "adnj1llchbbi8a",
"group_version": "v0alpha1",
"key": "/playlist.grafana.app/playlists/namespaces/default/adnj1llchbbi8a",
"key": "/group/playlist.grafana.app/resource/playlists/namespaces/default/name/adnj1llchbbi8a",
"meta": "eyJtZXRhZGF0YSI6eyJuYW1lIjoiYWRuajFsbGNoYmJpOGEiLCJuYW1lc3BhY2UiOiJkZWZhdWx0IiwidWlkIjoiYjAxOTljNjAtNWYzYS00MWJlLTliYTYtN2E1MmYxZGU4M2ZmIiwiY3JlYXRpb25UaW1lc3RhbXAiOiIyMDI0LTA2LTAyVDAzOjI4OjE3WiIsImFubm90YXRpb25zIjp7ImdyYWZhbmEuYXBwL29yaWdpbktleSI6IjIiLCJncmFmYW5hLmFwcC9vcmlnaW5OYW1lIjoiU1FMIiwiZ3JhZmFuYS5hcHAvb3JpZ2luVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oiLCJncmFmYW5hLmFwcC91cGRhdGVkVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oifX19",
"body": "eyJraW5kIjoiUGxheWxpc3QiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJtZXRhZGF0YSI6eyJuYW1lIjoiYWRuajFsbGNoYmJpOGEiLCJuYW1lc3BhY2UiOiJkZWZhdWx0IiwidWlkIjoiYjAxOTljNjAtNWYzYS00MWJlLTliYTYtN2E1MmYxZGU4M2ZmIiwiY3JlYXRpb25UaW1lc3RhbXAiOiIyMDI0LTA2LTAyVDAzOjI4OjE3WiIsImFubm90YXRpb25zIjp7ImdyYWZhbmEuYXBwL29yaWdpbktleSI6IjIiLCJncmFmYW5hLmFwcC9vcmlnaW5OYW1lIjoiU1FMIiwiZ3JhZmFuYS5hcHAvb3JpZ2luVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oiLCJncmFmYW5hLmFwcC91cGRhdGVkVGltZXN0YW1wIjoiMjAyNC0wNi0wMlQwMzoyODoxN1oifX0sInNwZWMiOnsidGl0bGUiOiJ0ZXN0IHBsYXlsaXN0IiwiaW50ZXJ2YWwiOiI1bSIsIml0ZW1zIjpbeyJ0eXBlIjoiZGFzaGJvYXJkX2J5X3VpZCIsInZhbHVlIjoiY2RuaXY1M2dtZDR3MGUifV19fQo=",
"title": "test playlist",
@@ -1,3 +1,3 @@
{
"key": "/playlist.grafana.app/playlists/namespaces/default/sdfsdfsdf"
"key": "/group/playlist.grafana.app/resource/playlists/namespaces/default/name/sdfsdfsdf"
}
@@ -7,7 +7,7 @@
"namespace": "default",
"name": "sdfsdfsdf",
"group_version": "v0alpha1",
"key": "/playlist.grafana.app/playlists/namespaces/default/sdfsdfsdf",
"key": "/group/playlist.grafana.app/resource/playlists/namespaces/default/name/sdfsdfsdf",
"meta": "eyJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjNjNzY5YjJlLWFhYTctNDZmNi1hYjgzLWUwMzgwNTBhNmE3NSIsInJlc291cmNlVmVyc2lvbiI6IjEiLCJjcmVhdGlvblRpbWVzdGFtcCI6IjIwMjQtMDYtMDJUMDM6NDk6MjlaIiwibWFuYWdlZEZpZWxkcyI6W3sibWFuYWdlciI6Ik1vemlsbGEiLCJvcGVyYXRpb24iOiJVcGRhdGUiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJ0aW1lIjoiMjAyNC0wNi0wMlQwMzo1Mzo1NVoiLCJmaWVsZHNUeXBlIjoiRmllbGRzVjEiLCJmaWVsZHNWMSI6eyJmOnNwZWMiOnsiZjppbnRlcnZhbCI6e30sImY6aXRlbXMiOnt9LCJmOnRpdGxlIjp7fX19fV19fQ==",
"body": "eyJraW5kIjoiUGxheWxpc3QiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjNjNzY5YjJlLWFhYTctNDZmNi1hYjgzLWUwMzgwNTBhNmE3NSIsInJlc291cmNlVmVyc2lvbiI6IjEiLCJjcmVhdGlvblRpbWVzdGFtcCI6IjIwMjQtMDYtMDJUMDM6NDk6MjlaIiwibWFuYWdlZEZpZWxkcyI6W3sibWFuYWdlciI6Ik1vemlsbGEiLCJvcGVyYXRpb24iOiJVcGRhdGUiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJ0aW1lIjoiMjAyNC0wNi0wMlQwMzo1Mzo1NVoiLCJmaWVsZHNUeXBlIjoiRmllbGRzVjEiLCJmaWVsZHNWMSI6eyJmOnNwZWMiOnsiZjppbnRlcnZhbCI6e30sImY6aXRlbXMiOnt9LCJmOnRpdGxlIjp7fX19fV19LCJzcGVjIjp7InRpdGxlIjoieHpjdnp4Y3Zxd2Vxd2UiLCJpbnRlcnZhbCI6IjVtIiwiaXRlbXMiOlt7InR5cGUiOiJkYXNoYm9hcmRfYnlfdWlkIiwidmFsdWUiOiJjZG5pdjUzZ21kNHcwZSJ9XX19Cg==",
"title": "xzcvzxcvqweqwe",
@@ -6,7 +6,7 @@
"namespace": "default",
"name": "sdfsdfsdf",
"group_version": "v0alpha1",
"key": "/playlist.grafana.app/playlists/namespaces/default/sdfsdfsdf",
"key": "/group/playlist.grafana.app/resource/playlists/namespace/default/name/sdfsdfsdf",
"meta": "eyJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjAyZmVhOGVlLTk2ZDYtNGIzMy04ZGI5LTU5MmI0NzU4NTM4NSIsImNyZWF0aW9uVGltZXN0YW1wIjoiMjAyNC0wNi0wNFQxNToxODozNFoiLCJtYW5hZ2VkRmllbGRzIjpbeyJtYW5hZ2VyIjoiTW96aWxsYSIsIm9wZXJhdGlvbiI6IlVwZGF0ZSIsImFwaVZlcnNpb24iOiJwbGF5bGlzdC5ncmFmYW5hLmFwcC92MGFscGhhMSIsInRpbWUiOiIyMDI0LTA2LTA0VDE1OjE4OjM0WiIsImZpZWxkc1R5cGUiOiJGaWVsZHNWMSIsImZpZWxkc1YxIjp7ImY6c3BlYyI6eyJmOmludGVydmFsIjp7fSwiZjppdGVtcyI6e30sImY6dGl0bGUiOnt9fX19XX19",
"body": "eyJraW5kIjoiUGxheWxpc3QiLCJhcGlWZXJzaW9uIjoicGxheWxpc3QuZ3JhZmFuYS5hcHAvdjBhbHBoYTEiLCJtZXRhZGF0YSI6eyJuYW1lIjoic2Rmc2Rmc2RmIiwibmFtZXNwYWNlIjoiZGVmYXVsdCIsInVpZCI6IjAyZmVhOGVlLTk2ZDYtNGIzMy04ZGI5LTU5MmI0NzU4NTM4NSIsImNyZWF0aW9uVGltZXN0YW1wIjoiMjAyNC0wNi0wNFQxNToxODozNFoiLCJtYW5hZ2VkRmllbGRzIjpbeyJtYW5hZ2VyIjoiTW96aWxsYSIsIm9wZXJhdGlvbiI6IlVwZGF0ZSIsImFwaVZlcnNpb24iOiJwbGF5bGlzdC5ncmFmYW5hLmFwcC92MGFscGhhMSIsInRpbWUiOiIyMDI0LTA2LTA0VDE1OjE4OjM0WiIsImZpZWxkc1R5cGUiOiJGaWVsZHNWMSIsImZpZWxkc1YxIjp7ImY6c3BlYyI6eyJmOmludGVydmFsIjp7fSwiZjppdGVtcyI6e30sImY6dGl0bGUiOnt9fX19XX0sInNwZWMiOnsidGl0bGUiOiJ4emN2enhjdiIsImludGVydmFsIjoiNW0iLCJpdGVtcyI6W3sidHlwZSI6ImRhc2hib2FyZF9ieV91aWQiLCJ2YWx1ZSI6ImNkbml2NTNnbWQ0dzBlIn1dfX0K",
"title": "xzcvzxcv",
@@ -281,5 +281,5 @@ func Test_getTeamMembershipUpdates(t *testing.T) {
}
func authedUserWithPermissions(userID, orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
return &user.SignedInUser{UserID: userID, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}}
return &user.SignedInUser{UserID: userID, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), permissions)}}
}
-23
View File
@@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@@ -567,25 +566,3 @@ func (ss *xormStore) getTeamMembers(ctx context.Context, query *team.GetTeamMemb
func (ss *xormStore) RegisterDelete(query string) {
ss.deletes = append(ss.deletes, query)
}
// This is just to ensure that all teams have a valid uid.
// To protect against upgrade / downgrade we need to run this for a couple of releases.
// FIXME: Remove this migration and make uid field required https://github.com/grafana/identity-access-team/issues/552
func (ss *xormStore) uidMigration() error {
return ss.db.WithDbSession(context.Background(), func(sess *db.Session) error {
switch ss.db.GetDBType() {
case migrator.SQLite:
_, err := sess.Exec("UPDATE team SET uid=printf('t%09d',id) WHERE uid IS NULL;")
return err
case migrator.Postgres:
_, err := sess.Exec("UPDATE team SET uid='t' || lpad('' || id::text,9,'0') WHERE uid IS NULL;")
return err
case migrator.MySQL:
_, err := sess.Exec("UPDATE team SET uid=concat('t',lpad(id,9,'0')) WHERE uid IS NULL;")
return err
default:
// this branch should be unreachable
return nil
}
})
}
-5
View File
@@ -18,11 +18,6 @@ type Service struct {
}
func ProvideService(db db.DB, cfg *setting.Cfg, tracer tracing.Tracer) (team.Service, error) {
store := &xormStore{db: db, cfg: cfg, deletes: []string{}}
if err := store.uidMigration(); err != nil {
return nil, err
}
return &Service{
store: &xormStore{db: db, cfg: cfg, deletes: []string{}},
tracer: tracer,
-27
View File
@@ -17,7 +17,6 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
@@ -65,10 +64,6 @@ func ProvideService(
return s, err
}
if err := s.uidMigration(db); err != nil {
return nil, err
}
bundleRegistry.RegisterSupportItemCollector(s.supportBundleCollector())
return s, nil
}
@@ -529,25 +524,3 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits.Set(globalQuotaTag, cfg.Quota.Global.User)
return limits, nil
}
// This is just to ensure that all users have a valid uid.
// To protect against upgrade / downgrade we need to run this for a couple of releases.
// FIXME: Remove this migration and make uid field required https://github.com/grafana/identity-access-team/issues/552
func (s *Service) uidMigration(store db.DB) error {
return store.WithDbSession(context.Background(), func(sess *db.Session) error {
switch store.GetDBType() {
case migrator.SQLite:
_, err := sess.Exec("UPDATE user SET uid=printf('u%09d',id) WHERE uid IS NULL;")
return err
case migrator.Postgres:
_, err := sess.Exec("UPDATE `user` SET uid='u' || lpad('' || id::text,9,'0') WHERE uid IS NULL;")
return err
case migrator.MySQL:
_, err := sess.Exec("UPDATE user SET uid=concat('u',lpad(id,9,'0')) WHERE uid IS NULL;")
return err
default:
// this branch should be unreachable
return nil
}
})
}
+10
View File
@@ -1,12 +1,15 @@
package setting
import (
"os"
"time"
)
type CloudMigrationSettings struct {
IsTarget bool
GcomAPIToken string
SnapshotFolder string
StartSnapshotTimeout time.Duration
FetchInstanceTimeout time.Duration
CreateAccessPolicyTimeout time.Duration
FetchAccessPolicyTimeout time.Duration
@@ -23,6 +26,8 @@ func (cfg *Cfg) readCloudMigrationSettings() {
cloudMigration := cfg.Raw.Section("cloud_migration")
cfg.CloudMigration.IsTarget = cloudMigration.Key("is_target").MustBool(false)
cfg.CloudMigration.GcomAPIToken = cloudMigration.Key("gcom_api_token").MustString("")
cfg.CloudMigration.SnapshotFolder = cloudMigration.Key("snapshot_folder").MustString("")
cfg.CloudMigration.StartSnapshotTimeout = cloudMigration.Key("start_snapshot_timeout").MustDuration(5 * time.Second)
cfg.CloudMigration.FetchInstanceTimeout = cloudMigration.Key("fetch_instance_timeout").MustDuration(5 * time.Second)
cfg.CloudMigration.CreateAccessPolicyTimeout = cloudMigration.Key("create_access_policy_timeout").MustDuration(5 * time.Second)
cfg.CloudMigration.FetchAccessPolicyTimeout = cloudMigration.Key("fetch_access_policy_timeout").MustDuration(5 * time.Second)
@@ -32,4 +37,9 @@ func (cfg *Cfg) readCloudMigrationSettings() {
cfg.CloudMigration.DeleteTokenTimeout = cloudMigration.Key("delete_token_timeout").MustDuration(5 * time.Second)
cfg.CloudMigration.TokenExpiresAfter = cloudMigration.Key("token_expires_after").MustDuration(7 * 24 * time.Hour)
cfg.CloudMigration.IsDeveloperMode = cloudMigration.Key("developer_mode").MustBool(false)
if cfg.CloudMigration.SnapshotFolder == "" {
homeDir, _ := os.UserHomeDir()
cfg.CloudMigration.SnapshotFolder = homeDir
}
}
+5
View File
@@ -0,0 +1,5 @@
// Package apistore provides a kubernetes store.Interface for a ResourceServer
//
// This package is responsible for running all the apiserver specific logic
// before and after sending requests to the StorageServer
package apistore
+11 -26
View File
@@ -13,7 +13,6 @@ import (
"io"
"reflect"
"strconv"
"strings"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
@@ -21,7 +20,6 @@ import (
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/storagebackend"
@@ -33,8 +31,6 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
const SortByKey = "grafana.app/sortBy"
var _ storage.Interface = (*Storage)(nil)
// Storage implements storage.Interface and stores resources in unified storage
@@ -306,28 +302,6 @@ func toListRequest(key string, opts storage.ListOptions) (*resource.ListRequest,
for _, r := range requirements {
v := r.Key()
// TODO?? sorting in list not supported
if v == SortByKey {
if r.Operator() != selection.Equals {
return nil, predicate, apierrors.NewBadRequest("invalid sort operation // " + r.String())
}
parts := strings.Split(v, " ")
if len(parts) != 2 {
return nil, predicate, apierrors.NewBadRequest("invalid sort operation // " + r.String())
}
sort := &resource.Sort{Field: parts[0]}
switch parts[1] {
case "ASC":
sort.Order = resource.Sort_ASC
case "DESC":
sort.Order = resource.Sort_DESC
default:
return nil, predicate, apierrors.NewBadRequest("invalid sort order // " + r.String())
}
// TODO! Must update the predicate!
continue
}
req.Options.Labels = append(req.Options.Labels, &resource.Requirement{
Key: v,
Operator: string(r.Operator()),
@@ -336,6 +310,17 @@ func toListRequest(key string, opts storage.ListOptions) (*resource.ListRequest,
}
}
if opts.Predicate.Field != nil && !opts.Predicate.Field.Empty() {
requirements := opts.Predicate.Field.Requirements()
for _, r := range requirements {
requirement := &resource.Requirement{Key: r.Field, Operator: string(r.Operator)}
if r.Value != "" {
requirement.Values = append(requirement.Values, r.Value)
}
req.Options.Labels = append(req.Options.Labels, requirement)
}
}
if opts.ResourceVersion != "" {
rv, err := strconv.ParseInt(opts.ResourceVersion, 10, 64)
if err != nil {
+5
View File
@@ -0,0 +1,5 @@
// Package entitybridge implements an ResourceServer using existing EntityAPI contracts
//
// This package will be removed and replaced with a more streamlined SQL implementation
// that leverages what we have learned from the entity deployments so far
package entitybridge
@@ -8,6 +8,7 @@ import (
"time"
"gocloud.dev/blob/fileblob"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/klog/v2"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
@@ -29,7 +30,7 @@ func ProvideResourceServer(db db.DB, cfg *setting.Cfg, features featuremgmt.Feat
}
supportBlobs := true
useEntitySQL := true // save in SQL (but watch not working)
useEntitySQL := true
// Create a local blob filesystem blob store
if supportBlobs {
@@ -271,11 +272,15 @@ func (b *entityBridge) PrepareList(ctx context.Context, req *resource.ListReques
WithBody: true,
}
// Assumes everything is equals
if len(req.Options.Labels) > 0 {
query.Labels = make(map[string]string)
for _, q := range req.Options.Labels {
query.Labels[q.Key] = q.Values[0]
// The entity structure only supports equals
// the rest will be processed handled by the upstream predicate
op := selection.Operator(q.Operator)
if op == selection.Equals || op == selection.DoubleEquals {
query.Labels[q.Key] = q.Values[0]
}
}
}
+40 -51
View File
@@ -69,8 +69,9 @@ type cdkBackend struct {
nextRV NextResourceVersion
mutex sync.Mutex
// Typically one... the server wrapper
subscribers []chan *WrittenEvent
// Simple watch stream -- NOTE, this only works for single tenant!
broadcaster Broadcaster[*WrittenEvent]
stream chan<- *WrittenEvent
}
func (s *cdkBackend) getPath(key *ResourceKey, rv int64) string {
@@ -123,24 +124,19 @@ func (s *cdkBackend) WriteEvent(ctx context.Context, event WriteEvent) (rv int64
}
// Async notify all subscribers
if s.subscribers != nil {
if s.stream != nil {
go func() {
write := &WrittenEvent{
WriteEvent: event,
WriteEvent: event,
Timestamp: time.Now().UnixMilli(),
ResourceVersion: rv,
}
for _, sub := range s.subscribers {
sub <- write
}
s.stream <- write
}()
}
return rv, err
}
// Read implements ResourceStoreServer.
func (s *cdkBackend) Read(ctx context.Context, req *ReadRequest) (*ReadResponse, error) {
rv := req.ResourceVersion
@@ -167,15 +163,11 @@ func (s *cdkBackend) Read(ctx context.Context, req *ReadRequest) (*ReadResponse,
}
raw, err := s.bucket.ReadAll(ctx, path)
if err == nil && bytes.Contains(raw, []byte(`"DeletedMarker"`)) {
tmp := &unstructured.Unstructured{}
err = tmp.UnmarshalJSON(raw)
if err == nil && tmp.GetKind() == "DeletedMarker" {
return nil, apierrors.NewNotFound(schema.GroupResource{
Group: req.Key.Group,
Resource: req.Key.Resource,
}, req.Key.Name)
}
if err == nil && isDeletedMarker(raw) {
return nil, apierrors.NewNotFound(schema.GroupResource{
Group: req.Key.Group,
Resource: req.Key.Resource,
}, req.Key.Name)
}
return &ReadResponse{
@@ -184,7 +176,17 @@ func (s *cdkBackend) Read(ctx context.Context, req *ReadRequest) (*ReadResponse,
}, err
}
// List implements AppendingStore.
func isDeletedMarker(raw []byte) bool {
if bytes.Contains(raw, []byte(`"DeletedMarker"`)) {
tmp := &unstructured.Unstructured{}
err := tmp.UnmarshalJSON(raw)
if err == nil && tmp.GetKind() == "DeletedMarker" {
return true
}
}
return false
}
func (s *cdkBackend) PrepareList(ctx context.Context, req *ListRequest) (*ListResponse, error) {
resources, err := buildTree(ctx, s, req.Options.Key)
if err != nil {
@@ -198,44 +200,31 @@ func (s *cdkBackend) PrepareList(ctx context.Context, req *ListRequest) (*ListRe
if err != nil {
return nil, err
}
rsp.Items = append(rsp.Items, &ResourceWrapper{
ResourceVersion: latest.rv,
Value: raw,
})
if !isDeletedMarker(raw) {
rsp.Items = append(rsp.Items, &ResourceWrapper{
ResourceVersion: latest.rv,
Value: raw,
})
}
}
return rsp, nil
}
// Watch implements AppendingStore.
func (s *cdkBackend) WatchWriteEvents(ctx context.Context) (<-chan *WrittenEvent, error) {
stream := make(chan *WrittenEvent, 10)
{
s.mutex.Lock()
defer s.mutex.Unlock()
s.mutex.Lock()
defer s.mutex.Unlock()
// Add the event stream
s.subscribers = append(s.subscribers, stream)
}
// Wait for context done
go func() {
// Wait till the context is done
<-ctx.Done()
// Then remove the subscription
s.mutex.Lock()
defer s.mutex.Unlock()
// Copy all streams without our listener
subs := []chan *WrittenEvent{}
for _, sub := range s.subscribers {
if sub != stream {
subs = append(subs, sub)
}
if s.broadcaster == nil {
var err error
s.broadcaster, err = NewBroadcaster(context.Background(), func(c chan<- *WrittenEvent) error {
s.stream = c
return nil
})
if err != nil {
return nil, err
}
s.subscribers = subs
}()
return stream, nil
}
return s.broadcaster.Subscribe(ctx)
}
// group > resource > namespace > name > versions
+2
View File
@@ -0,0 +1,2 @@
// Package resource creates a ResourceServer that handles generic storage operations
package resource
@@ -39,14 +39,14 @@ func (f *Authenticator) Authenticate(ctx context.Context) (context.Context, erro
if !ok {
return nil, fmt.Errorf("no metadata found")
}
user, err := f.DecodeMetadata(ctx, md)
user, err := f.decodeMetadata(ctx, md)
if err != nil {
return nil, err
}
return identity.WithRequester(ctx, user), nil
}
func (f *Authenticator) DecodeMetadata(ctx context.Context, meta metadata.MD) (identity.Requester, error) {
func (f *Authenticator) decodeMetadata(ctx context.Context, meta metadata.MD) (identity.Requester, error) {
// Avoid NPE/panic with getting keys
getter := func(key string) string {
v := meta.Get(key)
@@ -23,7 +23,7 @@ func TestBasicEncodeDecode(t *testing.T) {
auth := &Authenticator{}
md := encodeIdentityInMetadata(before)
after, err := auth.DecodeMetadata(context.Background(), md)
after, err := auth.decodeMetadata(context.Background(), md)
require.NoError(t, err)
require.Equal(t, before.GetID(), after.GetID())
require.Equal(t, before.GetUID(), after.GetUID())
+69 -54
View File
@@ -1201,12 +1201,16 @@ type ListOptions struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Namespace+Group+Resource+etc
// Group+Namespace+Resource (not name)
Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// (best effort) Match label
// Allowed to send more results than actually match because the filter will be appled
// to the resutls agin in the client. That time with the full field selector
Labels []*Requirement `protobuf:"bytes,2,rep,name=labels,proto3" json:"labels,omitempty"`
// (best effort) fields matcher
// Allowed to send more results than actually match because the filter will be appled
// to the resutls agin in the client. That time with the full field selector
Fields []*Requirement `protobuf:"bytes,3,rep,name=fields,proto3" json:"fields,omitempty"`
}
func (x *ListOptions) Reset() {
@@ -1255,6 +1259,13 @@ func (x *ListOptions) GetLabels() []*Requirement {
return nil
}
func (x *ListOptions) GetFields() []*Requirement {
if x != nil {
return x.Fields
}
return nil
}
type ListRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -2529,13 +2540,16 @@ var file_resource_proto_rawDesc = []byte{
0x63, 0x65, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f,
0x72, 0x64, 0x65, 0x72, 0x22, 0x1a, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x07, 0x0a,
0x03, 0x41, 0x53, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x45, 0x53, 0x43, 0x10, 0x01,
0x22, 0x65, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12,
0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65,
0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x22, 0x94, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x06, 0x6c, 0x61, 0x62,
0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c,
0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52,
0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x22, 0xec, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74,
0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, 0xec, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f,
0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12,
@@ -2831,53 +2845,54 @@ var file_resource_proto_depIdxs = []int32{
1, // 8: resource.Sort.order:type_name -> resource.Sort.Order
5, // 9: resource.ListOptions.key:type_name -> resource.ResourceKey
17, // 10: resource.ListOptions.labels:type_name -> resource.Requirement
0, // 11: resource.ListRequest.version_match:type_name -> resource.ResourceVersionMatch
19, // 12: resource.ListRequest.options:type_name -> resource.ListOptions
6, // 13: resource.ListResponse.items:type_name -> resource.ResourceWrapper
19, // 14: resource.WatchRequest.options:type_name -> resource.ListOptions
2, // 15: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type
35, // 16: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource
35, // 17: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource
5, // 18: resource.HistoryRequest.key:type_name -> resource.ResourceKey
7, // 19: resource.HistoryResponse.items:type_name -> resource.ResourceMeta
5, // 20: resource.OriginRequest.key:type_name -> resource.ResourceKey
5, // 21: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey
27, // 22: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo
3, // 23: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus
5, // 24: resource.PutBlobRequest.resource:type_name -> resource.ResourceKey
4, // 25: resource.PutBlobRequest.method:type_name -> resource.PutBlobRequest.Method
8, // 26: resource.PutBlobResponse.status:type_name -> resource.StatusResult
5, // 27: resource.GetBlobRequest.resource:type_name -> resource.ResourceKey
8, // 28: resource.GetBlobResponse.status:type_name -> resource.StatusResult
15, // 29: resource.ResourceStore.Read:input_type -> resource.ReadRequest
9, // 30: resource.ResourceStore.Create:input_type -> resource.CreateRequest
11, // 31: resource.ResourceStore.Update:input_type -> resource.UpdateRequest
13, // 32: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest
20, // 33: resource.ResourceStore.List:input_type -> resource.ListRequest
22, // 34: resource.ResourceStore.Watch:input_type -> resource.WatchRequest
15, // 35: resource.ResourceIndex.Read:input_type -> resource.ReadRequest
24, // 36: resource.ResourceIndex.History:input_type -> resource.HistoryRequest
26, // 37: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest
31, // 38: resource.BlobStore.PutBlob:input_type -> resource.PutBlobRequest
33, // 39: resource.BlobStore.GetBlob:input_type -> resource.GetBlobRequest
29, // 40: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest
16, // 41: resource.ResourceStore.Read:output_type -> resource.ReadResponse
10, // 42: resource.ResourceStore.Create:output_type -> resource.CreateResponse
12, // 43: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
14, // 44: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse
21, // 45: resource.ResourceStore.List:output_type -> resource.ListResponse
23, // 46: resource.ResourceStore.Watch:output_type -> resource.WatchEvent
16, // 47: resource.ResourceIndex.Read:output_type -> resource.ReadResponse
25, // 48: resource.ResourceIndex.History:output_type -> resource.HistoryResponse
28, // 49: resource.ResourceIndex.Origin:output_type -> resource.OriginResponse
32, // 50: resource.BlobStore.PutBlob:output_type -> resource.PutBlobResponse
34, // 51: resource.BlobStore.GetBlob:output_type -> resource.GetBlobResponse
30, // 52: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse
41, // [41:53] is the sub-list for method output_type
29, // [29:41] is the sub-list for method input_type
29, // [29:29] is the sub-list for extension type_name
29, // [29:29] is the sub-list for extension extendee
0, // [0:29] is the sub-list for field type_name
17, // 11: resource.ListOptions.fields:type_name -> resource.Requirement
0, // 12: resource.ListRequest.version_match:type_name -> resource.ResourceVersionMatch
19, // 13: resource.ListRequest.options:type_name -> resource.ListOptions
6, // 14: resource.ListResponse.items:type_name -> resource.ResourceWrapper
19, // 15: resource.WatchRequest.options:type_name -> resource.ListOptions
2, // 16: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type
35, // 17: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource
35, // 18: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource
5, // 19: resource.HistoryRequest.key:type_name -> resource.ResourceKey
7, // 20: resource.HistoryResponse.items:type_name -> resource.ResourceMeta
5, // 21: resource.OriginRequest.key:type_name -> resource.ResourceKey
5, // 22: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey
27, // 23: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo
3, // 24: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus
5, // 25: resource.PutBlobRequest.resource:type_name -> resource.ResourceKey
4, // 26: resource.PutBlobRequest.method:type_name -> resource.PutBlobRequest.Method
8, // 27: resource.PutBlobResponse.status:type_name -> resource.StatusResult
5, // 28: resource.GetBlobRequest.resource:type_name -> resource.ResourceKey
8, // 29: resource.GetBlobResponse.status:type_name -> resource.StatusResult
15, // 30: resource.ResourceStore.Read:input_type -> resource.ReadRequest
9, // 31: resource.ResourceStore.Create:input_type -> resource.CreateRequest
11, // 32: resource.ResourceStore.Update:input_type -> resource.UpdateRequest
13, // 33: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest
20, // 34: resource.ResourceStore.List:input_type -> resource.ListRequest
22, // 35: resource.ResourceStore.Watch:input_type -> resource.WatchRequest
15, // 36: resource.ResourceIndex.Read:input_type -> resource.ReadRequest
24, // 37: resource.ResourceIndex.History:input_type -> resource.HistoryRequest
26, // 38: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest
31, // 39: resource.BlobStore.PutBlob:input_type -> resource.PutBlobRequest
33, // 40: resource.BlobStore.GetBlob:input_type -> resource.GetBlobRequest
29, // 41: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest
16, // 42: resource.ResourceStore.Read:output_type -> resource.ReadResponse
10, // 43: resource.ResourceStore.Create:output_type -> resource.CreateResponse
12, // 44: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
14, // 45: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse
21, // 46: resource.ResourceStore.List:output_type -> resource.ListResponse
23, // 47: resource.ResourceStore.Watch:output_type -> resource.WatchEvent
16, // 48: resource.ResourceIndex.Read:output_type -> resource.ReadResponse
25, // 49: resource.ResourceIndex.History:output_type -> resource.HistoryResponse
28, // 50: resource.ResourceIndex.Origin:output_type -> resource.OriginResponse
32, // 51: resource.BlobStore.PutBlob:output_type -> resource.PutBlobResponse
34, // 52: resource.BlobStore.GetBlob:output_type -> resource.GetBlobResponse
30, // 53: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse
42, // [42:54] is the sub-list for method output_type
30, // [30:42] is the sub-list for method input_type
30, // [30:30] is the sub-list for extension type_name
30, // [30:30] is the sub-list for extension extendee
0, // [0:30] is the sub-list for field type_name
}
func init() { file_resource_proto_init() }
+17 -18
View File
@@ -154,9 +154,9 @@ message ReadResponse {
// The label filtering requirements:
// https://github.com/kubernetes/kubernetes/blob/v1.30.1/staging/src/k8s.io/apimachinery/pkg/labels/selector.go#L141
message Requirement {
string key = 1;
string key = 1;
string operator = 2; // See https://github.com/kubernetes/kubernetes/blob/v1.30.1/staging/src/k8s.io/apimachinery/pkg/selection/operator.go#L21
repeated string values = 3; // typically one value, but depends on the operator
repeated string values = 3; // typically one value, but depends on the operator
}
message Sort {
@@ -164,12 +164,12 @@ message Sort {
ASC = 0;
DESC = 1;
}
string field = 1;
string field = 1;
Order order = 2;
}
message ListOptions {
// Namespace+Group+Resource+etc
// Group+Namespace+Resource (not name)
ResourceKey key = 1;
// (best effort) Match label
@@ -177,11 +177,10 @@ message ListOptions {
// to the resutls agin in the client. That time with the full field selector
repeated Requirement labels = 2;
// TODO (later!) once we have a blob > search doc
// Match fields (not yet supported)
// metadata.name
// metadata.namespace
// repeated Requirement fields = 3;
// (best effort) fields matcher
// Allowed to send more results than actually match because the filter will be appled
// to the resutls agin in the client. That time with the full field selector
repeated Requirement fields = 3;
}
enum ResourceVersionMatch {
@@ -217,16 +216,16 @@ message ListResponse {
int64 resource_version = 3;
// remainingItemCount is the number of subsequent items in the list which are not included in this
// list response. If the list request contained label or field selectors, then the number of
// remaining items is unknown and the field will be left unset and omitted during serialization.
// If the list is complete (either because it is not chunking or because this is the last chunk),
// then there are no more remaining items and this field will be left unset and omitted during
// serialization.
// list response. If the list request contained label or field selectors, then the number of
// remaining items is unknown and the field will be left unset and omitted during serialization.
// If the list is complete (either because it is not chunking or because this is the last chunk),
// then there are no more remaining items and this field will be left unset and omitted during
// serialization.
//
// The intended use of the remainingItemCount is *estimating* the size of a collection. Clients
// should not rely on the remainingItemCount to be set or to be exact.
// +optional
int64 remaining_item_count = 4; // 0 won't be set either (no next page token)
// The intended use of the remainingItemCount is *estimating* the size of a collection. Clients
// should not rely on the remainingItemCount to be set or to be exact.
// +optional
int64 remaining_item_count = 4; // 0 won't be set either (no next page token)
}
message WatchRequest {
+5 -7
View File
@@ -22,13 +22,11 @@ import (
// Package-level errors.
var (
ErrNotFound = errors.New("entity not found")
ErrOptimisticLockingFailed = errors.New("optimistic locking failed")
ErrUserNotFoundInContext = errors.New("user not found in context")
ErrUnableToReadResourceJSON = errors.New("unable to read resource json")
ErrNextPageTokenNotSupported = errors.New("nextPageToken not yet supported")
ErrLimitNotSupported = errors.New("limit not yet supported")
ErrNotImplementedYet = errors.New("not implemented yet")
ErrNotFound = errors.New("entity not found")
ErrOptimisticLockingFailed = errors.New("optimistic locking failed")
ErrUserNotFoundInContext = errors.New("user not found in context")
ErrUnableToReadResourceJSON = errors.New("unable to read resource json")
ErrNotImplementedYet = errors.New("not implemented yet")
)
// ResourceServer implements all services
+38 -12
View File
@@ -2,7 +2,6 @@ package resource
import (
"context"
"embed"
"encoding/json"
"fmt"
"os"
@@ -21,6 +20,7 @@ import (
func TestSimpleServer(t *testing.T) {
testUserA := &identity.StaticRequester{
Namespace: identity.NamespaceUser,
Login: "testuser",
UserID: 123,
UserUID: "u123",
OrgRole: identity.RoleAdmin,
@@ -38,7 +38,10 @@ func TestSimpleServer(t *testing.T) {
Metadata: fileblob.MetadataDontWrite, // skip
})
require.NoError(t, err)
<<<<<<< HEAD
=======
>>>>>>> origin/resource-store-bridge
fmt.Printf("ROOT: %s\n\n", tmp)
}
store, err := NewCDKBackend(ctx, CDKBackendOptions{
@@ -52,7 +55,30 @@ func TestSimpleServer(t *testing.T) {
require.NoError(t, err)
t.Run("playlist happy CRUD paths", func(t *testing.T) {
raw := testdata(t, "01_create_playlist.json")
raw := []byte(`{
"apiVersion": "playlist.grafana.app/v0alpha1",
"kind": "Playlist",
"metadata": {
"name": "fdgsv37qslr0ga",
"namespace": "default",
"annotations": {
"grafana.app/originName": "elsewhere",
"grafana.app/originPath": "path/to/item",
"grafana.app/originTimestamp": "2024-02-02T00:00:00Z"
}
},
"spec": {
"title": "hello",
"interval": "5m",
"items": [
{
"type": "dashboard_by_uid",
"value": "vmie2cmWz"
}
]
}
}`)
key := &ResourceKey{
Group: "playlist.grafana.app",
Resource: "rrrr", // can be anything :(
@@ -131,15 +157,15 @@ func TestSimpleServer(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, found.Status)
require.Equal(t, int32(404), found.Status.Code)
// And the deleted value should not be in the results
all, err = server.List(ctx, &ListRequest{Options: &ListOptions{
Key: &ResourceKey{
Group: key.Group,
Resource: key.Resource,
},
}})
require.NoError(t, err)
require.Len(t, all.Items, 0) // empty
})
}
//go:embed testdata/*
var testdataFS embed.FS
func testdata(t *testing.T, filename string) []byte {
t.Helper()
b, err := testdataFS.ReadFile(`testdata/` + filename)
require.NoError(t, err)
return b
}
+7
View File
@@ -2,7 +2,10 @@ package playlist
import (
"encoding/json"
<<<<<<< HEAD
"fmt"
=======
>>>>>>> origin/resource-store-bridge
"testing"
"github.com/stretchr/testify/require"
@@ -35,7 +38,11 @@ func TestIntegrationFoldersApp(t *testing.T) {
v1Disco, err := json.MarshalIndent(resources, "", " ")
require.NoError(t, err)
<<<<<<< HEAD
fmt.Printf("%s", string(v1Disco))
=======
//fmt.Printf("%s", string(v1Disco))
>>>>>>> origin/resource-store-bridge
require.JSONEq(t, `{
"kind": "APIResourceList",
"apiVersion": "v1",
+77
View File
@@ -0,0 +1,77 @@
package graphite
import (
"context"
"fmt"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var (
_ backend.AdmissionHandler = (*Service)(nil)
)
// ValidateAdmission implements backend.AdmissionHandler.
func (s *Service) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
rsp, err := s.MutateAdmission(ctx, req)
if rsp != nil {
return &backend.ValidationResponse{
Allowed: rsp.Allowed,
Result: rsp.Result,
Warnings: rsp.Warnings,
}, err
}
return nil, err
}
// MutateAdmission implements backend.AdmissionHandler.
func (s *Service) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
expected := (&backend.DataSourceInstanceSettings{}).GVK()
if req.Kind.Kind != expected.Kind && req.Kind.Group != expected.Group {
return getBadRequest("expected DataSourceInstanceSettings protobuf payload"), nil
}
// Convert the payload from protobuf to an SDK struct
settings, err := backend.DataSourceInstanceSettingsFromProto(req.ObjectBytes, "")
if err != nil {
return nil, err
}
if settings == nil {
return getBadRequest("missing datasource settings"), nil
}
switch settings.APIVersion {
case "", "v0alpha1":
// OK!
default:
return getBadRequest(fmt.Sprintf("expected apiVersion: v0alpha1, found: %s", settings.APIVersion)), nil
}
if settings.URL != "" {
return getBadRequest("unsupported URL value"), nil
}
pb, err := backend.DataSourceInstanceSettingsToProtoBytes(settings)
return &backend.MutationResponse{
Allowed: true,
ObjectBytes: pb,
}, err
}
// ConvertObject implements backend.AdmissionHandler.
func (s *Service) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
return nil, fmt.Errorf("not implemented")
}
func getBadRequest(msg string) *backend.MutationResponse {
return &backend.MutationResponse{
Allowed: false,
Result: &backend.StatusResult{
Status: "Failure",
Message: msg,
Reason: string(metav1.StatusReasonBadRequest),
Code: http.StatusBadRequest,
},
}
}