Zanzana: reconcile generic schema (#95492)

* Rename to CheckObject

* Implement authz.AccessClient

* Move folder tree to reconciler and use new schema

* Move shared functionality to common package

* Add reconciler for managed permissions and resource translations

* Add support for folder resources
This commit is contained in:
Karl Persson 2024-10-28 16:32:16 +01:00 committed by GitHub
parent 7b5c84f366
commit e0163c93c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 496 additions and 142 deletions

View File

@ -236,7 +236,7 @@ func (a *AccessControl) Check(ctx context.Context, req accesscontrol.CheckReques
}
// Check direct access to resource first
res, err := a.zclient.Check(ctx, in)
res, err := a.zclient.CheckObject(ctx, in)
if err != nil {
return false, err
}
@ -262,7 +262,7 @@ func (a *AccessControl) Check(ctx context.Context, req accesscontrol.CheckReques
TupleKey: folderKey,
}
folderRes, err := a.zclient.Check(ctx, folderReq)
folderRes, err := a.zclient.CheckObject(ctx, folderReq)
if err != nil {
return false, err
}

View File

@ -58,6 +58,119 @@ func teamMembershipCollector(store db.DB) legacyTupleCollector {
}
}
// folderTreeCollector collects folder tree structure and writes it as relation tuples
func folderTreeCollector2(store db.DB) legacyTupleCollector {
return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) {
ctx, span := tracer.Start(ctx, "accesscontrol.migrator.folderTreeCollector")
defer span.End()
const query = `
SELECT uid, parent_uid, org_id FROM folder
`
type folder struct {
OrgID int64 `xorm:"org_id"`
FolderUID string `xorm:"uid"`
ParentUID string `xorm:"parent_uid"`
}
var folders []folder
err := store.WithDbSession(ctx, func(sess *db.Session) error {
return sess.SQL(query).Find(&folders)
})
if err != nil {
return nil, err
}
tuples := make(map[string]map[string]*openfgav1.TupleKey)
for _, f := range folders {
var tuple *openfgav1.TupleKey
if f.ParentUID == "" {
continue
}
tuple = &openfgav1.TupleKey{
Object: zanzana.NewTupleEntry("folder2", f.FolderUID, ""),
Relation: zanzana.RelationParent,
User: zanzana.NewTupleEntry("folder2", f.ParentUID, ""),
}
if tuples[tuple.Object] == nil {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
}
tuples[tuple.Object][tuple.String()] = tuple
}
return tuples, nil
}
}
// managedPermissionsCollector collects managed permissions into provided tuple map.
// It will only store actions that are supported by our schema. Managed permissions can
// be directly mapped to user/team/role without having to write an intermediate role.
func managedPermissionsCollector2(store db.DB) legacyTupleCollector {
return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) {
query := `
SELECT u.uid as user_uid, t.uid as team_uid, p.action, p.kind, p.identifier, r.org_id
FROM permission p
INNER JOIN role r ON p.role_id = r.id
LEFT JOIN user_role ur ON r.id = ur.role_id
LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ur.user_id
LEFT JOIN team_role tr ON r.id = tr.role_id
LEFT JOIN team t ON tr.team_id = t.id
LEFT JOIN builtin_role br ON r.id = br.role_id
WHERE r.name LIKE 'managed:%'
`
type Permission struct {
RoleName string `xorm:"role_name"`
OrgID int64 `xorm:"org_id"`
Action string `xorm:"action"`
Kind string
Identifier string
UserUID string `xorm:"user_uid"`
TeamUID string `xorm:"team_uid"`
}
var permissions []Permission
err := store.WithDbSession(ctx, func(sess *db.Session) error {
return sess.SQL(query).Find(&permissions)
})
if err != nil {
return nil, err
}
tuples := make(map[string]map[string]*openfgav1.TupleKey)
for _, p := range permissions {
var subject string
if len(p.UserUID) > 0 {
subject = zanzana.NewTupleEntry(zanzana.TypeUser, p.UserUID, "")
} else if len(p.TeamUID) > 0 {
subject = zanzana.NewTupleEntry(zanzana.TypeTeam, p.TeamUID, "member")
} else {
// FIXME(kalleep): Unsuported role binding (org role). We need to have basic roles in place
continue
}
tuple, ok := zanzana.TranslateToResourceTuple(subject, p.Action, p.Kind, p.Identifier)
if !ok {
continue
}
if tuples[tuple.Object] == nil {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
}
tuples[tuple.Object][tuple.String()] = tuple
}
return tuples, nil
}
}
func zanzanaCollector(client zanzana.Client, relations []string) zanzanaTupleCollector {
return func(ctx context.Context, client zanzana.Client, object string) (map[string]*openfgav1.TupleKey, error) {
// list will use continuation token to collect all tuples for object and relation

View File

@ -65,6 +65,18 @@ func NewZanzanaReconciler(client zanzana.Client, store db.DB, lock *serverlock.S
zanzanaCollector(client, []string{zanzana.RelationTeamMember, zanzana.RelationTeamAdmin}),
client,
),
newResourceReconciler(
"folder tree",
folderTreeCollector2(store),
zanzanaCollector(client, []string{zanzana.RelationParent}),
client,
),
newResourceReconciler(
"managed permissison",
managedPermissionsCollector2(store),
zanzanaCollector(client, zanzana.ResourceRelations),
client,
),
},
}
}
@ -136,7 +148,6 @@ func (r *ZanzanaReconciler) reconcile(ctx context.Context) {
r.log.Debug("Finished reconciliation", "elapsed", time.Since(now))
}
// in tests we can skip creating a lock
if r.lock == nil {
run(ctx)
return

View File

@ -4,18 +4,23 @@ import (
"context"
"fmt"
"google.golang.org/grpc"
"github.com/grafana/authlib/authz"
"github.com/grafana/authlib/claims"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"google.golang.org/grpc"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/authz/zanzana/client"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1"
"github.com/grafana/grafana/pkg/setting"
)
// Client is a wrapper around [openfgav1.OpenFGAServiceClient]
type Client interface {
Check(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error)
authz.AccessClient
List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error)
CheckObject(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error)
Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav1.ReadResponse, error)
ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error)
Write(ctx context.Context, in *openfgav1.WriteRequest) error

View File

@ -11,9 +11,17 @@ import (
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/grafana/authlib/authz"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/log"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1"
)
var _ authz.AccessClient = (*Client)(nil)
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/authz/zanzana/client")
type ClientOption func(c *Client)
@ -32,7 +40,10 @@ func WithLogger(logger log.Logger) ClientOption {
type Client struct {
logger log.Logger
client openfgav1.OpenFGAServiceClient
openfga openfgav1.OpenFGAServiceClient
authz authzv1.AuthzServiceClient
authzext authzextv1.AuthzExtentionServiceClient
tenantID string
storeID string
modelID string
@ -40,7 +51,9 @@ type Client struct {
func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption) (*Client, error) {
c := &Client{
client: openfgav1.NewOpenFGAServiceClient(cc),
openfga: openfgav1.NewOpenFGAServiceClient(cc),
authz: authzv1.NewAuthzServiceClient(cc),
authzext: authzextv1.NewAuthzExtentionServiceClient(cc),
}
for _, o := range opts {
@ -72,13 +85,70 @@ func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption)
return c, nil
}
func (c *Client) Check(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) {
// Check implements authz.AccessClient.
func (c *Client) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckRequest) (authz.CheckResponse, error) {
ctx, span := tracer.Start(ctx, "authz.zanzana.client.Check")
defer span.End()
res, err := c.authz.Check(ctx, &authzv1.CheckRequest{
Subject: id.GetUID(),
Verb: req.Verb,
Group: req.Group,
Resource: req.Resource,
Namespace: req.Namespace,
Name: req.Name,
Subresource: req.Subresource,
Path: req.Path,
Folder: req.Folder,
})
if err != nil {
return authz.CheckResponse{}, err
}
return authz.CheckResponse{Allowed: res.GetAllowed()}, nil
}
func (c *Client) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, error) {
ctx, span := tracer.Start(ctx, "authz.zanzana.client.Compile")
defer span.End()
_, err := c.authzext.List(ctx, &authzextv1.ListRequest{
Subject: id.GetUID(),
Group: req.Group,
Verb: utils.VerbList,
Resource: req.Resource,
Namespace: req.Namespace,
})
if err != nil {
return nil, err
}
// FIXME: implement checker
return func(namespace, name, folder string) bool { return false }, nil
}
func (c *Client) List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error) {
ctx, span := tracer.Start(ctx, "authz.zanzana.client.List")
defer span.End()
return c.authzext.List(ctx, &authzextv1.ListRequest{
Subject: id.GetUID(),
Group: req.Group,
Verb: utils.VerbList,
Resource: req.Resource,
Namespace: req.Namespace,
})
}
func (c *Client) CheckObject(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) {
ctx, span := tracer.Start(ctx, "authz.zanzana.client.CheckObject")
defer span.End()
in.StoreId = c.storeID
in.AuthorizationModelId = c.modelID
return c.client.Check(ctx, in)
return c.openfga.Check(ctx, in)
}
func (c *Client) Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav1.ReadResponse, error) {
@ -86,7 +156,7 @@ func (c *Client) Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav
defer span.End()
in.StoreId = c.storeID
return c.client.Read(ctx, in)
return c.openfga.Read(ctx, in)
}
func (c *Client) ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
@ -96,13 +166,13 @@ func (c *Client) ListObjects(ctx context.Context, in *openfgav1.ListObjectsReque
in.StoreId = c.storeID
in.AuthorizationModelId = c.modelID
return c.client.ListObjects(ctx, in)
return c.openfga.ListObjects(ctx, in)
}
func (c *Client) Write(ctx context.Context, in *openfgav1.WriteRequest) error {
in.StoreId = c.storeID
in.AuthorizationModelId = c.modelID
_, err := c.client.Write(ctx, in)
_, err := c.openfga.Write(ctx, in)
return err
}
@ -115,7 +185,7 @@ func (c *Client) getStore(ctx context.Context, name string) (*openfgav1.Store, e
// We should create an issue to support some way to get stores by name.
// For now we need to go thourh all stores until we find a match or we hit the end.
for {
res, err := c.client.ListStores(ctx, &openfgav1.ListStoresRequest{
res, err := c.openfga.ListStores(ctx, &openfgav1.ListStoresRequest{
PageSize: &wrapperspb.Int32Value{Value: 20},
ContinuationToken: continuationToken,
})
@ -142,7 +212,7 @@ func (c *Client) getStore(ctx context.Context, name string) (*openfgav1.Store, e
func (c *Client) loadModel(ctx context.Context, storeID string) (string, error) {
// ReadAuthorizationModels returns authorization models for a store sorted in descending order of creation.
// So with a pageSize of 1 we will get the latest model.
res, err := c.client.ReadAuthorizationModels(ctx, &openfgav1.ReadAuthorizationModelsRequest{
res, err := c.openfga.ReadAuthorizationModels(ctx, &openfgav1.ReadAuthorizationModelsRequest{
StoreId: storeID,
PageSize: &wrapperspb.Int32Value{Value: 1},
})

View File

@ -3,16 +3,34 @@ package client
import (
"context"
"github.com/grafana/authlib/authz"
"github.com/grafana/authlib/claims"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1"
)
var _ authz.AccessClient = (*NoopClient)(nil)
func NewNoop() *NoopClient {
return &NoopClient{}
}
type NoopClient struct{}
func (nc NoopClient) Check(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) {
func (nc *NoopClient) Check(ctx context.Context, id claims.AuthInfo, req authz.CheckRequest) (authz.CheckResponse, error) {
return authz.CheckResponse{}, nil
}
func (nc *NoopClient) Compile(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (authz.ItemChecker, error) {
return nil, nil
}
func (nc *NoopClient) List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error) {
return nil, nil
}
func (nc NoopClient) CheckObject(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) {
return nil, nil
}

View File

@ -0,0 +1,34 @@
package common
import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
)
type TypeInfo struct {
Type string
}
var typedResources = map[string]TypeInfo{
NewNamespaceResourceIdent(
folderalpha1.FolderResourceInfo.GroupResource().Group,
folderalpha1.FolderResourceInfo.GroupResource().Resource,
): TypeInfo{Type: "folder2"},
}
func GetTypeInfo(group, resource string) (TypeInfo, bool) {
info, ok := typedResources[NewNamespaceResourceIdent(group, resource)]
return info, ok
}
var VerbMapping = map[string]string{
utils.VerbGet: "read",
utils.VerbList: "read",
utils.VerbWatch: "read",
utils.VerbCreate: "create",
utils.VerbUpdate: "write",
utils.VerbPatch: "write",
utils.VerbDelete: "delete",
utils.VerbDeleteCollection: "delete",
}

View File

@ -0,0 +1,86 @@
package common
import (
"fmt"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"google.golang.org/protobuf/types/known/structpb"
)
const (
resourceType = "resource"
namespaceType = "namespace"
folderResourceType = "folder_resource"
)
func NewTypedIdent(typ string, name string) string {
return fmt.Sprintf("%s:%s", typ, name)
}
func NewResourceIdent(group, resource, name string) string {
return fmt.Sprintf("%s:%s/%s", resourceType, FormatGroupResource(group, resource), name)
}
func NewFolderResourceIdent(group, resource, folder string) string {
return fmt.Sprintf("%s:%s/%s", folderResourceType, FormatGroupResource(group, resource), folder)
}
func NewNamespaceResourceIdent(group, resource string) string {
return fmt.Sprintf("%s:%s", namespaceType, FormatGroupResource(group, resource))
}
func FormatGroupResource(group, resource string) string {
return fmt.Sprintf("%s/%s", group, resource)
}
func NewResourceTuple(subject, relation, group, resource, name string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: NewResourceIdent(group, resource, name),
Condition: &openfgav1.RelationshipCondition{
Name: "group_filter",
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"resource_group": structpb.NewStringValue(FormatGroupResource(group, resource)),
},
},
},
}
}
func NewFolderResourceTuple(subject, relation, group, resource, folder string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: NewFolderResourceIdent(group, resource, folder),
Condition: &openfgav1.RelationshipCondition{
Name: "group_filter",
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"resource_group": structpb.NewStringValue(FormatGroupResource(group, resource)),
},
},
},
}
}
func NewNamespaceResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: NewNamespaceResourceIdent(group, resource),
}
}
func NewFolderTuple(subject, relation, name string) *openfgav1.TupleKey {
return NewTypedTuple("folder2", subject, relation, name)
}
func NewTypedTuple(typ, subject, relation, name string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: NewTypedIdent(typ, name),
}
}

View File

@ -11,8 +11,6 @@ import (
"go.opentelemetry.io/otel"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/grafana/grafana/pkg/apimachinery/utils"
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/infra/log"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana/schema"
@ -199,47 +197,3 @@ func (s *Server) loadModel(ctx context.Context, storeID string, modules []transf
return writeRes.GetAuthorizationModelId(), nil
}
func newTypedIdent(typ string, name string) string {
return fmt.Sprintf("%s:%s", typ, name)
}
func newResourceIdent(group, resource, name string) string {
return fmt.Sprintf("%s:%s/%s", resourceType, formatGroupResource(group, resource), name)
}
func newFolderResourceIdent(group, resource, folder string) string {
return fmt.Sprintf("%s:%s/%s", folderResourceType, formatGroupResource(group, resource), folder)
}
func newNamespaceResourceIdent(group, resource string) string {
return fmt.Sprintf("%s:%s", namespaceType, formatGroupResource(group, resource))
}
func formatGroupResource(group, resource string) string {
return fmt.Sprintf("%s/%s", group, resource)
}
type TypeInfo struct {
typ string
}
var typedResources = map[string]TypeInfo{
newNamespaceResourceIdent(folderalpha1.GROUP, folderalpha1.RESOURCE): TypeInfo{typ: "folder2"},
}
func typeInfo(group, resource string) (TypeInfo, bool) {
info, ok := typedResources[newNamespaceResourceIdent(group, resource)]
return info, ok
}
var mapping = map[string]string{
utils.VerbGet: "read",
utils.VerbList: "read",
utils.VerbWatch: "read",
utils.VerbCreate: "create",
utils.VerbUpdate: "write",
utils.VerbPatch: "write",
utils.VerbDelete: "delete",
utils.VerbDeleteCollection: "delete",
}

View File

@ -4,6 +4,7 @@ import (
"context"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"google.golang.org/protobuf/types/known/structpb"
)
@ -12,14 +13,14 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
ctx, span := tracer.Start(ctx, "authzServer.Check")
defer span.End()
if info, ok := typeInfo(r.GetGroup(), r.GetResource()); ok {
if info, ok := common.GetTypeInfo(r.GetGroup(), r.GetResource()); ok {
return s.checkTyped(ctx, r, info)
}
return s.checkGeneric(ctx, r)
}
func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info TypeInfo) (*authzv1.CheckResponse, error) {
relation := mapping[r.GetVerb()]
func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info common.TypeInfo) (*authzv1.CheckResponse, error) {
relation := common.VerbMapping[r.GetVerb()]
// 1. check if subject has direct access to resource
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
@ -28,7 +29,7 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info T
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: newTypedIdent(info.typ, r.GetName()),
Object: common.NewTypedIdent(info.Type, r.GetName()),
},
})
if err != nil {
@ -46,7 +47,7 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info T
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
},
})
if err != nil {
@ -57,7 +58,7 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info T
}
func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) {
relation := mapping[r.GetVerb()]
relation := common.VerbMapping[r.GetVerb()]
// 1. check if subject has direct access to resource
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: s.storeID,
@ -65,11 +66,11 @@ func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*au
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: newResourceIdent(r.GetGroup(), r.GetResource(), r.GetName()),
Object: common.NewResourceIdent(r.GetGroup(), r.GetResource(), r.GetName()),
},
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())),
"requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())),
},
},
})
@ -90,7 +91,7 @@ func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*au
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
},
})
@ -113,11 +114,11 @@ func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*au
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: newFolderResourceIdent(r.GetGroup(), r.GetResource(), r.GetFolder()),
Object: common.NewFolderResourceIdent(r.GetGroup(), r.GetResource(), r.GetFolder()),
},
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())),
"requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())),
},
},
})

View File

@ -8,7 +8,7 @@ import (
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"google.golang.org/protobuf/types/known/structpb"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/zanzana/proto/v1"
)
@ -16,14 +16,14 @@ func (s *Server) List(ctx context.Context, r *authzextv1.ListRequest) (*authzext
ctx, span := tracer.Start(ctx, "authzServer.List")
defer span.End()
if info, ok := typeInfo(r.GetGroup(), r.GetResource()); ok {
if info, ok := common.GetTypeInfo(r.GetGroup(), r.GetResource()); ok {
return s.listTyped(ctx, r, info)
}
return s.listGeneric(ctx, r)
}
func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info TypeInfo) (*authzextv1.ListResponse, error) {
relation := mapping[r.GetVerb()]
func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info common.TypeInfo) (*authzextv1.ListResponse, error) {
relation := common.VerbMapping[r.GetVerb()]
// 1. check if subject has access through namespace because then they can read all of them
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
@ -32,7 +32,7 @@ func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
},
})
if err != nil {
@ -47,8 +47,8 @@ func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info
listRes, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: s.storeID,
AuthorizationModelId: s.modelID,
Type: info.typ,
Relation: mapping[utils.VerbGet],
Type: info.Type,
Relation: relation,
User: r.GetSubject(),
})
if err != nil {
@ -56,12 +56,12 @@ func (s *Server) listTyped(ctx context.Context, r *authzextv1.ListRequest, info
}
return &authzextv1.ListResponse{
Items: typedObjects(info.typ, listRes.GetObjects()),
Items: typedObjects(info.Type, listRes.GetObjects()),
}, nil
}
func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*authzextv1.ListResponse, error) {
relation := mapping[r.GetVerb()]
relation := common.VerbMapping[r.GetVerb()]
// 1. check if subject has access through namespace because then they can read all of them
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
@ -70,7 +70,7 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a
TupleKey: &openfgav1.CheckRequestTupleKey{
User: r.GetSubject(),
Relation: relation,
Object: newNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
},
})
if err != nil {
@ -90,7 +90,7 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a
User: r.GetSubject(),
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())),
"requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())),
},
},
})
@ -107,7 +107,7 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a
User: r.GetSubject(),
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"requested_group": structpb.NewStringValue(formatGroupResource(r.GetGroup(), r.GetResource())),
"requested_group": structpb.NewStringValue(common.FormatGroupResource(r.GetGroup(), r.GetResource())),
},
},
})

View File

@ -6,10 +6,10 @@ import (
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
"github.com/grafana/grafana/pkg/services/authz/zanzana/store"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/setting"
@ -67,65 +67,17 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
AuthorizationModelId: srv.modelID,
Writes: &openfgav1.WriteRequestWrites{
TupleKeys: []*openfgav1.TupleKey{
newResourceTuple("user:1", "read", dashboardGroup, dashboardResource, "1"),
newNamespaceResourceTuple("user:2", "read", dashboardGroup, dashboardResource),
newResourceTuple("user:3", "view", dashboardGroup, dashboardResource, "1"),
newFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "1"),
newFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "3"),
newFolderResourceTuple("user:5", "view", dashboardGroup, dashboardResource, "1"),
newFolderTuple("user:6", "read", "1"),
newNamespaceResourceTuple("user:7", "read", folderGroup, folderResource),
common.NewResourceTuple("user:1", "read", dashboardGroup, dashboardResource, "1"),
common.NewNamespaceResourceTuple("user:2", "read", dashboardGroup, dashboardResource),
common.NewResourceTuple("user:3", "view", dashboardGroup, dashboardResource, "1"),
common.NewFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "1"),
common.NewFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "3"),
common.NewFolderResourceTuple("user:5", "view", dashboardGroup, dashboardResource, "1"),
common.NewFolderTuple("user:6", "read", "1"),
common.NewNamespaceResourceTuple("user:7", "read", folderGroup, folderResource),
},
},
})
require.NoError(t, err)
return srv
}
func newResourceTuple(subject, relation, group, resource, name string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: newResourceIdent(group, resource, name),
Condition: &openfgav1.RelationshipCondition{
Name: "group_filter",
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"resource_group": structpb.NewStringValue(formatGroupResource(group, resource)),
},
},
},
}
}
func newFolderResourceTuple(subject, relation, group, resource, folder string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: newFolderResourceIdent(group, resource, folder),
Condition: &openfgav1.RelationshipCondition{
Name: "group_filter",
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"resource_group": structpb.NewStringValue(formatGroupResource(group, resource)),
},
},
},
}
}
func newNamespaceResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: newNamespaceResourceIdent(group, resource),
}
}
func newFolderTuple(subject, relation, name string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: newTypedIdent("folder2", name),
}
}

View File

@ -1,5 +1,10 @@
package zanzana
import (
dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
)
type actionKindTranslation struct {
objectType string
orgScoped bool
@ -102,3 +107,67 @@ var basicRolesTranslations = map[string]string{
RoleViewer: "basic_viewer",
RoleNone: "basic_none",
}
type resourceTranslation struct {
typ string
group string
resource string
mapping map[string]actionMappig
}
type actionMappig struct {
relation string
group string
resource string
}
func newMapping(relation string) actionMappig {
return newScopedMapping(relation, "", "")
}
func newScopedMapping(relation, group, resource string) actionMappig {
return actionMappig{relation, group, resource}
}
var (
folderGroup = folderalpha1.FolderResourceInfo.GroupResource().Group
folderResource = folderalpha1.FolderResourceInfo.GroupResource().Resource
dashboardGroup = dashboardalpha1.DashboardResourceInfo.GroupResource().Group
dashboardResource = dashboardalpha1.DashboardResourceInfo.GroupResource().Resource
)
var resourceTranslations = map[string]resourceTranslation{
KindFolders: {
typ: TypeFolder2,
group: folderGroup,
resource: folderResource,
mapping: map[string]actionMappig{
"folders:read": newMapping(RelationRead),
"folders:write": newMapping(RelationWrite),
"folders:create": newMapping(RelationCreate),
"folders:delete": newMapping(RelationDelete),
"folders.permissions:read": newMapping(RelationPermissionsRead),
"folders.permissions:write": newMapping(RelationPermissionsWrite),
"dashboards:read": newScopedMapping(RelationRead, dashboardGroup, dashboardResource),
"dashboards:write": newScopedMapping(RelationWrite, dashboardGroup, dashboardResource),
"dashboards:create": newScopedMapping(RelationCreate, dashboardGroup, dashboardResource),
"dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource),
"dashboards.permissions:read": newScopedMapping(RelationPermissionsRead, dashboardGroup, dashboardResource),
"dashboards.permissions:write": newScopedMapping(RelationPermissionsWrite, dashboardGroup, dashboardResource),
},
},
KindDashboards: {
typ: TypeResource,
group: dashboardGroup,
resource: dashboardResource,
mapping: map[string]actionMappig{
"dashboards:read": newMapping(RelationRead),
"dashboards:write": newMapping(RelationWrite),
"dashboards:create": newMapping(RelationCreate),
"dashboards:delete": newMapping(RelationDelete),
"dashboards.permissions:read": newMapping(RelationPermissionsRead),
"dashboards.permissions:write": newMapping(RelationPermissionsWrite),
},
},
}

View File

@ -5,6 +5,7 @@ import (
"strconv"
"strings"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
@ -13,8 +14,10 @@ const (
TypeTeam string = "team"
TypeRole string = "role"
TypeFolder string = "folder"
TypeFolder2 string = "folder2"
TypeDashboard string = "dashboard"
TypeOrg string = "org"
TypeResource string = "resource"
)
const (
@ -23,8 +26,19 @@ const (
RelationParent string = "parent"
RelationAssignee string = "assignee"
RelationOrg string = "org"
// FIXME action sets
RelationAdmin string = "admin"
RelationRead string = "read"
RelationWrite string = "write"
RelationCreate string = "create"
RelationDelete string = "delete"
RelationPermissionsRead string = "permissions_read"
RelationPermissionsWrite string = "permissions_write"
)
var ResourceRelations = []string{RelationRead, RelationWrite, RelationCreate, RelationDelete, RelationPermissionsRead, RelationPermissionsWrite}
const (
KindOrg string = "org"
KindDashboards string = "dashboards"
@ -89,6 +103,33 @@ func TranslateToTuple(user string, action, kind, identifier string, orgID int64)
return tuple, true
}
func TranslateToResourceTuple(subject string, action, kind, name string) (*openfgav1.TupleKey, bool) {
translation, ok := resourceTranslations[kind]
if !ok {
return nil, false
}
m, ok := translation.mapping[action]
if !ok {
return nil, false
}
if translation.typ == TypeResource {
return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, name), true
}
if translation.typ == TypeFolder2 {
if m.group != "" && m.resource != "" {
return common.NewFolderResourceTuple(subject, m.relation, m.group, m.resource, name), true
}
return common.NewFolderTuple(subject, m.relation, name), true
}
return common.NewTypedTuple(translation.typ, subject, m.relation, name), true
}
func TranslateToOrgTuple(user string, action string, orgID int64) (*openfgav1.TupleKey, bool) {
typeTranslation, ok := actionKindTranslations[KindOrg]
if !ok {