mirror of https://github.com/grafana/grafana
AccessControl: Add accesscontrol metadata to datasources DTOs (#42675)
* AccessControl: Provide scope to frontend * Covering datasources with accesscontrol metadata * Write benchmark tests for GetResourcesMetadata * Add accesscontrol util and interface * Add the hasPermissionInMetadata function in the frontend access control code * Use IsDisabled rather that performing a feature toggle check Co-authored-by: Karl Persson <kalle.persson@grafana.com>pull/43144/head
parent
2b1ed43cb2
commit
c7cabdfd6f
@ -0,0 +1,14 @@ |
||||
import { KeyValue } from '.'; |
||||
|
||||
/** |
||||
* With FGAC, the backend will return additional access control metadata to objects. |
||||
* These metadata will contain user permissions associated to a given resource. |
||||
* |
||||
* For example: |
||||
* { |
||||
* accessControl: { "datasources:read": true, "datasources:write": true } |
||||
* } |
||||
*/ |
||||
export interface WithAccessControlMetadata { |
||||
accessControl?: KeyValue<boolean>; |
||||
} |
@ -0,0 +1,65 @@ |
||||
// go:build integration
|
||||
package accesscontrol |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func setupTestEnv(b *testing.B, resourceCount, permissionPerResource int) ([]*Permission, map[string]bool) { |
||||
res := make([]*Permission, resourceCount*permissionPerResource) |
||||
ids := make(map[string]bool, resourceCount) |
||||
|
||||
for r := 0; r < resourceCount; r++ { |
||||
for p := 0; p < permissionPerResource; p++ { |
||||
perm := Permission{Action: fmt.Sprintf("resources:action%v", p), Scope: fmt.Sprintf("resources:id:%v", r)} |
||||
id := r*permissionPerResource + p |
||||
res[id] = &perm |
||||
} |
||||
ids[fmt.Sprintf("%d", r)] = true |
||||
} |
||||
|
||||
return res, ids |
||||
} |
||||
|
||||
func benchGetMetadata(b *testing.B, resourceCount, permissionPerResource int) { |
||||
permissions, ids := setupTestEnv(b, resourceCount, permissionPerResource) |
||||
b.ResetTimer() |
||||
|
||||
var metadata map[string]Metadata |
||||
var err error |
||||
for n := 0; n < b.N; n++ { |
||||
metadata, err = GetResourcesMetadata(context.Background(), permissions, "resources", ids) |
||||
require.NoError(b, err) |
||||
assert.Len(b, metadata, resourceCount) |
||||
for _, resourceMetadata := range metadata { |
||||
assert.Len(b, resourceMetadata, permissionPerResource) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Lots of permissions
|
||||
func BenchmarkGetResourcesMetadata_10_1000(b *testing.B) { benchGetMetadata(b, 10, 1000) } // ~0.0017s/op
|
||||
func BenchmarkGetResourcesMetadata_10_10000(b *testing.B) { benchGetMetadata(b, 10, 10000) } // ~0.016s/op
|
||||
func BenchmarkGetResourcesMetadata_10_100000(b *testing.B) { benchGetMetadata(b, 10, 100000) } // ~0.17s/op
|
||||
func BenchmarkGetResourcesMetadata_10_1000000(b *testing.B) { |
||||
if testing.Short() { |
||||
b.Skip("Skipping benchmark in short mode") |
||||
} |
||||
benchGetMetadata(b, 10, 1000000) |
||||
} // ~3.89s/op
|
||||
|
||||
// Lots of resources (worst case)
|
||||
func BenchmarkGetResourcesMetadata_1000_10(b *testing.B) { benchGetMetadata(b, 1000, 10) } // ~0,0023s/op
|
||||
func BenchmarkGetResourcesMetadata_10000_10(b *testing.B) { benchGetMetadata(b, 10000, 10) } // ~0.021s/op
|
||||
func BenchmarkGetResourcesMetadata_100000_10(b *testing.B) { benchGetMetadata(b, 100000, 10) } // ~0.22s/op
|
||||
func BenchmarkGetResourcesMetadata_1000000_10(b *testing.B) { |
||||
if testing.Short() { |
||||
b.Skip("Skipping benchmark in short mode") |
||||
} |
||||
benchGetMetadata(b, 1000000, 10) |
||||
} // ~2.8s/op
|
@ -0,0 +1,104 @@ |
||||
package accesscontrol |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestGetResourcesMetadata(t *testing.T) { |
||||
tests := []struct { |
||||
desc string |
||||
resource string |
||||
resourcesIDs map[string]bool |
||||
permissions []*Permission |
||||
expected map[string]Metadata |
||||
}{ |
||||
{ |
||||
desc: "Should return no permission for resources 1,2,3 given the user has no permission", |
||||
resource: "resources", |
||||
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true}, |
||||
expected: map[string]Metadata{}, |
||||
}, |
||||
{ |
||||
desc: "Should return no permission for resources 1,2,3 given the user has permissions for 4 only", |
||||
resource: "resources", |
||||
permissions: []*Permission{ |
||||
{Action: "resources:action1", Scope: Scope("resources", "id", "4")}, |
||||
{Action: "resources:action2", Scope: Scope("resources", "id", "4")}, |
||||
{Action: "resources:action3", Scope: Scope("resources", "id", "4")}, |
||||
}, |
||||
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true}, |
||||
expected: map[string]Metadata{}, |
||||
}, |
||||
{ |
||||
desc: "Should only return permissions for resources 1 and 2, given the user has no permissions for 3", |
||||
resource: "resources", |
||||
permissions: []*Permission{ |
||||
{Action: "resources:action1", Scope: Scope("resources", "id", "1")}, |
||||
{Action: "resources:action2", Scope: Scope("resources", "id", "2")}, |
||||
{Action: "resources:action3", Scope: Scope("resources", "id", "2")}, |
||||
}, |
||||
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true}, |
||||
expected: map[string]Metadata{ |
||||
"1": {"resources:action1": true}, |
||||
"2": {"resources:action2": true, "resources:action3": true}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "Should return permissions with global scopes for resources 1,2,3", |
||||
resource: "resources", |
||||
permissions: []*Permission{ |
||||
{Action: "resources:action4", Scope: Scope("resources", "id", "*")}, |
||||
{Action: "resources:action5", Scope: Scope("resources", "*")}, |
||||
{Action: "resources:action6", Scope: "*"}, |
||||
{Action: "resources:action1", Scope: Scope("resources", "id", "1")}, |
||||
{Action: "resources:action2", Scope: Scope("resources", "id", "2")}, |
||||
{Action: "resources:action3", Scope: Scope("resources", "id", "2")}, |
||||
}, |
||||
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true}, |
||||
expected: map[string]Metadata{ |
||||
"1": {"resources:action1": true, "resources:action4": true, "resources:action5": true, "resources:action6": true}, |
||||
"2": {"resources:action2": true, "resources:action3": true, "resources:action4": true, "resources:action5": true, "resources:action6": true}, |
||||
"3": {"resources:action4": true, "resources:action5": true, "resources:action6": true}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "Should correctly filter out irrelevant permissions for resources 1,2,3", |
||||
resource: "resources", |
||||
permissions: []*Permission{ |
||||
{Action: "resources:action1", Scope: Scope("resources", "id", "1")}, |
||||
{Action: "otherresources:action1", Scope: Scope("resources", "id", "1")}, |
||||
{Action: "resources:action2", Scope: Scope("otherresources", "id", "*")}, |
||||
{Action: "otherresources:action1", Scope: Scope("otherresources", "id", "*")}, |
||||
}, |
||||
resourcesIDs: map[string]bool{"1": true, "2": true, "3": true}, |
||||
expected: map[string]Metadata{ |
||||
"1": {"resources:action1": true, "otherresources:action1": true}, |
||||
}, |
||||
}, |
||||
{ |
||||
desc: "Should correctly handle permissions with multilayer scope", |
||||
resource: "resources:sub", |
||||
permissions: []*Permission{ |
||||
{Action: "resources:action1", Scope: Scope("resources", "sub", "id", "1")}, |
||||
{Action: "resources:action1", Scope: Scope("resources", "sub", "id", "123")}, |
||||
}, |
||||
resourcesIDs: map[string]bool{"1": true, "123": true}, |
||||
expected: map[string]Metadata{ |
||||
"1": {"resources:action1": true}, |
||||
"123": {"resources:action1": true}, |
||||
}, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.desc, func(t *testing.T) { |
||||
metadata, err := GetResourcesMetadata(context.Background(), tt.permissions, tt.resource, tt.resourcesIDs) |
||||
require.NoError(t, err) |
||||
|
||||
assert.EqualValues(t, tt.expected, metadata) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
import config from '../../core/config'; |
||||
|
||||
// addAccessControlQueryParam appends ?accesscontrol=true to a url when accesscontrol is enabled
|
||||
export function addAccessControlQueryParam(url: string): string { |
||||
if (!config.featureToggles['accesscontrol']) { |
||||
return url; |
||||
} |
||||
return url + '?accesscontrol=true'; |
||||
} |
Loading…
Reference in new issue