mirror of https://github.com/grafana/grafana
parent
5533b30135
commit
9f5581942f
@ -0,0 +1,149 @@ |
||||
package server |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1" |
||||
"google.golang.org/protobuf/types/known/structpb" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common" |
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1" |
||||
) |
||||
|
||||
func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) { |
||||
if info, ok := common.GetTypeInfo(r.GetGroup(), r.GetResource()); ok { |
||||
return s.batchCheckTyped(ctx, r, info) |
||||
} |
||||
|
||||
return s.batchCheckGeneric(ctx, r) |
||||
} |
||||
|
||||
func (s *Server) batchCheckTyped(ctx context.Context, r *authzextv1.BatchCheckRequest, info common.TypeInfo) (*authzextv1.BatchCheckResponse, error) { |
||||
relation := common.VerbMapping[r.GetVerb()] |
||||
|
||||
// 1. 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: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()), |
||||
}, |
||||
}) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if res.GetAllowed() { |
||||
return &authzextv1.BatchCheckResponse{All: true}, nil |
||||
} |
||||
|
||||
BatchRes := &authzextv1.BatchCheckResponse{ |
||||
Items: make(map[string]bool, len(r.Items)), |
||||
} |
||||
|
||||
// 2. check if subject has direct access to resources
|
||||
for _, i := range r.Items { |
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ |
||||
StoreId: s.storeID, |
||||
AuthorizationModelId: s.modelID, |
||||
TupleKey: &openfgav1.CheckRequestTupleKey{ |
||||
User: r.GetSubject(), |
||||
Relation: relation, |
||||
Object: common.NewTypedIdent(info.Type, i.GetName()), |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
BatchRes.Items[i.GetName()] = res.GetAllowed() |
||||
} |
||||
|
||||
return BatchRes, nil |
||||
} |
||||
|
||||
func (s *Server) batchCheckGeneric(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) { |
||||
relation := common.VerbMapping[r.GetVerb()] |
||||
|
||||
// 1. 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: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()), |
||||
}, |
||||
}) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if res.GetAllowed() { |
||||
return &authzextv1.BatchCheckResponse{All: true}, nil |
||||
} |
||||
|
||||
BatchRes := &authzextv1.BatchCheckResponse{ |
||||
Items: make(map[string]bool, len(r.Items)), |
||||
} |
||||
|
||||
for _, i := range r.Items { |
||||
// 2. 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: common.NewResourceIdent(r.GetGroup(), r.GetResource(), i.GetName()), |
||||
}, |
||||
Context: &structpb.Struct{ |
||||
Fields: map[string]*structpb.Value{ |
||||
"requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())), |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if res.GetAllowed() { |
||||
BatchRes.Items[i.GetName()] = true |
||||
continue |
||||
} |
||||
|
||||
if i.Folder == "" { |
||||
BatchRes.Items[i.GetName()] = false |
||||
continue |
||||
} |
||||
|
||||
// 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: common.NewFolderResourceIdent(r.GetGroup(), r.GetResource(), i.GetFolder()), |
||||
}, |
||||
Context: &structpb.Struct{ |
||||
Fields: map[string]*structpb.Value{ |
||||
"requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())), |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
BatchRes.Items[i.GetName()] = res.GetAllowed() |
||||
} |
||||
|
||||
return BatchRes, nil |
||||
} |
@ -0,0 +1,113 @@ |
||||
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 testBatchCheck(t *testing.T, server *Server) { |
||||
newBatch := func(subject, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest { |
||||
return &authzextv1.BatchCheckRequest{ |
||||
// FIXME: namespace should map to store
|
||||
// Namespace: storeID,
|
||||
Subject: subject, |
||||
Verb: utils.VerbGet, |
||||
Group: group, |
||||
Resource: resource, |
||||
Items: items, |
||||
} |
||||
} |
||||
|
||||
t.Run("user:1 should only be able to read resource:dashboards.grafana.app/dashboards/1", func(t *testing.T) { |
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:1", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ |
||||
{Name: "1", Folder: "1"}, |
||||
{Name: "2", Folder: "2"}, |
||||
})) |
||||
require.NoError(t, err) |
||||
assert.False(t, res.All) |
||||
require.Len(t, res.Items, 2) |
||||
|
||||
assert.True(t, res.Items["1"]) |
||||
assert.False(t, res.Items["2"]) |
||||
}) |
||||
|
||||
t.Run("user:2 should be able to read resource:dashboards.grafana.app/dashboards/{1,2} through namespace", func(t *testing.T) { |
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:2", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ |
||||
{Name: "1", Folder: "1"}, |
||||
{Name: "2", Folder: "2"}, |
||||
})) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.All) |
||||
assert.Len(t, res.Items, 0) |
||||
}) |
||||
|
||||
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.BatchCheck(context.Background(), newBatch("user:3", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ |
||||
{Name: "1", Folder: "1"}, |
||||
{Name: "2", Folder: "2"}, |
||||
})) |
||||
require.NoError(t, err) |
||||
assert.False(t, res.All) |
||||
require.Len(t, res.Items, 2) |
||||
|
||||
assert.True(t, res.Items["1"]) |
||||
assert.False(t, res.Items["2"]) |
||||
}) |
||||
|
||||
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.BatchCheck(context.Background(), newBatch("user:4", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ |
||||
{Name: "1", Folder: "1"}, |
||||
{Name: "2", Folder: "3"}, |
||||
{Name: "3", Folder: "2"}, |
||||
})) |
||||
require.NoError(t, err) |
||||
assert.False(t, res.All) |
||||
require.Len(t, res.Items, 3) |
||||
|
||||
assert.True(t, res.Items["1"]) |
||||
assert.True(t, res.Items["2"]) |
||||
assert.False(t, res.Items["3"]) |
||||
}) |
||||
|
||||
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.BatchCheck(context.Background(), newBatch("user:5", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{ |
||||
{Name: "1", Folder: "1"}, |
||||
{Name: "2", Folder: "2"}, |
||||
})) |
||||
require.NoError(t, err) |
||||
assert.False(t, res.All) |
||||
require.Len(t, res.Items, 2) |
||||
|
||||
assert.True(t, res.Items["1"]) |
||||
assert.False(t, res.Items["2"]) |
||||
}) |
||||
|
||||
t.Run("user:6 should be able to read folder 1", func(t *testing.T) { |
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:6", folderGroup, folderResource, []*authzextv1.BatchCheckItem{ |
||||
{Name: "1"}, |
||||
{Name: "2"}, |
||||
})) |
||||
require.NoError(t, err) |
||||
assert.False(t, res.All) |
||||
require.Len(t, res.Items, 2) |
||||
|
||||
assert.True(t, res.Items["1"]) |
||||
assert.False(t, res.Items["2"]) |
||||
}) |
||||
|
||||
t.Run("user:7 should be able to read folder {1,2} through namespace access", func(t *testing.T) { |
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:7", folderGroup, folderResource, []*authzextv1.BatchCheckItem{ |
||||
{Name: "1"}, |
||||
{Name: "2"}, |
||||
})) |
||||
require.NoError(t, err) |
||||
assert.True(t, res.All) |
||||
require.Len(t, res.Items, 0) |
||||
}) |
||||
} |
Loading…
Reference in new issue