merge upstream
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
@@ -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),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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()},
|
||||
}),
|
||||
}})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}, {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
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: ???,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}}
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,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,
|
||||
}}),
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user