Zanzana: Consistently add context (#98862)

* Zanzana: Reworks how contextuals are loaded

* Cleanup listObjectWithStream

* Run list test with streaming enabled
pull/98877/head
Karl Persson 12 months ago committed by GitHub
parent 04acbcdef2
commit 0f9b107201
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 51
      pkg/services/authz/zanzana/common/info.go
  2. 84
      pkg/services/authz/zanzana/server/server.go
  3. 17
      pkg/services/authz/zanzana/server/server_batch_check.go
  4. 54
      pkg/services/authz/zanzana/server/server_check.go
  5. 27
      pkg/services/authz/zanzana/server/server_list.go
  6. 43
      pkg/services/authz/zanzana/server/server_list_streamed.go
  7. 2
      pkg/services/authz/zanzana/server/server_read.go
  8. 6
      pkg/services/authz/zanzana/server/server_test.go
  9. 2
      pkg/services/authz/zanzana/server/server_write.go

@ -27,27 +27,54 @@ func getTypeInfo(group, resource string) (typeInfo, bool) {
}
func NewResourceInfoFromCheck(r *authzv1.CheckRequest) ResourceInfo {
if info, ok := getTypeInfo(r.GetGroup(), r.GetResource()); ok {
return newResource(info.Type, r.GetGroup(), r.GetResource(), r.GetName(), r.GetFolder(), r.GetSubresource(), info.Relations)
}
return newResource(TypeResource, r.GetGroup(), r.GetResource(), r.GetName(), r.GetFolder(), r.GetSubresource(), RelationsResource)
typ, relations := getTypeAndRelations(r.GetGroup(), r.GetResource())
return newResource(
typ,
r.GetGroup(),
r.GetResource(),
r.GetName(),
r.GetFolder(),
r.GetSubresource(),
relations,
)
}
func NewResourceInfoFromBatchItem(i *authzextv1.BatchCheckItem) ResourceInfo {
if info, ok := getTypeInfo(i.GetGroup(), i.GetResource()); ok {
return newResource(info.Type, i.GetGroup(), i.GetResource(), i.GetName(), i.GetFolder(), i.GetSubresource(), info.Relations)
}
return newResource(TypeResource, i.GetGroup(), i.GetResource(), i.GetName(), i.GetFolder(), i.GetSubresource(), RelationsResource)
typ, relations := getTypeAndRelations(i.GetGroup(), i.GetResource())
return newResource(
typ,
i.GetGroup(),
i.GetResource(),
i.GetName(),
i.GetFolder(),
i.GetSubresource(),
relations,
)
}
func NewResourceInfoFromList(r *authzv1.ListRequest) ResourceInfo {
if info, ok := getTypeInfo(r.GetGroup(), r.GetResource()); ok {
return newResource(info.Type, r.GetGroup(), r.GetResource(), "", "", r.GetSubresource(), info.Relations)
typ, relations := getTypeAndRelations(r.GetGroup(), r.GetResource())
return newResource(
typ,
r.GetGroup(),
r.GetResource(),
"",
"",
r.GetSubresource(),
relations,
)
}
func getTypeAndRelations(group, resource string) (string, []string) {
if info, ok := getTypeInfo(group, resource); ok {
return info.Type, info.Relations
}
return newResource(TypeResource, r.GetGroup(), r.GetResource(), "", "", r.GetSubresource(), RelationsResource)
return TypeResource, RelationsResource
}
func newResource(typ string, group, resource, name, folder, subresource string, relations []string) ResourceInfo {
func newResource(
typ, group, resource, name, folder, subresource string, relations []string,
) ResourceInfo {
return ResourceInfo{
typ: typ,
group: group,

@ -2,6 +2,7 @@ package server
import (
"context"
"strings"
"sync"
"time"
@ -11,6 +12,7 @@ import (
"github.com/openfga/language/pkg/go/transformer"
"go.opentelemetry.io/otel"
dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
@ -94,14 +96,39 @@ func NewAuthz(cfg *setting.Cfg, openfga openfgav1.OpenFGAServiceServer, opts ...
return s, nil
}
func (s *Server) getGlobalAuthorizationContext(ctx context.Context) ([]*openfgav1.TupleKey, error) {
cacheKey := "global_authorization_context"
contextualTuples := make([]*openfgav1.TupleKey, 0)
func (s *Server) getContextuals(ctx context.Context, subject string) (*openfgav1.ContextualTupleKeys, error) {
contextuals, err := s.getGlobalAuthorizationContext(ctx)
if err != nil {
return nil, err
}
if strings.HasPrefix(subject, common.TypeRenderService+":") {
contextuals = append(
contextuals,
&openfgav1.TupleKey{
User: subject,
Relation: common.RelationSetView,
Object: common.NewGroupResourceIdent(
dashboardalpha1.DashboardResourceInfo.GroupResource().Group,
dashboardalpha1.DashboardResourceInfo.GroupResource().Resource,
"",
),
},
)
}
if len(contextuals) > 0 {
return &openfgav1.ContextualTupleKeys{TupleKeys: contextuals}, nil
}
return nil, nil
}
func (s *Server) getGlobalAuthorizationContext(ctx context.Context) ([]*openfgav1.TupleKey, error) {
const cacheKey = "global_authorization_context"
cached, found := s.cache.Get(cacheKey)
if found {
contextualTuples = cached.([]*openfgav1.TupleKey)
return contextualTuples, nil
return cached.([]*openfgav1.TupleKey), nil
}
res, err := s.Read(ctx, &authzextv1.ReadRequest{
@ -111,53 +138,12 @@ func (s *Server) getGlobalAuthorizationContext(ctx context.Context) ([]*openfgav
return nil, err
}
tuples := common.ToOpenFGATuples(res.Tuples)
contextualTuples := make([]*openfgav1.TupleKey, 0, len(res.GetTuples()))
tuples := common.ToOpenFGATuples(res.GetTuples())
for _, t := range tuples {
contextualTuples = append(contextualTuples, t.GetKey())
}
s.cache.SetDefault(cacheKey, contextualTuples)
s.cache.SetDefault(cacheKey, contextualTuples)
return contextualTuples, nil
}
func (s *Server) addCheckAuthorizationContext(ctx context.Context, req *openfgav1.CheckRequest) error {
contextualTuples, err := s.getGlobalAuthorizationContext(ctx)
if err != nil {
return err
}
if len(contextualTuples) == 0 {
return nil
}
if req.ContextualTuples == nil {
req.ContextualTuples = &openfgav1.ContextualTupleKeys{}
}
if req.ContextualTuples.TupleKeys == nil {
req.ContextualTuples.TupleKeys = make([]*openfgav1.TupleKey, 0)
}
req.ContextualTuples.TupleKeys = append(req.ContextualTuples.TupleKeys, contextualTuples...)
return nil
}
func (s *Server) addListAuthorizationContext(ctx context.Context, req *openfgav1.ListObjectsRequest) error {
contextualTuples, err := s.getGlobalAuthorizationContext(ctx)
if err != nil {
return err
}
if len(contextualTuples) == 0 {
return nil
}
if req.ContextualTuples == nil {
req.ContextualTuples = &openfgav1.ContextualTupleKeys{}
}
if req.ContextualTuples.TupleKeys == nil {
req.ContextualTuples.TupleKeys = make([]*openfgav1.TupleKey, 0)
}
req.ContextualTuples.TupleKeys = append(req.ContextualTuples.TupleKeys, contextualTuples...)
return nil
}

@ -4,13 +4,14 @@ import (
"context"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
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) {
ctx, span := tracer.Start(ctx, "authzServer.BatchCheck")
ctx, span := tracer.Start(ctx, "server.BatchCheck")
defer span.End()
if err := authorize(ctx, r.GetNamespace()); err != nil {
@ -26,10 +27,15 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest
return nil, err
}
contextuals, err := s.getContextuals(ctx, r.GetSubject())
if err != nil {
return nil, err
}
groupResourceAccess := make(map[string]bool)
for _, item := range r.GetItems() {
res, err := s.batchCheckItem(ctx, r, item, store, groupResourceAccess)
res, err := s.batchCheckItem(ctx, r, item, contextuals, store, groupResourceAccess)
if err != nil {
return nil, err
}
@ -50,6 +56,7 @@ func (s *Server) batchCheckItem(
ctx context.Context,
r *authzextv1.BatchCheckRequest,
item *authzextv1.BatchCheckItem,
contextuals *openfgav1.ContextualTupleKeys,
store *storeInfo,
groupResourceAccess map[string]bool,
) (*authzv1.CheckResponse, error) {
@ -61,7 +68,7 @@ func (s *Server) batchCheckItem(
allowed, ok := groupResourceAccess[groupResource]
if !ok {
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, store)
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, contextuals, store)
if err != nil {
return nil, err
}
@ -75,8 +82,8 @@ func (s *Server) batchCheckItem(
}
if resource.IsGeneric() {
return s.checkGeneric(ctx, r.GetSubject(), relation, resource, store)
return s.checkGeneric(ctx, r.GetSubject(), relation, resource, contextuals, store)
}
return s.checkTyped(ctx, r.GetSubject(), relation, resource, store)
return s.checkTyped(ctx, r.GetSubject(), relation, resource, contextuals, store)
}

@ -2,8 +2,6 @@ package server
import (
"context"
"fmt"
"strings"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
@ -12,7 +10,7 @@ import (
)
func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) {
ctx, span := tracer.Start(ctx, "authzServer.Check")
ctx, span := tracer.Start(ctx, "server.Check")
defer span.End()
if err := authorize(ctx, r.GetNamespace()); err != nil {
@ -26,8 +24,13 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
relation := common.VerbMapping[r.GetVerb()]
contextuals, err := s.getContextuals(ctx, r.GetSubject())
if err != nil {
return nil, err
}
resource := common.NewResourceInfoFromCheck(r)
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, store)
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, contextuals, store)
if err != nil {
return nil, err
}
@ -37,20 +40,20 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
}
if resource.IsGeneric() {
return s.checkGeneric(ctx, r.GetSubject(), relation, resource, store)
return s.checkGeneric(ctx, r.GetSubject(), relation, resource, contextuals, store)
}
return s.checkTyped(ctx, r.GetSubject(), relation, resource, store)
return s.checkTyped(ctx, r.GetSubject(), relation, resource, contextuals, store)
}
// checkGroupResource check if subject has access to the full "GroupResource", if they do they can access every object
// within it.
func (s *Server) checkGroupResource(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.CheckResponse, error) {
func (s *Server) checkGroupResource(ctx context.Context, subject, relation string, resource common.ResourceInfo, contextuals *openfgav1.ContextualTupleKeys, store *storeInfo) (*authzv1.CheckResponse, error) {
if !common.IsGroupResourceRelation(relation) {
return &authzv1.CheckResponse{Allowed: false}, nil
}
req := &openfgav1.CheckRequest{
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
@ -58,13 +61,8 @@ func (s *Server) checkGroupResource(ctx context.Context, subject, relation strin
Relation: relation,
Object: resource.GroupResourceIdent(),
},
}
if strings.HasPrefix(subject, fmt.Sprintf("%s:", common.TypeRenderService)) {
common.AddRenderContext(req)
}
res, err := s.check(ctx, req)
ContextualTuples: contextuals,
})
if err != nil {
return nil, err
}
@ -73,13 +71,13 @@ func (s *Server) checkGroupResource(ctx context.Context, subject, relation strin
}
// checkTyped checks on our typed resources e.g. folder.
func (s *Server) checkTyped(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.CheckResponse, error) {
func (s *Server) checkTyped(ctx context.Context, subject, relation string, resource common.ResourceInfo, contextuals *openfgav1.ContextualTupleKeys, store *storeInfo) (*authzv1.CheckResponse, error) {
if !resource.IsValidRelation(relation) {
return &authzv1.CheckResponse{Allowed: false}, nil
}
// Check if subject has direct access to resource
res, err := s.check(ctx, &openfgav1.CheckRequest{
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
@ -87,6 +85,7 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation string, resou
Relation: relation,
Object: resource.ResourceIdent(),
},
ContextualTuples: contextuals,
})
if err != nil {
return nil, err
@ -102,7 +101,7 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation string, resou
// checkGeneric check our generic "resource" type. It checks:
// 1. If subject has access as a sub resource for a folder.
// 2. If subject has direct access to resource.
func (s *Server) checkGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.CheckResponse, error) {
func (s *Server) checkGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, contextuals *openfgav1.ContextualTupleKeys, store *storeInfo) (*authzv1.CheckResponse, error) {
var (
folderIdent = resource.FolderIdent()
resourceCtx = resource.Context()
@ -111,7 +110,7 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res
if folderIdent != "" && common.IsFolderResourceRelation(folderRelation) {
// Check if subject has access as a sub resource for the folder
res, err := s.check(ctx, &openfgav1.CheckRequest{
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
@ -119,7 +118,8 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res
Relation: folderRelation,
Object: folderIdent,
},
Context: resourceCtx,
Context: resourceCtx,
ContextualTuples: contextuals,
})
if err != nil {
@ -137,7 +137,7 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res
}
// Check if subject has direct access to resource
res, err := s.check(ctx, &openfgav1.CheckRequest{
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
@ -145,7 +145,8 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res
Relation: relation,
Object: resourceIdent,
},
Context: resourceCtx,
Context: resourceCtx,
ContextualTuples: contextuals,
})
if err != nil {
@ -154,12 +155,3 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res
return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil
}
func (s *Server) check(ctx context.Context, req *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) {
err := s.addCheckAuthorizationContext(ctx, req)
if err != nil {
s.logger.Error("failed to add authorization context", "error", err)
}
return s.openfga.Check(ctx, req)
}

@ -12,14 +12,19 @@ import (
)
func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.ListResponse, error) {
ctx, span := tracer.Start(ctx, "authzServer.List")
ctx, span := tracer.Start(ctx, "server.List")
defer span.End()
if err := authorize(ctx, r.GetNamespace()); err != nil {
return nil, err
}
store, err := s.getStoreInfo(ctx, r.Namespace)
store, err := s.getStoreInfo(ctx, r.GetNamespace())
if err != nil {
return nil, err
}
contextuals, err := s.getContextuals(ctx, r.GetSubject())
if err != nil {
return nil, err
}
@ -27,7 +32,7 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis
relation := common.VerbMapping[r.GetVerb()]
resource := common.NewResourceInfoFromList(r)
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, store)
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, contextuals, store)
if err != nil {
return nil, err
}
@ -37,25 +42,20 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis
}
if resource.IsGeneric() {
return s.listGeneric(ctx, r.GetSubject(), relation, resource, store)
return s.listGeneric(ctx, r.GetSubject(), relation, resource, contextuals, store)
}
return s.listTyped(ctx, r.GetSubject(), relation, resource, store)
return s.listTyped(ctx, r.GetSubject(), relation, resource, contextuals, store)
}
func (s *Server) listObjects(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
err := s.addListAuthorizationContext(ctx, req)
if err != nil {
s.logger.Error("failed to add authorization context", "error", err)
}
if s.cfg.UseStreamedListObjects {
return s.streamedListObjects(ctx, req)
}
return s.openfga.ListObjects(ctx, req)
}
func (s *Server) listTyped(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.ListResponse, error) {
func (s *Server) listTyped(ctx context.Context, subject, relation string, resource common.ResourceInfo, contextuals *openfgav1.ContextualTupleKeys, store *storeInfo) (*authzv1.ListResponse, error) {
if !resource.IsValidRelation(relation) {
return &authzv1.ListResponse{}, nil
}
@ -67,6 +67,7 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour
Type: resource.Type(),
Relation: relation,
User: subject,
ContextualTuples: contextuals,
})
if err != nil {
return nil, err
@ -77,7 +78,7 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour
}, nil
}
func (s *Server) listGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.ListResponse, error) {
func (s *Server) listGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, contextuals *openfgav1.ContextualTupleKeys, store *storeInfo) (*authzv1.ListResponse, error) {
var (
folderRelation = common.FolderResourceRelation(relation)
resourceCtx = resource.Context()
@ -93,6 +94,7 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation string, reso
Relation: folderRelation,
User: subject,
Context: resourceCtx,
ContextualTuples: contextuals,
})
if err != nil {
@ -112,6 +114,7 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation string, reso
Relation: relation,
User: subject,
Context: resourceCtx,
ContextualTuples: contextuals,
})
if err != nil {
return nil, err

@ -14,11 +14,11 @@ func (s *Server) streamedListObjects(ctx context.Context, req *openfgav1.ListObj
if !s.cfg.CheckQueryCache {
return s.listObjectsWithStream(ctx, req)
}
return s.streamedListObjectsCached(ctx, req)
return s.listObjectsWithStreamCached(ctx, req)
}
func (s *Server) streamedListObjectsCached(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
ctx, span := tracer.Start(ctx, "authzServer.streamedListObjectsCached")
func (s *Server) listObjectsWithStreamCached(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
ctx, span := tracer.Start(ctx, "server.listObjectsWithStreamCached")
defer span.End()
reqHash, err := getRequestHash(req)
@ -39,7 +39,7 @@ func (s *Server) streamedListObjectsCached(ctx context.Context, req *openfgav1.L
}
func (s *Server) listObjectsWithStream(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
ctx, span := tracer.Start(ctx, "authzServer.listObjectsWithStream")
ctx, span := tracer.Start(ctx, "server.listObjectsWithStream")
defer span.End()
r := &openfgav1.StreamedListObjectsRequest{
@ -49,47 +49,32 @@ func (s *Server) listObjectsWithStream(ctx context.Context, req *openfgav1.ListO
Relation: req.GetRelation(),
User: req.GetUser(),
Context: req.GetContext(),
ContextualTuples: req.ContextualTuples,
}
clientStream, err := s.openfgaClient.StreamedListObjects(ctx, r)
stream, err := s.openfgaClient.StreamedListObjects(ctx, r)
if err != nil {
return nil, err
}
done := make(chan struct{})
var streamedObjectIDs []string
var streamingErr error
var streamingResp *openfgav1.StreamedListObjectsResponse
go func() {
for {
streamingResp, streamingErr = clientStream.Recv()
if streamingErr == nil {
streamedObjectIDs = append(streamedObjectIDs, streamingResp.GetObject())
} else {
if errors.Is(streamingErr, io.EOF) {
streamingErr = nil
}
var objects []string
for {
res, err := stream.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, err
}
done <- struct{}{}
}()
<-done
if streamingErr != nil {
return nil, streamingErr
objects = append(objects, res.GetObject())
}
return &openfgav1.ListObjectsResponse{
Objects: streamedObjectIDs,
Objects: objects,
}, nil
}
func getRequestHash(req *openfgav1.ListObjectsRequest) (string, error) {
if req == nil {
return "", errors.New("request must not be empty")
}
hash := fnv.New64a()
_, err := hash.Write([]byte(req.String()))
if err != nil {

@ -10,7 +10,7 @@ import (
)
func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) {
ctx, span := tracer.Start(ctx, "authzServer.Read")
ctx, span := tracer.Start(ctx, "server.Read")
defer span.End()
if err := authorize(ctx, req.GetNamespace()); err != nil {

@ -56,6 +56,12 @@ func TestIntegrationServer(t *testing.T) {
testList(t, srv)
})
t.Run("test list streaming", func(t *testing.T) {
srv.cfg.UseStreamedListObjects = true
testList(t, srv)
srv.cfg.UseStreamedListObjects = false
})
t.Run("test batch check", func(t *testing.T) {
testBatchCheck(t, srv)
})

@ -10,7 +10,7 @@ import (
)
func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*authzextv1.WriteResponse, error) {
ctx, span := tracer.Start(ctx, "authzServer.Write")
ctx, span := tracer.Start(ctx, "server.Write")
defer span.End()
if err := authorize(ctx, req.GetNamespace()); err != nil {

Loading…
Cancel
Save