mirror of https://github.com/grafana/grafana
Zanzana: Implement initial check and list with schema for generic resources (#95056)
* Implement initial check with schema for generic resources * Implement List and add tests * Add namespace type and change to folder_resource name * Handle namespace grants for typed resources * Run tests as integration tests * Add support for verb in list requestspull/95407/head
parent
e894b19c1a
commit
bdbe12e980
@ -0,0 +1,61 @@ |
||||
module resource |
||||
|
||||
type namespace |
||||
relations |
||||
define view: [user, team#member, role#assignee] or edit |
||||
define edit: [user, team#member, role#assignee] or admin |
||||
define admin: [user, team#member, role#assignee] |
||||
|
||||
define read: [user, team#member, role#assignee] or view |
||||
define create: [user, team#member, role#assignee] or edit |
||||
define write: [user, team#member, role#assignee] or edit |
||||
define delete: [user, team#member, role#assignee] or edit |
||||
define permissions_read: [user, team#member, role#assignee] or admin |
||||
define permissions_write: [user, team#member, role#assignee] or admin |
||||
|
||||
type folder2 |
||||
relations |
||||
define parent: [folder2] |
||||
|
||||
# Action sets |
||||
define view: [user, team#member, role#assignee] or edit |
||||
define edit: [user, team#member, role#assignee] or admin |
||||
define admin: [user, team#member, role#assignee] |
||||
|
||||
define read: [user, team#member, role#assignee] or view or read from parent |
||||
define create: [user, team#member, role#assignee] or edit or create from parent |
||||
define write: [user, team#member, role#assignee] or edit or write from parent |
||||
define delete: [user, team#member, role#assignee] or edit or delete from parent |
||||
define permissions_read: [user, team#member, role#assignee] or admin or permissions_read from parent |
||||
define permissions_write: [user, team#member, role#assignee] or admin or permissions_write from parent |
||||
|
||||
type folder_resource |
||||
relations |
||||
define view: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit |
||||
define edit: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin |
||||
define admin: [user with group_filter, team#member with group_filter, role#assignee with group_filter] |
||||
|
||||
define read: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or view |
||||
define create: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit |
||||
define write: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit |
||||
define delete: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit |
||||
define permissions_read: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin |
||||
define permissions_write: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin |
||||
|
||||
type resource |
||||
relations |
||||
define view: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit |
||||
define edit: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin |
||||
define admin: [user with group_filter, team#member with group_filter, role#assignee with group_filter] |
||||
|
||||
define read: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or view |
||||
define create: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit |
||||
define write: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit |
||||
define delete: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit |
||||
define permissions_read: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin |
||||
define permissions_write: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin |
||||
|
||||
condition group_filter(requested_group: string, resource_group: string) { |
||||
resource_group == requested_group |
||||
} |
||||
|
@ -0,0 +1,130 @@ |
||||
package server |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1" |
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1" |
||||
"google.golang.org/protobuf/types/known/structpb" |
||||
) |
||||
|
||||
func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) { |
||||
ctx, span := tracer.Start(ctx, "authzServer.Check") |
||||
defer span.End() |
||||
|
||||
if info, ok := typeInfo(r.GetGroup(), r.GetResource()); ok { |
||||
return s.checkTyped(ctx, r, info) |
||||
} |
||||
return s.checkGeneric(ctx, r) |
||||
} |
||||
|
||||
func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info TypeInfo) (*authzv1.CheckResponse, error) { |
||||
relation := mapping[r.GetVerb()] |
||||
|
||||
// 1. check if subject has direct access to resource
|
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
TupleKey: &openfgav1.CheckRequestTupleKey{ |
||||
User: r.GetSubject(), |
||||
Relation: relation, |
||||
Object: newTypedIdent(info.typ, r.GetName()), |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if res.GetAllowed() { |
||||
return &authzv1.CheckResponse{Allowed: true}, nil |
||||
} |
||||
|
||||
// 2. check if subject has access through namespace
|
||||
res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
TupleKey: &openfgav1.CheckRequestTupleKey{ |
||||
User: r.GetSubject(), |
||||
Relation: relation, |
||||
Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()), |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil |
||||
} |
||||
|
||||
func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) { |
||||
relation := mapping[r.GetVerb()] |
||||
// 1. check if subject has direct access to resource
|
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
TupleKey: &openfgav1.CheckRequestTupleKey{ |
||||
User: r.GetSubject(), |
||||
Relation: relation, |
||||
Object: newResourceIdent(r.GetGroup(), r.GetResource(), r.GetName()), |
||||
}, |
||||
Context: &structpb.Struct{ |
||||
Fields: map[string]*structpb.Value{ |
||||
"requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())), |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
if err != nil { |
||||
// FIXME: wrap error
|
||||
return nil, err |
||||
} |
||||
|
||||
if res.GetAllowed() { |
||||
return &authzv1.CheckResponse{Allowed: true}, nil |
||||
} |
||||
|
||||
// 2. check if subject has access through namespace
|
||||
res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
TupleKey: &openfgav1.CheckRequestTupleKey{ |
||||
User: r.GetSubject(), |
||||
Relation: relation, |
||||
Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()), |
||||
}, |
||||
}) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if res.GetAllowed() { |
||||
return &authzv1.CheckResponse{Allowed: true}, nil |
||||
} |
||||
|
||||
if r.Folder == "" { |
||||
return &authzv1.CheckResponse{Allowed: false}, nil |
||||
} |
||||
|
||||
// 3. check if subject has access as a sub resource for the folder
|
||||
res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
TupleKey: &openfgav1.CheckRequestTupleKey{ |
||||
User: r.GetSubject(), |
||||
Relation: relation, |
||||
Object: newFolderResourceIdent(r.GetGroup(), r.GetResource(), r.GetFolder()), |
||||
}, |
||||
Context: &structpb.Struct{ |
||||
Fields: map[string]*structpb.Value{ |
||||
"requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())), |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil |
||||
} |
@ -0,0 +1,96 @@ |
||||
package server |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils" |
||||
) |
||||
|
||||
func testCheck(t *testing.T, server *Server) { |
||||
newRead := func(subject, group, resource, folder, name string) *authzv1.CheckRequest { |
||||
return &authzv1.CheckRequest{ |
||||
// FIXME: namespace should map to store
|
||||
// Namespace: storeID,
|
||||
Subject: subject, |
||||
Verb: utils.VerbGet, |
||||
Group: group, |
||||
Resource: resource, |
||||
Name: name, |
||||
Folder: folder, |
||||
} |
||||
} |
||||
|
||||
t.Run("user:1 should only be able to read resource:dashboards.grafana.app/dashboards/1", func(t *testing.T) { |
||||
res, err := server.Check(context.Background(), newRead("user:1", dashboardGroup, dashboardResource, "1", "1")) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAllowed()) |
||||
|
||||
// sanity check
|
||||
res, err = server.Check(context.Background(), newRead("user:1", dashboardGroup, dashboardResource, "1", "2")) |
||||
require.NoError(t, err) |
||||
assert.False(t, res.GetAllowed()) |
||||
}) |
||||
|
||||
t.Run("user:2 should be able to read resource:dashboards.grafana.app/dashboards/1 through namespace", func(t *testing.T) { |
||||
res, err := server.Check(context.Background(), newRead("user:2", dashboardGroup, dashboardResource, "1", "1")) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAllowed()) |
||||
}) |
||||
|
||||
t.Run("user:3 should be able to read resource:dashboards.grafana.app/dashboards/1 with set relation", func(t *testing.T) { |
||||
res, err := server.Check(context.Background(), newRead("user:3", dashboardGroup, dashboardResource, "1", "1")) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAllowed()) |
||||
|
||||
// sanity check
|
||||
res, err = server.Check(context.Background(), newRead("user:3", dashboardGroup, dashboardResource, "1", "2")) |
||||
require.NoError(t, err) |
||||
assert.False(t, res.GetAllowed()) |
||||
}) |
||||
|
||||
t.Run("user:4 should be able to read all dashboards.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) { |
||||
res, err := server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "1", "1")) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAllowed()) |
||||
|
||||
res, err = server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "3", "2")) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAllowed()) |
||||
|
||||
// sanity check
|
||||
res, err = server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "1", "2")) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAllowed()) |
||||
|
||||
res, err = server.Check(context.Background(), newRead("user:4", dashboardGroup, dashboardResource, "2", "2")) |
||||
require.NoError(t, err) |
||||
assert.False(t, res.GetAllowed()) |
||||
}) |
||||
|
||||
t.Run("user:5 should be able to read resource:dashboards.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) { |
||||
res, err := server.Check(context.Background(), newRead("user:5", dashboardGroup, dashboardResource, "1", "1")) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAllowed()) |
||||
}) |
||||
|
||||
t.Run("user:6 should be able to read folder 1 ", func(t *testing.T) { |
||||
res, err := server.Check(context.Background(), newRead("user:6", folderGroup, folderResource, "", "1")) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAllowed()) |
||||
}) |
||||
|
||||
t.Run("user:7 should be able to read folder one through namespace access", func(t *testing.T) { |
||||
res, err := server.Check(context.Background(), newRead("user:7", folderGroup, folderResource, "", "1")) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAllowed()) |
||||
|
||||
res, err = server.Check(context.Background(), newRead("user:7", folderGroup, folderResource, "", "10")) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAllowed()) |
||||
}) |
||||
} |
@ -0,0 +1,146 @@ |
||||
package server |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"strings" |
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1" |
||||
"google.golang.org/protobuf/types/known/structpb" |
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils" |
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" |
||||
) |
||||
|
||||
func (s *Server) List(ctx context.Context, r *authzextv1.ListRequest) (*authzextv1.ListResponse, error) { |
||||
ctx, span := tracer.Start(ctx, "authzServer.List") |
||||
defer span.End() |
||||
|
||||
if info, ok := typeInfo(r.GetGroup(), r.GetResource()); ok { |
||||
return s.listTyped(ctx, r, info) |
||||
} |
||||
|
||||
return s.listGeneric(ctx, r) |
||||
} |
||||
func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info TypeInfo) (*authzextv1.ListResponse, error) { |
||||
relation := mapping[r.GetVerb()] |
||||
|
||||
// 1. check if subject has access through namespace because then they can read all of them
|
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
TupleKey: &openfgav1.CheckRequestTupleKey{ |
||||
User: r.GetSubject(), |
||||
Relation: relation, |
||||
Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()), |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if res.GetAllowed() { |
||||
return &authzextv1.ListResponse{All: true}, nil |
||||
} |
||||
|
||||
// 2. List all resources user has access too
|
||||
listRes, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
Type: info.typ, |
||||
Relation: mapping[utils.VerbGet], |
||||
User: r.GetSubject(), |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &authzextv1.ListResponse{ |
||||
Items: typedObjects(info.typ, listRes.GetObjects()), |
||||
}, nil |
||||
} |
||||
|
||||
func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*authzextv1.ListResponse, error) { |
||||
relation := mapping[r.GetVerb()] |
||||
|
||||
// 1. check if subject has access through namespace because then they can read all of them
|
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
TupleKey: &openfgav1.CheckRequestTupleKey{ |
||||
User: r.GetSubject(), |
||||
Relation: relation, |
||||
Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()), |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if res.Allowed { |
||||
return &authzextv1.ListResponse{All: true}, nil |
||||
} |
||||
|
||||
// 2. List all folders subject has access to resource type in
|
||||
folders, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
Type: "folder_resource", |
||||
Relation: relation, |
||||
User: r.GetSubject(), |
||||
Context: &structpb.Struct{ |
||||
Fields: map[string]*structpb.Value{ |
||||
"requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())), |
||||
}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// 3. List all resource directly assigned to subject
|
||||
direct, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
Type: "resource", |
||||
Relation: relation, |
||||
User: r.GetSubject(), |
||||
Context: &structpb.Struct{ |
||||
Fields: map[string]*structpb.Value{ |
||||
"requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())), |
||||
}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &authzextv1.ListResponse{ |
||||
Folders: folderObject(r.GetGroup(), r.GetResource(), folders.GetObjects()), |
||||
Items: directObjects(r.GetGroup(), r.GetResource(), direct.GetObjects()), |
||||
}, nil |
||||
} |
||||
|
||||
func typedObjects(typ string, objects []string) []string { |
||||
prefix := fmt.Sprintf("%s:", typ) |
||||
for i := range objects { |
||||
objects[i] = strings.TrimPrefix(objects[i], prefix) |
||||
} |
||||
return objects |
||||
} |
||||
|
||||
func directObjects(group, resource string, objects []string) []string { |
||||
prefix := fmt.Sprintf("%s:%s/%s/", resourceType, group, resource) |
||||
for i := range objects { |
||||
objects[i] = strings.TrimPrefix(objects[i], prefix) |
||||
} |
||||
return objects |
||||
} |
||||
|
||||
func folderObject(group, resource string, objects []string) []string { |
||||
prefix := fmt.Sprintf("%s:%s/%s/", folderResourceType, group, resource) |
||||
for i := range objects { |
||||
objects[i] = strings.TrimPrefix(objects[i], prefix) |
||||
} |
||||
return objects |
||||
} |
@ -0,0 +1,83 @@ |
||||
package server |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils" |
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" |
||||
) |
||||
|
||||
func testList(t *testing.T, server *Server) { |
||||
newList := func(subject, group, resource string) *authzextv1.ListRequest { |
||||
return &authzextv1.ListRequest{ |
||||
// FIXME: namespace should map to store
|
||||
// Namespace: storeID,
|
||||
Verb: utils.VerbList, |
||||
Subject: subject, |
||||
Group: group, |
||||
Resource: resource, |
||||
} |
||||
} |
||||
|
||||
t.Run("user:1 should list resource:dashboards.grafana.app/dashboards/1", func(t *testing.T) { |
||||
res, err := server.List(context.Background(), newList("user:1", dashboardGroup, dashboardResource)) |
||||
require.NoError(t, err) |
||||
assert.Len(t, res.GetItems(), 1) |
||||
assert.Len(t, res.GetFolders(), 0) |
||||
assert.Equal(t, res.GetItems()[0], "1") |
||||
}) |
||||
|
||||
t.Run("user:2 should be able to list all through group", func(t *testing.T) { |
||||
res, err := server.List(context.Background(), newList("user:2", dashboardGroup, dashboardResource)) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.GetAll()) |
||||
assert.Len(t, res.GetItems(), 0) |
||||
assert.Len(t, res.GetFolders(), 0) |
||||
}) |
||||
|
||||
t.Run("user:3 should be able to list resource:dashboards.grafana.app/dashboards/1 with set relation", func(t *testing.T) { |
||||
res, err := server.List(context.Background(), newList("user:3", dashboardGroup, dashboardResource)) |
||||
require.NoError(t, err) |
||||
|
||||
assert.Len(t, res.GetItems(), 1) |
||||
assert.Len(t, res.GetFolders(), 0) |
||||
assert.Equal(t, res.GetItems()[0], "1") |
||||
}) |
||||
|
||||
t.Run("user:4 should be able to list all dashboards.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) { |
||||
res, err := server.List(context.Background(), newList("user:4", dashboardGroup, dashboardResource)) |
||||
require.NoError(t, err) |
||||
assert.Len(t, res.GetItems(), 0) |
||||
assert.Len(t, res.GetFolders(), 2) |
||||
assert.Equal(t, res.GetFolders()[0], "1") |
||||
assert.Equal(t, res.GetFolders()[1], "3") |
||||
}) |
||||
|
||||
t.Run("user:5 should be get list all dashboards.grafana.app/dashboards in folder 1 with set relation", func(t *testing.T) { |
||||
res, err := server.List(context.Background(), newList("user:5", dashboardGroup, dashboardResource)) |
||||
require.NoError(t, err) |
||||
assert.Len(t, res.GetItems(), 0) |
||||
assert.Len(t, res.GetFolders(), 1) |
||||
assert.Equal(t, res.GetFolders()[0], "1") |
||||
}) |
||||
|
||||
t.Run("user:6 should be able to list folder 1", func(t *testing.T) { |
||||
res, err := server.List(context.Background(), newList("user:6", folderGroup, folderResource)) |
||||
require.NoError(t, err) |
||||
assert.Len(t, res.GetItems(), 1) |
||||
assert.Len(t, res.GetFolders(), 0) |
||||
assert.Equal(t, res.GetItems()[0], "1") |
||||
}) |
||||
|
||||
t.Run("user:7 should be able to list all folders", func(t *testing.T) { |
||||
res, err := server.List(context.Background(), newList("user:7", folderGroup, folderResource)) |
||||
require.NoError(t, err) |
||||
assert.Len(t, res.GetItems(), 0) |
||||
assert.Len(t, res.GetFolders(), 0) |
||||
assert.True(t, res.GetAll()) |
||||
}) |
||||
} |
@ -0,0 +1,131 @@ |
||||
package server |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1" |
||||
"github.com/stretchr/testify/require" |
||||
"google.golang.org/protobuf/types/known/structpb" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db" |
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/store" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/tests/testsuite" |
||||
) |
||||
|
||||
const ( |
||||
dashboardGroup = "dashboard.grafana.app" |
||||
dashboardResource = "dashboards" |
||||
|
||||
folderGroup = "folder.grafana.app" |
||||
folderResource = "folders" |
||||
) |
||||
|
||||
func TestMain(m *testing.M) { |
||||
testsuite.Run(m) |
||||
} |
||||
|
||||
func TestIntegrationServer(t *testing.T) { |
||||
if testing.Short() { |
||||
t.Skip("skipping integration test") |
||||
} |
||||
|
||||
testDB, cfg := db.InitTestDBWithCfg(t) |
||||
// Hack to skip these tests on mysql 5.7
|
||||
if testDB.GetDialect().DriverName() == migrator.MySQL { |
||||
if supported, err := testDB.RecursiveQueriesAreSupported(); !supported || err != nil { |
||||
t.Skip("skipping integration test") |
||||
} |
||||
} |
||||
|
||||
srv := setup(t, testDB, cfg) |
||||
t.Run("test check", func(t *testing.T) { |
||||
testCheck(t, srv) |
||||
}) |
||||
|
||||
t.Run("test list", func(t *testing.T) { |
||||
testList(t, srv) |
||||
}) |
||||
} |
||||
|
||||
func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { |
||||
t.Helper() |
||||
store, err := store.NewEmbeddedStore(cfg, testDB, log.NewNopLogger()) |
||||
require.NoError(t, err) |
||||
openfga, err := NewOpenFGA(&cfg.Zanzana, store, log.NewNopLogger()) |
||||
require.NoError(t, err) |
||||
|
||||
srv, err := NewAuthz(openfga) |
||||
require.NoError(t, err) |
||||
|
||||
// seed tuples
|
||||
_, err = openfga.Write(context.Background(), &openfgav1.WriteRequest{ |
||||
StoreId: srv.storeID, |
||||
AuthorizationModelId: srv.modelID, |
||||
Writes: &openfgav1.WriteRequestWrites{ |
||||
TupleKeys: []*openfgav1.TupleKey{ |
||||
newResourceTuple("user:1", "read", dashboardGroup, dashboardResource, "1"), |
||||
newNamespaceResourceTuple("user:2", "read", dashboardGroup, dashboardResource), |
||||
newResourceTuple("user:3", "view", dashboardGroup, dashboardResource, "1"), |
||||
newFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "1"), |
||||
newFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "3"), |
||||
newFolderResourceTuple("user:5", "view", dashboardGroup, dashboardResource, "1"), |
||||
newFolderTuple("user:6", "read", "1"), |
||||
newNamespaceResourceTuple("user:7", "read", folderGroup, folderResource), |
||||
}, |
||||
}, |
||||
}) |
||||
require.NoError(t, err) |
||||
return srv |
||||
} |
||||
|
||||
func newResourceTuple(subject, relation, group, resource, name string) *openfgav1.TupleKey { |
||||
return &openfgav1.TupleKey{ |
||||
User: subject, |
||||
Relation: relation, |
||||
Object: newResourceIdent(group, resource, name), |
||||
Condition: &openfgav1.RelationshipCondition{ |
||||
Name: "group_filter", |
||||
Context: &structpb.Struct{ |
||||
Fields: map[string]*structpb.Value{ |
||||
"resource_group": structpb.NewStringValue(formatGroupResource(group, resource)), |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
func newFolderResourceTuple(subject, relation, group, resource, folder string) *openfgav1.TupleKey { |
||||
return &openfgav1.TupleKey{ |
||||
User: subject, |
||||
Relation: relation, |
||||
Object: newFolderResourceIdent(group, resource, folder), |
||||
Condition: &openfgav1.RelationshipCondition{ |
||||
Name: "group_filter", |
||||
Context: &structpb.Struct{ |
||||
Fields: map[string]*structpb.Value{ |
||||
"resource_group": structpb.NewStringValue(formatGroupResource(group, resource)), |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
func newNamespaceResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey { |
||||
return &openfgav1.TupleKey{ |
||||
User: subject, |
||||
Relation: relation, |
||||
Object: newNamespaceResourceIdent(group, resource), |
||||
} |
||||
} |
||||
|
||||
func newFolderTuple(subject, relation, name string) *openfgav1.TupleKey { |
||||
return &openfgav1.TupleKey{ |
||||
User: subject, |
||||
Relation: relation, |
||||
Object: newTypedIdent("folder2", name), |
||||
} |
||||
} |
Loading…
Reference in new issue