Implement BatchCheck functionality in LegacyAccessClient and update related proto definitions
- Added BatchCheck method to LegacyAccessClient for handling batch authorization checks. - Updated proto definitions to remove BatchCheckRequest and BatchCheckResponse messages, replacing them with a new structure. - Adjusted related client and server implementations to align with the new BatchCheck structure. - Modified tests to validate the new BatchCheck functionality and ensure proper integration with existing authorization logic.
This commit is contained in:
@@ -167,3 +167,99 @@ func (c *LegacyAccessClient) Compile(ctx context.Context, id claims.AuthInfo, re
|
||||
return check(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, name))
|
||||
}, claims.NoopZookie{}, nil
|
||||
}
|
||||
|
||||
func (c *LegacyAccessClient) BatchCheck(ctx context.Context, id claims.AuthInfo, req claims.BatchCheckRequest) (claims.BatchCheckResponse, error) {
|
||||
ident, ok := id.(identity.Requester)
|
||||
if !ok {
|
||||
return claims.BatchCheckResponse{}, errors.New("expected identity.Requester for legacy access control")
|
||||
}
|
||||
|
||||
results := make(map[string]claims.BatchCheckResult, len(req.Checks))
|
||||
|
||||
// Cache checkers by action to avoid recreating them for each check
|
||||
checkerCache := make(map[string]func(scopes ...string) bool)
|
||||
|
||||
for _, check := range req.Checks {
|
||||
opts, ok := c.opts[check.Resource]
|
||||
if !ok {
|
||||
// For now w fallback to grafana admin if no options are found for resource.
|
||||
if ident.GetIsGrafanaAdmin() {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: true}
|
||||
} else {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: false}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if verb should be skipped
|
||||
if opts.Unchecked[check.Verb] {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: true}
|
||||
continue
|
||||
}
|
||||
|
||||
action, ok := opts.Mapping[check.Verb]
|
||||
if !ok {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: fmt.Errorf("missing action for %s %s", check.Verb, check.Resource),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Get or create cached checker for this action
|
||||
checker, ok := checkerCache[action]
|
||||
if !ok {
|
||||
checker = Checker(ident, action)
|
||||
checkerCache[action] = checker
|
||||
}
|
||||
|
||||
// Handle list and create verbs (no specific name)
|
||||
// TODO: Should we allow list/create without name in a BatchCheck request?
|
||||
if check.Name == "" {
|
||||
if check.Verb == utils.VerbList || check.Verb == utils.VerbCreate {
|
||||
// For list/create without name, check if user has the action at all
|
||||
// TODO: Is this correct for Create?
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: len(ident.GetPermissions()[action]) > 0,
|
||||
}
|
||||
} else {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: fmt.Errorf("unhandled authorization: %s %s", check.Group, check.Verb),
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check with resolver or direct scope
|
||||
var allowed bool
|
||||
if opts.Resolver != nil {
|
||||
ns, err := claims.ParseNamespace(check.Namespace)
|
||||
if err != nil {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: err,
|
||||
}
|
||||
continue
|
||||
}
|
||||
scopes, err := opts.Resolver.Resolve(ctx, ns, check.Name)
|
||||
if err != nil {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: err,
|
||||
}
|
||||
continue
|
||||
}
|
||||
allowed = checker(scopes...)
|
||||
} else {
|
||||
allowed = checker(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, check.Name))
|
||||
}
|
||||
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: allowed}
|
||||
}
|
||||
|
||||
return claims.BatchCheckResponse{
|
||||
Results: results,
|
||||
Zookie: claims.NoopZookie{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,6 @@ import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
service AuthzExtentionService {
|
||||
rpc BatchCheck(BatchCheckRequest) returns (BatchCheckResponse);
|
||||
|
||||
rpc Read(ReadRequest) returns (ReadResponse);
|
||||
rpc Write(WriteRequest) returns (WriteResponse);
|
||||
|
||||
@@ -231,29 +229,6 @@ message WriteRequest {
|
||||
|
||||
message WriteResponse {}
|
||||
|
||||
message BatchCheckRequest {
|
||||
string subject = 1;
|
||||
string namespace = 2;
|
||||
repeated BatchCheckItem items = 3;
|
||||
}
|
||||
|
||||
message BatchCheckItem {
|
||||
string verb = 1;
|
||||
string group = 2;
|
||||
string resource = 3;
|
||||
string name = 4;
|
||||
string subresource = 5;
|
||||
string folder = 6;
|
||||
}
|
||||
|
||||
message BatchCheckResponse {
|
||||
map<string, BatchCheckGroupResource> groups = 1;
|
||||
}
|
||||
|
||||
message BatchCheckGroupResource {
|
||||
map<string, bool> items = 1;
|
||||
}
|
||||
|
||||
message QueryRequest {
|
||||
string namespace = 1;
|
||||
QueryOperation operation = 2;
|
||||
|
||||
@@ -19,18 +19,16 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion8
|
||||
|
||||
const (
|
||||
AuthzExtentionService_BatchCheck_FullMethodName = "/authz.extention.v1.AuthzExtentionService/BatchCheck"
|
||||
AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read"
|
||||
AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write"
|
||||
AuthzExtentionService_Mutate_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Mutate"
|
||||
AuthzExtentionService_Query_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Query"
|
||||
AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read"
|
||||
AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write"
|
||||
AuthzExtentionService_Mutate_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Mutate"
|
||||
AuthzExtentionService_Query_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Query"
|
||||
)
|
||||
|
||||
// AuthzExtentionServiceClient is the client API for AuthzExtentionService 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.
|
||||
type AuthzExtentionServiceClient interface {
|
||||
BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error)
|
||||
Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error)
|
||||
Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error)
|
||||
Mutate(ctx context.Context, in *MutateRequest, opts ...grpc.CallOption) (*MutateResponse, error)
|
||||
@@ -45,16 +43,6 @@ func NewAuthzExtentionServiceClient(cc grpc.ClientConnInterface) AuthzExtentionS
|
||||
return &authzExtentionServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *authzExtentionServiceClient) BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(BatchCheckResponse)
|
||||
err := c.cc.Invoke(ctx, AuthzExtentionService_BatchCheck_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authzExtentionServiceClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ReadResponse)
|
||||
@@ -99,7 +87,6 @@ func (c *authzExtentionServiceClient) Query(ctx context.Context, in *QueryReques
|
||||
// All implementations should embed UnimplementedAuthzExtentionServiceServer
|
||||
// for forward compatibility
|
||||
type AuthzExtentionServiceServer interface {
|
||||
BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error)
|
||||
Read(context.Context, *ReadRequest) (*ReadResponse, error)
|
||||
Write(context.Context, *WriteRequest) (*WriteResponse, error)
|
||||
Mutate(context.Context, *MutateRequest) (*MutateResponse, error)
|
||||
@@ -110,9 +97,6 @@ type AuthzExtentionServiceServer interface {
|
||||
type UnimplementedAuthzExtentionServiceServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedAuthzExtentionServiceServer) BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchCheck not implemented")
|
||||
}
|
||||
func (UnimplementedAuthzExtentionServiceServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Read not implemented")
|
||||
}
|
||||
@@ -137,24 +121,6 @@ func RegisterAuthzExtentionServiceServer(s grpc.ServiceRegistrar, srv AuthzExten
|
||||
s.RegisterService(&AuthzExtentionService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AuthzExtentionService_BatchCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BatchCheckRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AuthzExtentionService_BatchCheck_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, req.(*BatchCheckRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthzExtentionService_Read_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReadRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -234,10 +200,6 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "authz.extention.v1.AuthzExtentionService",
|
||||
HandlerType: (*AuthzExtentionServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "BatchCheck",
|
||||
Handler: _AuthzExtentionService_BatchCheck_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Read",
|
||||
Handler: _AuthzExtentionService_Read_Handler,
|
||||
|
||||
@@ -13,7 +13,6 @@ type Client interface {
|
||||
authlib.AccessClient
|
||||
Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error)
|
||||
Write(ctx context.Context, req *authzextv1.WriteRequest) error
|
||||
BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error)
|
||||
|
||||
Mutate(ctx context.Context, req *authzextv1.MutateRequest) error
|
||||
Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error)
|
||||
|
||||
@@ -68,11 +68,11 @@ func (c *Client) Write(ctx context.Context, req *authzextv1.WriteRequest) error
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Check")
|
||||
func (c *Client) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authlib.zanzana.client.BatchCheck")
|
||||
defer span.End()
|
||||
|
||||
return c.authzext.BatchCheck(ctx, req)
|
||||
return c.authzlibclient.BatchCheck(ctx, id, req)
|
||||
}
|
||||
|
||||
func (c *Client) WriteNew(ctx context.Context, req *authzextv1.WriteRequest) error {
|
||||
|
||||
@@ -34,8 +34,11 @@ func (nc NoopClient) Write(ctx context.Context, req *authzextv1.WriteRequest) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
return nil, nil
|
||||
func (nc NoopClient) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) {
|
||||
return authlib.BatchCheckResponse{
|
||||
Results: make(map[string]authlib.BatchCheckResult),
|
||||
Zookie: authlib.NoopZookie{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error {
|
||||
|
||||
@@ -132,3 +132,54 @@ func (c *ShadowClient) Compile(ctx context.Context, id authlib.AuthInfo, req aut
|
||||
|
||||
return shadowItemChecker, authlib.NoopZookie{}, err
|
||||
}
|
||||
|
||||
func (c *ShadowClient) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) {
|
||||
acResChan := make(chan authlib.BatchCheckResponse, 1)
|
||||
acErrChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
if c.zanzanaClient == nil {
|
||||
return
|
||||
}
|
||||
|
||||
zanzanaCtx := context.WithoutCancel(ctx)
|
||||
zanzanaCtxTimeout, cancel := context.WithTimeout(zanzanaCtx, zanzanaTimeout)
|
||||
defer cancel()
|
||||
|
||||
timer := prometheus.NewTimer(c.metrics.evaluationsSeconds.WithLabelValues("zanzana"))
|
||||
res, err := c.zanzanaClient.BatchCheck(zanzanaCtxTimeout, id, req)
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to run zanzana batch check", "error", err)
|
||||
}
|
||||
timer.ObserveDuration()
|
||||
|
||||
acRes := <-acResChan
|
||||
acErr := <-acErrChan
|
||||
|
||||
if acErr == nil {
|
||||
// Compare results for each correlation ID
|
||||
for corrID, acResult := range acRes.Results {
|
||||
zanzanaResult, exists := res.Results[corrID]
|
||||
if !exists {
|
||||
c.metrics.evaluationStatusTotal.WithLabelValues("error").Inc()
|
||||
c.logger.Warn("Zanzana batch check missing result", "correlationId", corrID, "user", id.GetUID())
|
||||
continue
|
||||
}
|
||||
if zanzanaResult.Allowed != acResult.Allowed {
|
||||
c.metrics.evaluationStatusTotal.WithLabelValues("error").Inc()
|
||||
c.logger.Warn("Zanzana batch check result does not match", "expected", acResult.Allowed, "actual", zanzanaResult.Allowed, "correlationId", corrID, "user", id.GetUID())
|
||||
} else {
|
||||
c.metrics.evaluationStatusTotal.WithLabelValues("success").Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
timer := prometheus.NewTimer(c.metrics.evaluationsSeconds.WithLabelValues("rbac"))
|
||||
res, err := c.accessClient.BatchCheck(ctx, id, req)
|
||||
timer.ObserveDuration()
|
||||
acResChan <- res
|
||||
acErrChan <- err
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
)
|
||||
|
||||
type typeInfo struct {
|
||||
@@ -73,7 +72,7 @@ func NewResourceInfoFromCheck(r *authzv1.CheckRequest) ResourceInfo {
|
||||
return resource
|
||||
}
|
||||
|
||||
func NewResourceInfoFromBatchItem(i *authzextv1.BatchCheckItem) ResourceInfo {
|
||||
func NewResourceInfoFromBatchItem(i *authzv1.BatchCheckItem) ResourceInfo {
|
||||
typ, relations := getTypeAndRelations(i.GetGroup(), i.GetResource())
|
||||
return newResource(
|
||||
typ,
|
||||
|
||||
@@ -2,97 +2,457 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
// checkKey represents a unique check to be performed
|
||||
type checkKey struct {
|
||||
relation string
|
||||
object string
|
||||
}
|
||||
|
||||
// batchCheckBuilder encapsulates state for building OpenFGA batch checks
|
||||
type batchCheckBuilder struct {
|
||||
subject string
|
||||
contextuals *openfgav1.ContextualTupleKeys
|
||||
checks []*openfgav1.BatchCheckItem
|
||||
checksSeen map[checkKey]bool
|
||||
checkMapping map[string]checkKey
|
||||
counter int
|
||||
}
|
||||
|
||||
func newBatchCheckBuilder(subject string, contextuals *openfgav1.ContextualTupleKeys) *batchCheckBuilder {
|
||||
return &batchCheckBuilder{
|
||||
subject: subject,
|
||||
contextuals: contextuals,
|
||||
checks: make([]*openfgav1.BatchCheckItem, 0),
|
||||
checksSeen: make(map[checkKey]bool),
|
||||
checkMapping: make(map[string]checkKey),
|
||||
counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *batchCheckBuilder) addCheck(relation, object string, context *structpb.Struct) {
|
||||
if object == "" {
|
||||
return
|
||||
}
|
||||
|
||||
key := checkKey{relation: relation, object: object}
|
||||
if b.checksSeen[key] {
|
||||
return
|
||||
}
|
||||
b.checksSeen[key] = true
|
||||
|
||||
correlationID := fmt.Sprintf("c%d", b.counter)
|
||||
b.counter++
|
||||
|
||||
b.checks = append(b.checks, &openfgav1.BatchCheckItem{
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: b.subject,
|
||||
Relation: relation,
|
||||
Object: object,
|
||||
},
|
||||
ContextualTuples: b.contextuals,
|
||||
Context: context,
|
||||
CorrelationId: correlationID,
|
||||
})
|
||||
b.checkMapping[correlationID] = key
|
||||
}
|
||||
|
||||
// BatchCheck implements authzv1.AuthzServiceServer.BatchCheck
|
||||
// This performs multiple access checks in a single request using OpenFGA's native BatchCheck API.
|
||||
func (s *Server) BatchCheck(ctx context.Context, r *authzv1.BatchCheckRequest) (*authzv1.BatchCheckResponse, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "server.BatchCheck")
|
||||
defer span.End()
|
||||
|
||||
if err := authorize(ctx, r.GetNamespace(), s.cfg); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
span.SetAttributes(attribute.Int("check_count", len(r.GetChecks())))
|
||||
|
||||
batchRes := &authzextv1.BatchCheckResponse{
|
||||
Groups: make(map[string]*authzextv1.BatchCheckGroupResource),
|
||||
}
|
||||
defer func(t time.Time) {
|
||||
s.metrics.requestDurationSeconds.WithLabelValues("server.BatchCheck", "").Observe(time.Since(t).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
store, err := s.getStoreInfo(ctx, r.GetNamespace())
|
||||
res, err := s.batchCheck(ctx, r)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform batch check request", "error", err)
|
||||
return nil, fmt.Errorf("failed to perform batch check request: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Server) batchCheck(ctx context.Context, r *authzv1.BatchCheckRequest) (*authzv1.BatchCheckResponse, error) {
|
||||
items := r.GetChecks()
|
||||
if len(items) == 0 {
|
||||
return &authzv1.BatchCheckResponse{
|
||||
Results: make(map[string]*authzv1.BatchCheckResult),
|
||||
}, nil
|
||||
}
|
||||
|
||||
namespace := r.GetNamespace()
|
||||
if err := authorize(ctx, namespace, s.cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store, err := s.getStoreInfo(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contextuals, err := s.getContextuals(r.GetSubject())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupResourceAccess := make(map[string]bool)
|
||||
results := make(map[string]*authzv1.BatchCheckResult, len(items))
|
||||
subject := r.GetSubject()
|
||||
|
||||
for _, item := range r.GetItems() {
|
||||
res, err := s.batchCheckItem(ctx, r, item, contextuals, store, groupResourceAccess)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
// Phase 1: Check GroupResource access (broadest permissions)
|
||||
// Example: user has "get" on "dashboards" group_resource → all dashboards allowed
|
||||
s.runGroupResourcePhase(ctx, store, subject, items, contextuals, results)
|
||||
if len(results) == len(items) {
|
||||
return s.buildResponse(results), nil
|
||||
}
|
||||
|
||||
// Phase 2: Check folder permission inheritance (can_get, can_create, etc. on parent folder)
|
||||
// Example: user has "can_get" on folder-A → all dashboards in folder-A allowed
|
||||
s.runFolderPermissionPhase(ctx, store, subject, items, contextuals, results)
|
||||
if len(results) == len(items) {
|
||||
return s.buildResponse(results), nil
|
||||
}
|
||||
|
||||
// Phase 3: Check folder subresource access (folder_get, folder_create, etc.)
|
||||
// Example: user has "folder_get" on folder-A → dashboards in folder-A allowed via subresource
|
||||
s.runFolderSubresourcePhase(ctx, store, subject, items, contextuals, results)
|
||||
if len(results) == len(items) {
|
||||
return s.buildResponse(results), nil
|
||||
}
|
||||
|
||||
// Phase 4: Check direct resource access
|
||||
// Example: user has "get" directly on dashboard-123
|
||||
s.runDirectResourcePhase(ctx, store, subject, items, contextuals, results)
|
||||
|
||||
// Mark any remaining unresolved items as denied
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; !resolved {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false}
|
||||
}
|
||||
}
|
||||
|
||||
return s.buildResponse(results), nil
|
||||
}
|
||||
|
||||
func (s *Server) buildResponse(results map[string]*authzv1.BatchCheckResult) *authzv1.BatchCheckResponse {
|
||||
return &authzv1.BatchCheckResponse{
|
||||
Results: results,
|
||||
Zookie: &authzv1.Zookie{Timestamp: time.Now().UnixMilli()},
|
||||
}
|
||||
}
|
||||
|
||||
// runGroupResourcePhase checks if the user has GroupResource-level access.
|
||||
// This is the broadest permission - if allowed, all items in that group are allowed.
|
||||
func (s *Server) runGroupResourcePhase(
|
||||
ctx context.Context,
|
||||
store *storeInfo,
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
// Group items by their GroupResource
|
||||
type grInfo struct {
|
||||
relation string
|
||||
grIdent string
|
||||
items []string // correlation IDs
|
||||
}
|
||||
groupedItems := make(map[string]*grInfo) // groupResource -> info
|
||||
|
||||
for _, item := range items {
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
if !common.IsGroupResourceRelation(relation) {
|
||||
continue
|
||||
}
|
||||
|
||||
groupResource := common.FormatGroupResource(item.GetGroup(), item.GetResource(), item.GetSubresource())
|
||||
if _, ok := batchRes.Groups[groupResource]; !ok {
|
||||
batchRes.Groups[groupResource] = &authzextv1.BatchCheckGroupResource{
|
||||
Items: make(map[string]bool),
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
gr := resource.GroupResource()
|
||||
|
||||
if _, exists := groupedItems[gr]; !exists {
|
||||
groupedItems[gr] = &grInfo{
|
||||
relation: relation,
|
||||
grIdent: resource.GroupResourceIdent(),
|
||||
items: make([]string, 0),
|
||||
}
|
||||
}
|
||||
batchRes.Groups[groupResource].Items[item.GetName()] = res.GetAllowed()
|
||||
groupedItems[gr].items = append(groupedItems[gr].items, item.GetCorrelationId())
|
||||
}
|
||||
|
||||
return batchRes, nil
|
||||
if len(groupedItems) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Build batch check for unique GroupResources
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
grCheckMapping := make(map[string]string) // OpenFGA correlationID -> groupResource
|
||||
|
||||
for gr, info := range groupedItems {
|
||||
correlationID := fmt.Sprintf("gr%d", builder.counter)
|
||||
builder.counter++
|
||||
builder.checks = append(builder.checks, &openfgav1.BatchCheckItem{
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: subject,
|
||||
Relation: info.relation,
|
||||
Object: info.grIdent,
|
||||
},
|
||||
ContextualTuples: contextuals,
|
||||
CorrelationId: correlationID,
|
||||
})
|
||||
grCheckMapping[correlationID] = gr
|
||||
}
|
||||
|
||||
openfgaRes, err := s.openfgaClient.BatchCheck(ctx, &openfgav1.BatchCheckRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
Checks: builder.checks,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to check group resource access", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Mark all items in allowed GroupResources
|
||||
for correlationID, result := range openfgaRes.GetResult() {
|
||||
gr := grCheckMapping[correlationID]
|
||||
if allowed, ok := result.GetCheckResult().(*openfgav1.BatchCheckSingleResult_Allowed); ok && allowed.Allowed {
|
||||
for _, itemCorrelationID := range groupedItems[gr].items {
|
||||
results[itemCorrelationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) batchCheckItem(
|
||||
// runFolderPermissionPhase checks folder permission inheritance (can_get, can_create, etc.).
|
||||
// This applies to folder-based resources like dashboards, panels, etc.
|
||||
func (s *Server) runFolderPermissionPhase(
|
||||
ctx context.Context,
|
||||
r *authzextv1.BatchCheckRequest,
|
||||
item *authzextv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
store *storeInfo,
|
||||
groupResourceAccess map[string]bool,
|
||||
) (*authzv1.CheckResponse, error) {
|
||||
var (
|
||||
relation = common.VerbMapping[item.GetVerb()]
|
||||
resource = common.NewResourceInfoFromBatchItem(item)
|
||||
groupResource = resource.GroupResource()
|
||||
)
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
checkToItems := make(map[checkKey][]string) // checkKey -> correlation IDs
|
||||
|
||||
allowed, ok := groupResourceAccess[groupResource]
|
||||
if !ok {
|
||||
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, contextuals, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; resolved {
|
||||
continue
|
||||
}
|
||||
|
||||
allowed = res.GetAllowed()
|
||||
groupResourceAccess[groupResource] = res.GetAllowed()
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
folderIdent := resource.FolderIdent()
|
||||
|
||||
// Only folder-based generic resources use folder permission inheritance
|
||||
if !resource.IsGeneric() || folderIdent == "" || !isFolderPermissionBasedResource(resource.GroupResource()) {
|
||||
continue
|
||||
}
|
||||
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
rel := common.FolderPermissionRelation(relation)
|
||||
key := checkKey{relation: rel, object: folderIdent}
|
||||
checkToItems[key] = append(checkToItems[key], item.GetCorrelationId())
|
||||
builder.addCheck(rel, folderIdent, resource.Context())
|
||||
}
|
||||
|
||||
if allowed {
|
||||
return &authzv1.CheckResponse{Allowed: true}, nil
|
||||
if len(builder.checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if resource.IsGeneric() {
|
||||
return s.checkGeneric(ctx, r.GetSubject(), relation, resource, contextuals, store)
|
||||
checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed folder permission phase", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
return s.checkTyped(ctx, r.GetSubject(), relation, resource, contextuals, store)
|
||||
// Mark items allowed by folder permissions
|
||||
for key, allowed := range checkResults {
|
||||
if allowed {
|
||||
for _, correlationID := range checkToItems[key] {
|
||||
results[correlationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runFolderSubresourcePhase checks folder subresource access (folder_get, folder_create, etc.).
|
||||
func (s *Server) runFolderSubresourcePhase(
|
||||
ctx context.Context,
|
||||
store *storeInfo,
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
checkToItems := make(map[checkKey][]string)
|
||||
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; resolved {
|
||||
continue
|
||||
}
|
||||
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
|
||||
var objectIdent string
|
||||
var subresRel string
|
||||
|
||||
if resource.IsGeneric() {
|
||||
// Generic resources: check subresource on folder
|
||||
folderIdent := resource.FolderIdent()
|
||||
if folderIdent == "" {
|
||||
continue
|
||||
}
|
||||
subresRel = common.SubresourceRelation(relation)
|
||||
if !common.IsSubresourceRelation(subresRel) {
|
||||
continue
|
||||
}
|
||||
objectIdent = folderIdent
|
||||
} else {
|
||||
// Typed resources: check subresource on the resource itself
|
||||
if !resource.HasSubresource() || !resource.IsValidRelation(relation) {
|
||||
continue
|
||||
}
|
||||
objectIdent = resource.ResourceIdent()
|
||||
if objectIdent == "" {
|
||||
continue
|
||||
}
|
||||
subresRel = common.SubresourceRelation(relation)
|
||||
}
|
||||
|
||||
key := checkKey{relation: subresRel, object: objectIdent}
|
||||
checkToItems[key] = append(checkToItems[key], item.GetCorrelationId())
|
||||
builder.addCheck(subresRel, objectIdent, resource.Context())
|
||||
}
|
||||
|
||||
if len(builder.checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed folder subresource phase", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for key, allowed := range checkResults {
|
||||
if allowed {
|
||||
for _, correlationID := range checkToItems[key] {
|
||||
results[correlationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runDirectResourcePhase checks direct resource access.
|
||||
func (s *Server) runDirectResourcePhase(
|
||||
ctx context.Context,
|
||||
store *storeInfo,
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
checkToItems := make(map[checkKey][]string)
|
||||
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; resolved {
|
||||
continue
|
||||
}
|
||||
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
|
||||
if !resource.IsValidRelation(relation) {
|
||||
continue
|
||||
}
|
||||
|
||||
resourceIdent := resource.ResourceIdent()
|
||||
if resourceIdent == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// For folders, use the computed permission relation
|
||||
checkRelation := relation
|
||||
if resource.Type() == common.TypeFolder {
|
||||
checkRelation = common.FolderPermissionRelation(relation)
|
||||
}
|
||||
|
||||
key := checkKey{relation: checkRelation, object: resourceIdent}
|
||||
checkToItems[key] = append(checkToItems[key], item.GetCorrelationId())
|
||||
builder.addCheck(checkRelation, resourceIdent, resource.Context())
|
||||
}
|
||||
|
||||
if len(builder.checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed direct resource phase", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for key, allowed := range checkResults {
|
||||
if allowed {
|
||||
for _, correlationID := range checkToItems[key] {
|
||||
results[correlationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeOpenFGABatchChecks executes the OpenFGA batch checks in chunks and returns results
|
||||
func (s *Server) executeOpenFGABatchChecks(ctx context.Context, store *storeInfo, builder *batchCheckBuilder) (map[checkKey]bool, error) {
|
||||
const maxChecksPerBatch = 50
|
||||
checkResults := make(map[checkKey]bool)
|
||||
|
||||
for i := 0; i < len(builder.checks); i += maxChecksPerBatch {
|
||||
end := i + maxChecksPerBatch
|
||||
if end > len(builder.checks) {
|
||||
end = len(builder.checks)
|
||||
}
|
||||
|
||||
openfgaRes, err := s.openfgaClient.BatchCheck(ctx, &openfgav1.BatchCheckRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
Checks: builder.checks[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to perform OpenFGA batch check: %w", err)
|
||||
}
|
||||
|
||||
// Process results
|
||||
for correlationID, result := range openfgaRes.GetResult() {
|
||||
key, ok := builder.checkMapping[correlationID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if allowed, ok := result.GetCheckResult().(*openfgav1.BatchCheckSingleResult_Allowed); ok {
|
||||
checkResults[key] = allowed.Allowed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return checkResults, nil
|
||||
}
|
||||
|
||||
@@ -1,193 +1,192 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
func testBatchCheck(t *testing.T, server *Server) {
|
||||
newReq := func(subject, verb, group, resource, subresource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
|
||||
for i, item := range items {
|
||||
items[i] = &authzextv1.BatchCheckItem{
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: item.GetName(),
|
||||
Folder: item.GetFolder(),
|
||||
}
|
||||
}
|
||||
|
||||
return &authzextv1.BatchCheckRequest{
|
||||
Namespace: namespace,
|
||||
// Helper to create a batch check request
|
||||
newReq := func(subject string, items []*authzv1.BatchCheckItem) *authzv1.BatchCheckRequest {
|
||||
return &authzv1.BatchCheckRequest{
|
||||
Subject: subject,
|
||||
Items: items,
|
||||
Namespace: namespace,
|
||||
Checks: items,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create a batch check item with correlation ID
|
||||
newItem := func(verb, group, resource, subresource, folder, name string) *authzv1.BatchCheckItem {
|
||||
correlationID := fmt.Sprintf("%s-%s-%s-%s", group, resource, folder, name)
|
||||
return &authzv1.BatchCheckItem{
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: name,
|
||||
Folder: folder,
|
||||
CorrelationId: correlationID,
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:1", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through group_resource", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:2", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
// user:2 has group_resource access, so both should be allowed
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:3", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "3"},
|
||||
{Name: "3", Folder: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:4", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "3", "2"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "3"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 3)
|
||||
require.Len(t, res.Results, 3)
|
||||
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.True(t, res.Groups[groupResource].Items["2"])
|
||||
assert.False(t, res.Groups[groupResource].Items["3"])
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "3", "2")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "3")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:5", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(folderGroup, folderResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1"},
|
||||
{Name: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:6", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "1"),
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:7 should be able to read folder {1,2} through group_resource access", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(folderGroup, folderResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1"},
|
||||
{Name: "2"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:7", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "1"),
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "2"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["1"])
|
||||
require.True(t, res.Groups[groupResource].Items["2"])
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "20", Folder: "6"},
|
||||
t.Run("user:8 should be able to read all resource:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:8", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "20"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["20"])
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "20")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:9 should be able to create dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "20", Folder: "6"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:9", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbCreate, dashboardGroup, dashboardResource, "", "6", "10"),
|
||||
newItem(utils.VerbCreate, dashboardGroup, dashboardResource, "", "6", "20"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
t.Log(res.Groups)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["20"])
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "20")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:10 should be able to get dashboard status for 10 and 11", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "11", Folder: "6"},
|
||||
{Name: "12", Folder: "6"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:10", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
t.Log(res.Groups)
|
||||
require.Len(t, res.Groups[groupResource].Items, 3)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["11"])
|
||||
require.False(t, res.Groups[groupResource].Items["12"])
|
||||
require.Len(t, res.Results, 3)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:11 should be able to get dashboard status for 10, 11 and 12 through group_resource", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "11", Folder: "6"},
|
||||
{Name: "12", Folder: "6"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:11", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
t.Log(res.Groups)
|
||||
require.Len(t, res.Groups[groupResource].Items, 3)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["11"])
|
||||
require.True(t, res.Groups[groupResource].Items["12"])
|
||||
require.Len(t, res.Results, 3)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("user:12 should be able to get dashboard status in folder 5 and 6", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "5"},
|
||||
{Name: "11", Folder: "6"},
|
||||
{Name: "12", Folder: "6"},
|
||||
{Name: "13", Folder: "1"},
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:12", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "13"),
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupResource].Items, 4)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["11"])
|
||||
require.True(t, res.Groups[groupResource].Items["12"])
|
||||
require.False(t, res.Groups[groupResource].Items["13"])
|
||||
require.Len(t, res.Results, 4)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "5", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "13")].Allowed)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/store"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
@@ -37,14 +36,14 @@ const (
|
||||
// Timeout for List operations
|
||||
listTimeout = 30 * time.Second
|
||||
|
||||
// BenchmarkBatchCheck measures the performance of BatchCheck requests with 50 items per batch.
|
||||
batchCheckSize = 50
|
||||
|
||||
// Resource type constants for benchmarks
|
||||
benchDashboardGroup = "dashboard.grafana.app"
|
||||
benchDashboardResource = "dashboards"
|
||||
benchFolderGroup = "folder.grafana.app"
|
||||
benchFolderResource = "folders"
|
||||
|
||||
// BenchmarkBatchCheck measures the performance of BatchCheck requests with 50 items per batch.
|
||||
batchCheckSize = 50
|
||||
)
|
||||
|
||||
// benchmarkData holds all the generated test data for benchmarks
|
||||
@@ -338,6 +337,14 @@ func setupBenchmarkServer(b *testing.B) (*Server, *benchmarkData) {
|
||||
}
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
|
||||
cfg.ZanzanaServer.CacheSettings.CheckCacheLimit = 100000 // Cache check results
|
||||
cfg.ZanzanaServer.CacheSettings.CheckQueryCacheEnabled = true // Cache check subproblems
|
||||
cfg.ZanzanaServer.CacheSettings.CheckIteratorCacheEnabled = true // Cache DB iterators for checks
|
||||
cfg.ZanzanaServer.CacheSettings.CheckIteratorCacheMaxResults = 10000 // Max results per iterator
|
||||
cfg.ZanzanaServer.CacheSettings.SharedIteratorEnabled = true // Share iterators across concurrent checks
|
||||
cfg.ZanzanaServer.CacheSettings.SharedIteratorLimit = 10000 // Max shared iterators
|
||||
|
||||
testStore := sqlstore.NewTestStore(b, sqlstore.WithCfg(cfg))
|
||||
|
||||
openFGAStore, err := store.NewEmbeddedStore(cfg, testStore, log.NewNopLogger())
|
||||
@@ -573,58 +580,62 @@ func BenchmarkCheck(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkBatchCheck measures the performance of BatchCheck requests
|
||||
func BenchmarkBatchCheck(b *testing.B) {
|
||||
srv, data := setupBenchmarkServer(b)
|
||||
ctx := newContextWithNamespace()
|
||||
|
||||
// Helper to create batch check requests
|
||||
newBatchCheckReq := func(subject string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
|
||||
return &authzextv1.BatchCheckRequest{
|
||||
Namespace: benchNamespace,
|
||||
// Helper to create batch check requests using the new authzv1 API
|
||||
newBatchCheckReq := func(subject string, items []*authzv1.BatchCheckItem) *authzv1.BatchCheckRequest {
|
||||
return &authzv1.BatchCheckRequest{
|
||||
Subject: subject,
|
||||
Items: items,
|
||||
Namespace: benchNamespace,
|
||||
Checks: items,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create batch items for resources in folders
|
||||
createBatchItems := func(resources []string, resourceFolders map[string]string) []*authzextv1.BatchCheckItem {
|
||||
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
|
||||
createBatchItems := func(resources []string, resourceFolders map[string]string) []*authzv1.BatchCheckItem {
|
||||
items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize)
|
||||
for i := 0; i < batchCheckSize && i < len(resources); i++ {
|
||||
resource := resources[i]
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: resource,
|
||||
Folder: resourceFolders[resource],
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: resource,
|
||||
Folder: resourceFolders[resource],
|
||||
CorrelationId: fmt.Sprintf("item-%d", i),
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Helper to create batch items for folders at a specific depth
|
||||
createFolderBatchItems := func(folders []string, depth int, folderDepths map[string]int) []*authzextv1.BatchCheckItem {
|
||||
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
|
||||
createFolderBatchItems := func(folders []string, depth int, folderDepths map[string]int) []*authzv1.BatchCheckItem {
|
||||
items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize)
|
||||
for _, folder := range folders {
|
||||
if folderDepths[folder] == depth && len(items) < batchCheckSize {
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-in-%s", folder),
|
||||
Folder: folder,
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-in-%s", folder),
|
||||
Folder: folder,
|
||||
CorrelationId: fmt.Sprintf("item-%d", len(items)),
|
||||
})
|
||||
}
|
||||
}
|
||||
// Fill remaining slots if needed
|
||||
for len(items) < batchCheckSize && len(folders) > 0 {
|
||||
folder := folders[len(items)%len(folders)]
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", len(items)),
|
||||
Folder: folder,
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", len(items)),
|
||||
Folder: folder,
|
||||
CorrelationId: fmt.Sprintf("item-%d", len(items)),
|
||||
})
|
||||
}
|
||||
return items
|
||||
@@ -636,6 +647,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with group_resource permission - should have access to everything
|
||||
user := data.users[0]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
b.Logf("Testing BatchCheck with %d items, user has group_resource permission (all access)", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -643,7 +655,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
@@ -651,6 +663,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with folder permission on shallow folder
|
||||
user := data.users[usersPerPattern]
|
||||
items := createFolderBatchItems(data.folders, 1, data.folderDepths)
|
||||
b.Logf("Testing BatchCheck with %d items at depth 1", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -658,7 +671,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
@@ -666,6 +679,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with folder permission on mid-depth folder
|
||||
user := data.users[2*usersPerPattern]
|
||||
items := createFolderBatchItems(data.folders, 4, data.folderDepths)
|
||||
b.Logf("Testing BatchCheck with %d items at depth 4", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -673,22 +687,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FolderInheritance/Depth7", func(b *testing.B) {
|
||||
// Check access on deepest folders (worst case for inheritance traversal)
|
||||
user := data.users[usersPerPattern]
|
||||
items := createFolderBatchItems(data.folders, data.maxDepth, data.folderDepths)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
@@ -696,6 +695,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with direct resource permission
|
||||
user := data.users[4*usersPerPattern]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
b.Logf("Testing BatchCheck with %d items, user has direct resource permission", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -703,22 +703,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("TeamMembership", func(b *testing.B) {
|
||||
// User who is a team member, team has folder permission
|
||||
user := data.users[5*usersPerPattern]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
@@ -726,6 +711,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with no permissions - tests denial path
|
||||
user := data.users[len(data.users)-1]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
b.Logf("Testing BatchCheck with %d items, user has NO permissions (denial case)", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -733,24 +719,28 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MixedFolders", func(b *testing.B) {
|
||||
// Batch of items across different folder depths
|
||||
user := data.users[usersPerPattern]
|
||||
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
|
||||
b.Run("MixedAccess", func(b *testing.B) {
|
||||
// Create items from different folders - user has access to some but not all
|
||||
user := data.users[3*usersPerPattern] // folder-scoped resource permission
|
||||
items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize)
|
||||
|
||||
// Mix of accessible and inaccessible resources
|
||||
for i := 0; i < batchCheckSize; i++ {
|
||||
folder := data.folders[i%len(data.folders)]
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", i),
|
||||
Folder: folder,
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", i),
|
||||
Folder: folder,
|
||||
CorrelationId: fmt.Sprintf("item-%d", i),
|
||||
})
|
||||
}
|
||||
b.Logf("Testing BatchCheck with %d items, user has mixed access (some allowed, some denied)", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -758,9 +748,31 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
|
||||
// Test BatchCheck at various folder depths
|
||||
for depth := 0; depth <= data.maxDepth; depth++ {
|
||||
depth := depth // capture for closure
|
||||
if len(data.foldersByDepth[depth]) == 0 {
|
||||
continue
|
||||
}
|
||||
b.Run(fmt.Sprintf("ByDepth/Depth%d", depth), func(b *testing.B) {
|
||||
user := fmt.Sprintf("user:depth-%d-access", depth)
|
||||
items := createFolderBatchItems(data.folders, depth, data.folderDepths)
|
||||
b.Logf("Testing BatchCheck with %d items at depth %d", len(items), depth)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkList measures the performance of List requests (Compile equivalent)
|
||||
|
||||
Reference in New Issue
Block a user