Zanzana: Support sub resources (#98201)

* Create and use common ResourceInfo struct

* Add support for formatting group resource with subresource

* Add initial support for handling subresource

* Add test for checking subresource for generic resource

* Bump authlib
This commit is contained in:
Karl Persson 2025-01-07 15:16:14 +01:00 committed by GitHub
parent 9e3094d68e
commit 9ed4bf3cd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 438 additions and 202 deletions

2
go.mod
View File

@ -74,7 +74,7 @@ require (
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20241211182001-0f317eb6b2f7 // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8 // @grafana/identity-access-team
github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f // @grafana/identity-access-team
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 // @grafana/identity-access-team
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d // @grafana/dataviz-squad
github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code

4
go.sum
View File

@ -2278,8 +2278,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20241211182001-0f317eb6b2f7 h1:VGLUQ2mwzlF1NGwTxpSfv1RnuOsDlNh/NT5KRvhZ0sQ=
github.com/grafana/alerting v0.0.0-20241211182001-0f317eb6b2f7/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8 h1:ctiV5lFI0zdccm4uRLSHIpdUukLvDNc9KGtYKal2eXE=
github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI=
github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f h1:BcgUTu26JtOudfpQ8LoLpZNV2CdMEyhLUZweCUgETZw=
github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI=
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk=
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=

View File

@ -656,6 +656,7 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI=
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I=
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw=
github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak=

View File

@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery
go 1.23.1
require (
github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8 // @grafana/identity-access-team
github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f // @grafana/identity-access-team
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 // @grafana/identity-access-team
github.com/stretchr/testify v1.10.0
k8s.io/apimachinery v0.32.0

View File

@ -28,8 +28,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8 h1:ctiV5lFI0zdccm4uRLSHIpdUukLvDNc9KGtYKal2eXE=
github.com/grafana/authlib v0.0.0-20241220142117-e573433309e8/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI=
github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f h1:BcgUTu26JtOudfpQ8LoLpZNV2CdMEyhLUZweCUgETZw=
github.com/grafana/authlib v0.0.0-20250107102310-3edeb9fc9d5f/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI=
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk=
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=

View File

@ -394,6 +394,19 @@ func rolePermissionsCollector(store db.DB) legacyTupleCollector {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
}
// For resource actions on folders we need to merge the tuples into one with combined
// group_resources.
if zanzana.IsFolderResourceTuple(tuple) {
key := tupleStringWithoutCondition(tuple)
if t, ok := tuples[tuple.Object][key]; ok {
zanzana.MergeFolderResourceTuples(t, tuple)
} else {
tuples[tuple.Object][key] = tuple
}
continue
}
tuples[tuple.Object][tuple.String()] = tuple
}

View File

@ -1,46 +1,122 @@
package common
import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
"google.golang.org/protobuf/types/known/structpb"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
)
type TypeInfo struct {
type typeInfo struct {
Type string
Relations []string
}
func (t TypeInfo) IsValidRelation(relation string) bool {
return isValidRelation(relation, t.Relations)
}
var typedResources = map[string]TypeInfo{
var typedResources = map[string]typeInfo{
FormatGroupResource(
folderalpha1.FolderResourceInfo.GroupResource().Group,
folderalpha1.FolderResourceInfo.GroupResource().Resource,
"",
): {Type: "folder", Relations: RelationsFolder},
}
func GetTypeInfo(group, resource string) (TypeInfo, bool) {
info, ok := typedResources[FormatGroupResource(group, resource)]
func getTypeInfo(group, resource string) (typeInfo, bool) {
info, ok := typedResources[FormatGroupResource(group, resource, "")]
return info, ok
}
var VerbMapping = map[string]string{
utils.VerbGet: RelationGet,
utils.VerbList: RelationGet,
utils.VerbWatch: RelationGet,
utils.VerbCreate: RelationCreate,
utils.VerbUpdate: RelationUpdate,
utils.VerbPatch: RelationUpdate,
utils.VerbDelete: RelationDelete,
utils.VerbDeleteCollection: RelationDelete,
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)
}
var RelationToVerbMapping = map[string]string{
RelationGet: utils.VerbGet,
RelationCreate: utils.VerbCreate,
RelationUpdate: utils.VerbUpdate,
RelationDelete: utils.VerbDelete,
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)
}
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)
}
return newResource(TypeResource, r.GetGroup(), r.GetResource(), "", "", r.GetSubresource(), RelationsResource)
}
func newResource(typ string, group, resource, name, folder, subresource string, relations []string) ResourceInfo {
return ResourceInfo{
typ: typ,
group: group,
resource: resource,
name: name,
folder: folder,
subresource: subresource,
relations: relations,
}
}
type ResourceInfo struct {
typ string
group string
resource string
name string
folder string
subresource string
relations []string
}
func (r ResourceInfo) GroupResource() string {
return FormatGroupResource(r.group, r.resource, r.subresource)
}
func (r ResourceInfo) GroupResourceIdent() string {
return NewGroupResourceIdent(r.group, r.resource, r.subresource)
}
func (r ResourceInfo) ResourceIdent() string {
if r.name == "" {
return ""
}
if r.IsGeneric() {
return NewResourceIdent(r.group, r.resource, r.subresource, r.name)
}
return NewTypedIdent(r.typ, r.name)
}
func (r ResourceInfo) FolderIdent() string {
if r.folder == "" {
return ""
}
return NewFolderIdent(r.folder)
}
func (r ResourceInfo) IsGeneric() bool {
return r.typ == TypeResource
}
func (r ResourceInfo) Type() string {
return r.typ
}
func (r ResourceInfo) Context() *structpb.Struct {
if !r.IsGeneric() {
return nil
}
return &structpb.Struct{
Fields: map[string]*structpb.Value{
"requested_group": structpb.NewStringValue(r.GroupResource()),
},
}
}
func (r ResourceInfo) IsValidRelation(relation string) bool {
return isValidRelation(relation, r.relations)
}

View File

@ -6,10 +6,13 @@ import (
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"google.golang.org/protobuf/types/known/structpb"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
)
const ClusterNamespace = "cluster"
const (
TypeUser string = "user"
TypeServiceAccount string = "service-account"
@ -82,7 +85,25 @@ var RelationsFolder = append(
RelationDelete,
)
const ClusterNamespace = "cluster"
// VerbMapping is mapping a k8s verb to a zanzana relation.
var VerbMapping = map[string]string{
utils.VerbGet: RelationGet,
utils.VerbList: RelationGet,
utils.VerbWatch: RelationGet,
utils.VerbCreate: RelationCreate,
utils.VerbUpdate: RelationUpdate,
utils.VerbPatch: RelationUpdate,
utils.VerbDelete: RelationDelete,
utils.VerbDeleteCollection: RelationDelete,
}
// RelationToVerbMapping is mapping a zanzana relation to k8s verb.
var RelationToVerbMapping = map[string]string{
RelationGet: utils.VerbGet,
RelationCreate: utils.VerbCreate,
RelationUpdate: utils.VerbUpdate,
RelationDelete: utils.VerbDelete,
}
func IsGroupResourceRelation(relation string) bool {
return isValidRelation(relation, RelationsGroupResource)
@ -92,10 +113,6 @@ func IsFolderResourceRelation(relation string) bool {
return isValidRelation(relation, RelationsFolderResource)
}
func IsResourceRelation(relation string) bool {
return isValidRelation(relation, RelationsResource)
}
func isValidRelation(relation string, valid []string) bool {
for _, r := range valid {
if r == relation {
@ -113,32 +130,36 @@ 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", TypeResource, FormatGroupResource(group, resource), name)
func NewResourceIdent(group, resource, subresource, name string) string {
return fmt.Sprintf("%s:%s/%s", TypeResource, FormatGroupResource(group, resource, subresource), name)
}
func NewFolderIdent(name string) string {
return fmt.Sprintf("%s:%s", TypeFolder, name)
}
func NewGroupResourceIdent(group, resource string) string {
return fmt.Sprintf("%s:%s", TypeGroupResouce, FormatGroupResource(group, resource))
func NewGroupResourceIdent(group, resource, subresource string) string {
return fmt.Sprintf("%s:%s", TypeGroupResouce, FormatGroupResource(group, resource, subresource))
}
func FormatGroupResource(group, resource string) string {
func FormatGroupResource(group, resource, subresource string) string {
if subresource != "" {
return fmt.Sprintf("%s/%s/%s", group, resource, subresource)
}
return fmt.Sprintf("%s/%s", group, resource)
}
func NewResourceTuple(subject, relation, group, resource, name string) *openfgav1.TupleKey {
func NewResourceTuple(subject, relation, group, resource, subresource, name string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: NewResourceIdent(group, resource, name),
Object: NewResourceIdent(group, resource, subresource, name),
Condition: &openfgav1.RelationshipCondition{
Name: "group_filter",
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"group_resource": structpb.NewStringValue(FormatGroupResource(group, resource)),
"group_resource": structpb.NewStringValue(FormatGroupResource(group, resource, subresource)),
},
},
},
@ -151,7 +172,7 @@ func isFolderResourceRelationSet(relation string) bool {
relation == RelationFolderResourceSetAdmin
}
func NewFolderResourceTuple(subject, relation, group, resource, folder string) *openfgav1.TupleKey {
func NewFolderResourceTuple(subject, relation, group, resource, subresource, folder string) *openfgav1.TupleKey {
relation = FolderResourceRelation(relation)
var condition *openfgav1.RelationshipCondition
if !isFolderResourceRelationSet(relation) {
@ -160,7 +181,7 @@ func NewFolderResourceTuple(subject, relation, group, resource, folder string) *
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"group_resources": structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{structpb.NewStringValue(FormatGroupResource(group, resource))},
Values: []*structpb.Value{structpb.NewStringValue(FormatGroupResource(group, resource, subresource))},
}),
},
},
@ -175,18 +196,18 @@ func NewFolderResourceTuple(subject, relation, group, resource, folder string) *
}
}
func NewGroupResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey {
func NewGroupResourceTuple(subject, relation, group, resource, subresource string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: NewGroupResourceIdent(group, resource),
Object: NewGroupResourceIdent(group, resource, subresource),
}
}
func NewFolderParentTuple(folder, parent string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
Object: NewFolderIdent(folder),
Relation: "parent",
Relation: RelationParent,
User: NewFolderIdent(parent),
}
}
@ -306,14 +327,7 @@ func AddRenderContext(req *openfgav1.CheckRequest) {
Object: NewGroupResourceIdent(
dashboardalpha1.DashboardResourceInfo.GroupResource().Group,
dashboardalpha1.DashboardResourceInfo.GroupResource().Resource,
"",
),
})
}
func NewResourceContext(group, resource string) *structpb.Struct {
return &structpb.Struct{
Fields: map[string]*structpb.Value{
"requested_group": structpb.NewStringValue(FormatGroupResource(group, resource)),
},
}
}

View File

@ -30,7 +30,7 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest
return nil, err
}
groupResource := common.FormatGroupResource(item.GetGroup(), item.GetResource())
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),
@ -51,12 +51,13 @@ func (s *Server) batchCheckItem(
) (*authzv1.CheckResponse, error) {
var (
relation = common.VerbMapping[item.GetVerb()]
groupResource = common.FormatGroupResource(item.GetGroup(), item.GetResource())
resource = common.NewResourceInfoFromBatchItem(item)
groupResource = resource.GroupResource()
)
allowed, ok := groupResourceAccess[groupResource]
if !ok {
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, item.GetGroup(), item.GetResource(), store)
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, store)
if err != nil {
return nil, err
}
@ -69,8 +70,9 @@ func (s *Server) batchCheckItem(
return &authzv1.CheckResponse{Allowed: true}, nil
}
if info, ok := common.GetTypeInfo(item.GetGroup(), item.GetResource()); ok {
return s.checkTyped(ctx, r.GetSubject(), relation, item.GetName(), info, store)
if resource.IsGeneric() {
return s.checkGeneric(ctx, r.GetSubject(), relation, resource, store)
}
return s.checkGeneric(ctx, r.GetSubject(), relation, item.GetGroup(), item.GetResource(), item.GetName(), item.GetFolder(), store)
return s.checkTyped(ctx, r.GetSubject(), relation, resource, store)
}

View File

@ -9,18 +9,19 @@ import (
"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"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
)
func testBatchCheck(t *testing.T, server *Server) {
newReq := func(subject, verb, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
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,
Name: item.GetName(),
Folder: item.GetFolder(),
Verb: verb,
Group: group,
Resource: resource,
Subresource: subresource,
Name: item.GetName(),
Folder: item.GetFolder(),
}
}
@ -32,8 +33,8 @@ func testBatchCheck(t *testing.T, server *Server) {
}
t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
@ -45,8 +46,8 @@ func testBatchCheck(t *testing.T, server *Server) {
})
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through group_resource", func(t *testing.T) {
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
@ -55,8 +56,8 @@ func testBatchCheck(t *testing.T, server *Server) {
})
t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
@ -68,8 +69,8 @@ func testBatchCheck(t *testing.T, server *Server) {
})
t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "3"},
{Name: "3", Folder: "2"},
@ -83,8 +84,8 @@ func testBatchCheck(t *testing.T, server *Server) {
})
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 := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
{Name: "2", Folder: "2"},
}))
@ -96,8 +97,8 @@ func testBatchCheck(t *testing.T, server *Server) {
})
t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
groupResource := zanzana.FormatGroupResource(folderGroup, folderResource)
res, err := server.BatchCheck(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{
groupResource := common.FormatGroupResource(folderGroup, folderResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
{Name: "1"},
{Name: "2"},
}))
@ -109,8 +110,8 @@ func testBatchCheck(t *testing.T, server *Server) {
})
t.Run("user:7 should be able to read folder {1,2} through group_resource access", func(t *testing.T) {
groupResource := zanzana.FormatGroupResource(folderGroup, folderResource)
res, err := server.BatchCheck(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{
groupResource := common.FormatGroupResource(folderGroup, folderResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
{Name: "1"},
{Name: "2"},
}))
@ -121,8 +122,8 @@ func testBatchCheck(t *testing.T, server *Server) {
})
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 := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "10", Folder: "6"},
{Name: "20", Folder: "6"},
}))
@ -133,8 +134,8 @@ func testBatchCheck(t *testing.T, server *Server) {
})
t.Run("user:9 should be able to create dashboards in folder 6 through folder 5", func(t *testing.T) {
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
res, err := server.BatchCheck(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
{Name: "10", Folder: "6"},
{Name: "20", Folder: "6"},
}))
@ -144,4 +145,50 @@ func testBatchCheck(t *testing.T, server *Server) {
require.True(t, res.Groups[groupResource].Items["10"])
require.True(t, res.Groups[groupResource].Items["20"])
})
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(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
{Name: "10", Folder: "6"},
{Name: "11", Folder: "6"},
{Name: "12", Folder: "6"},
}))
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"])
})
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(context.Background(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
{Name: "10", Folder: "6"},
{Name: "11", Folder: "6"},
{Name: "12", Folder: "6"},
}))
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"])
})
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(context.Background(), 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"},
}))
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"])
})
}

View File

@ -21,7 +21,9 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
}
relation := common.VerbMapping[r.GetVerb()]
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
resource := common.NewResourceInfoFromCheck(r)
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, store)
if err != nil {
return nil, err
}
@ -30,15 +32,16 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
return res, nil
}
if info, ok := common.GetTypeInfo(r.GetGroup(), r.GetResource()); ok {
return s.checkTyped(ctx, r.GetSubject(), relation, r.GetName(), info, store)
if resource.IsGeneric() {
return s.checkGeneric(ctx, r.GetSubject(), relation, resource, store)
}
return s.checkGeneric(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), r.GetName(), r.GetFolder(), store)
return s.checkTyped(ctx, r.GetSubject(), relation, resource, 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, group, resource string, store *storeInfo) (*authzv1.CheckResponse, error) {
func (s *Server) checkGroupResource(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.CheckResponse, error) {
if !common.IsGroupResourceRelation(relation) {
return &authzv1.CheckResponse{Allowed: false}, nil
}
@ -49,7 +52,7 @@ func (s *Server) checkGroupResource(ctx context.Context, subject, relation, grou
TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject,
Relation: relation,
Object: common.NewGroupResourceIdent(group, resource),
Object: resource.GroupResourceIdent(),
},
}
@ -66,8 +69,8 @@ func (s *Server) checkGroupResource(ctx context.Context, subject, relation, grou
}
// checkTyped checks on our typed resources e.g. folder.
func (s *Server) checkTyped(ctx context.Context, subject, relation, name string, info common.TypeInfo, store *storeInfo) (*authzv1.CheckResponse, error) {
if !info.IsValidRelation(relation) {
func (s *Server) checkTyped(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.CheckResponse, error) {
if !resource.IsValidRelation(relation) {
return &authzv1.CheckResponse{Allowed: false}, nil
}
@ -78,7 +81,7 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation, name string,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject,
Relation: relation,
Object: common.NewTypedIdent(info.Type, name),
Object: resource.ResourceIdent(),
},
})
if err != nil {
@ -95,21 +98,22 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation, name string,
// 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, group, resource, name, folder string, store *storeInfo) (*authzv1.CheckResponse, error) {
func (s *Server) checkGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.CheckResponse, error) {
var (
resourceCtx = common.NewResourceContext(group, resource)
folderIdent = resource.FolderIdent()
resourceCtx = resource.Context()
folderRelation = common.FolderResourceRelation(relation)
)
if folder != "" && common.IsFolderResourceRelation(folderRelation) {
if folderIdent != "" && common.IsFolderResourceRelation(folderRelation) {
// Check if subject has access as a sub resource for the folder
res, err := s.check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject,
Relation: common.FolderResourceRelation(relation),
Object: common.NewFolderIdent(folder),
Relation: folderRelation,
Object: folderIdent,
},
Context: resourceCtx,
})
@ -123,7 +127,8 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res
}
}
if !common.IsResourceRelation(relation) {
resourceIdent := resource.ResourceIdent()
if !resource.IsValidRelation(relation) || resourceIdent == "" {
return &authzv1.CheckResponse{Allowed: false}, nil
}
@ -134,7 +139,7 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res
TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject,
Relation: relation,
Object: common.NewResourceIdent(group, resource, name),
Object: resourceIdent,
},
Context: resourceCtx,
})

View File

@ -12,104 +12,145 @@ import (
)
func testCheck(t *testing.T, server *Server) {
newReq := func(subject, verb, group, resource, folder, name string) *authzv1.CheckRequest {
newReq := func(subject, verb, group, resource, subresource, folder, name string) *authzv1.CheckRequest {
return &authzv1.CheckRequest{
Namespace: namespace,
Subject: subject,
Verb: verb,
Group: group,
Resource: resource,
Name: name,
Folder: folder,
Namespace: namespace,
Subject: subject,
Verb: verb,
Group: group,
Resource: resource,
Subresource: subresource,
Name: name,
Folder: folder,
}
}
t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
res, err := server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
// sanity check
res, err = server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "1", "2"))
res, err = server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
// sanity check no access to subresource
res, err = server.Check(context.Background(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "1"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/1 through group_resource", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
res, err := server.Check(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
res, err := server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
// sanity check
res, err = server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "1", "2"))
res, err = server.Check(context.Background(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
res, err := server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "3", "2"))
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "3", "2"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
// sanity check
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "1", "2"))
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "2"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "2", "2"))
res, err = server.Check(context.Background(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
res, err := server.Check(context.Background(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:6 should be able to read folder 1 ", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", "1"))
res, err := server.Check(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", "", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:7 should be able to read folder one through group_resource access", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "1"))
res, err := server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "10"))
res, err = server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "", "10"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboar in folder 6 through folder 5", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "6", "10"))
res, err := server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "10"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "5", "11"))
res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", "5", "11"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, folderGroup, folderResource, "4", "12"))
res, err = server.Check(context.Background(), newReq("user:8", utils.VerbGet, folderGroup, folderResource, "", "4", "12"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:9 should be able to create dashboards in folder 5", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "5", ""))
res, err := server.Check(context.Background(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", "5", ""))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:10 should be able to read dashboard status for dashboard 10", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "1"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:11 should be able to read dashboard status for dashboard 10 through group_resource", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "", "10"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:12 should be able to read dashboard status for all dashboards in folder 5", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "10"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "11"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
// inherited from folder 5
res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(context.Background(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "13"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
}

View File

@ -21,8 +21,9 @@ 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, r.GetGroup(), r.GetResource(), store)
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, store)
if err != nil {
return nil, err
}
@ -31,11 +32,11 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis
return &authzv1.ListResponse{All: true}, nil
}
if info, ok := common.GetTypeInfo(r.GetGroup(), r.GetResource()); ok {
return s.listTyped(ctx, r.GetSubject(), relation, info, store)
if resource.IsGeneric() {
return s.listGeneric(ctx, r.GetSubject(), relation, resource, store)
}
return s.listGeneric(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
return s.listTyped(ctx, r.GetSubject(), relation, resource, store)
}
func (s *Server) listObjects(ctx context.Context, req *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
@ -50,8 +51,8 @@ func (s *Server) listObjects(ctx context.Context, req *openfgav1.ListObjectsRequ
return s.openfga.ListObjects(ctx, req)
}
func (s *Server) listTyped(ctx context.Context, subject, relation string, info common.TypeInfo, store *storeInfo) (*authzv1.ListResponse, error) {
if !info.IsValidRelation(relation) {
func (s *Server) listTyped(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.ListResponse, error) {
if !resource.IsValidRelation(relation) {
return &authzv1.ListResponse{}, nil
}
@ -59,7 +60,7 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, info c
res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
Type: info.Type,
Type: resource.Type(),
Relation: relation,
User: subject,
})
@ -68,14 +69,14 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, info c
}
return &authzv1.ListResponse{
Items: typedObjects(info.Type, res.GetObjects()),
Items: typedObjects(resource.Type(), res.GetObjects()),
}, nil
}
func (s *Server) listGeneric(ctx context.Context, subject, relation, group, resource string, store *storeInfo) (*authzv1.ListResponse, error) {
func (s *Server) listGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, store *storeInfo) (*authzv1.ListResponse, error) {
var (
resourceCtx = common.NewResourceContext(group, resource)
folderRelation = common.FolderResourceRelation(relation)
resourceCtx = resource.Context()
)
// 1. List all folders subject has access to resource type in
@ -98,8 +99,8 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation, group, reso
}
// 2. List all resource directly assigned to subject
var resources []string
if common.IsResourceRelation(relation) {
var objects []string
if resource.IsValidRelation(relation) {
res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
@ -112,12 +113,12 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation, group, reso
return nil, err
}
resources = res.GetObjects()
objects = res.GetObjects()
}
return &authzv1.ListResponse{
Folders: folderObject(folders),
Items: directObjects(group, resource, resources),
Items: directObjects(resource.GroupResource(), objects),
}, nil
}
@ -129,8 +130,8 @@ func typedObjects(typ string, objects []string) []string {
return objects
}
func directObjects(group, resource string, objects []string) []string {
prefix := fmt.Sprintf("%s:%s/%s/", resourceType, group, resource)
func directObjects(gr string, objects []string) []string {
prefix := fmt.Sprintf("%s:%s/", resourceType, gr)
for i := range objects {
objects[i] = strings.TrimPrefix(objects[i], prefix)
}

View File

@ -12,18 +12,19 @@ import (
)
func testList(t *testing.T, server *Server) {
newList := func(subject, group, resource string) *authzv1.ListRequest {
newList := func(subject, group, resource, subresource string) *authzv1.ListRequest {
return &authzv1.ListRequest{
Namespace: namespace,
Verb: utils.VerbList,
Subject: subject,
Group: group,
Resource: resource,
Namespace: namespace,
Verb: utils.VerbList,
Subject: subject,
Group: group,
Resource: resource,
Subresource: subresource,
}
}
t.Run("user:1 should list resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:1", dashboardGroup, dashboardResource))
res, err := server.List(context.Background(), newList("user:1", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 1)
assert.Len(t, res.GetFolders(), 0)
@ -31,7 +32,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:2 should be able to list all through group", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:2", dashboardGroup, dashboardResource))
res, err := server.List(context.Background(), newList("user:2", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.True(t, res.GetAll())
assert.Len(t, res.GetItems(), 0)
@ -39,7 +40,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:3 should be able to list resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:3", dashboardGroup, dashboardResource))
res, err := server.List(context.Background(), newList("user:3", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 1)
@ -48,24 +49,17 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:4 should be able to list all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:4", dashboardGroup, dashboardResource))
res, err := server.List(context.Background(), newList("user:4", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 2)
first := res.GetFolders()[0]
second := res.GetFolders()[1]
if first == "3" {
first, second = second, first
}
assert.Equal(t, first, "1")
assert.Equal(t, second, "3")
assert.Contains(t, res.GetFolders(), "1")
assert.Contains(t, res.GetFolders(), "3")
})
t.Run("user:5 should be get list all dashboard.grafana.app/dashboards in folder 1 with set relation", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:5", dashboardGroup, dashboardResource))
t.Run("user:5 should be list all dashboard.grafana.app/dashboards in folder 1 with set relation", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:5", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 1)
@ -73,7 +67,7 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:6 should be able to list folder 1", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:6", folderGroup, folderResource))
res, err := server.List(context.Background(), newList("user:6", folderGroup, folderResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 1)
assert.Len(t, res.GetFolders(), 0)
@ -81,10 +75,47 @@ func testList(t *testing.T, server *Server) {
})
t.Run("user:7 should be able to list all folders", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:7", folderGroup, folderResource))
res, err := server.List(context.Background(), newList("user:7", folderGroup, folderResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 0)
assert.True(t, res.GetAll())
})
t.Run("user:8 should be able to list resoruce:dashboard.grafana.app/dashboard in folder 6 and folder 5", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:8", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetFolders(), 2)
assert.Contains(t, res.GetFolders(), "5")
assert.Contains(t, res.GetFolders(), "6")
})
t.Run("user:10 should be able to get resoruce:dashboard.grafana.app/dashboard/status for 10 and 11", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:10", dashboardGroup, dashboardResource, statusSubresource))
require.NoError(t, err)
assert.Len(t, res.GetFolders(), 0)
assert.Len(t, res.GetItems(), 2)
assert.Contains(t, res.GetItems(), "10")
assert.Contains(t, res.GetItems(), "11")
})
t.Run("user:11 should be able to list all resoruce:dashboard.grafana.app/dashboard/status ", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:11", dashboardGroup, dashboardResource, statusSubresource))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 0)
assert.True(t, res.GetAll())
})
t.Run("user:12 should be able to list all resoruce:dashboard.grafana.app/dashboard/status in folder 5 and 6", func(t *testing.T) {
res, err := server.List(context.Background(), newList("user:12", dashboardGroup, dashboardResource, statusSubresource))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 2)
assert.Contains(t, res.GetFolders(), "5")
assert.Contains(t, res.GetFolders(), "6")
})
}

View File

@ -24,6 +24,8 @@ const (
folderGroup = "folder.grafana.app"
folderResource = "folders"
statusSubresource = "status"
)
func TestMain(m *testing.M) {
@ -76,20 +78,24 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
AuthorizationModelId: storeInf.ModelID,
Writes: &openfgav1.WriteRequestWrites{
TupleKeys: []*openfgav1.TupleKey{
common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "1"),
common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "1"),
common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource),
common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource),
common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "1"),
common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "1"),
common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "3"),
common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "1"),
common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"),
common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"),
common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource, ""),
common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource, ""),
common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "", "1"),
common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "1"),
common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "3"),
common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "1"),
common.NewFolderTuple("user:6", common.RelationGet, "1"),
common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource),
common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource, ""),
common.NewFolderParentTuple("5", "4"),
common.NewFolderParentTuple("6", "5"),
common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "5"),
common.NewFolderResourceTuple("user:9", "create", dashboardGroup, dashboardResource, "5"),
common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "5"),
common.NewFolderResourceTuple("user:9", common.RelationCreate, dashboardGroup, dashboardResource, "", "5"),
common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "10"),
common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "11"),
common.NewGroupResourceTuple("user:11", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource),
common.NewFolderResourceTuple("user:12", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "5"),
},
},
})

View File

@ -29,17 +29,18 @@ type resourceTranslation struct {
}
type actionMappig struct {
relation string
group string
resource string
relation string
group string
resource string
subresource string
}
func newMapping(relation string) actionMappig {
return newScopedMapping(relation, "", "")
func newMapping(relation, subresource string) actionMappig {
return newScopedMapping(relation, "", "", subresource)
}
func newScopedMapping(relation, group, resource string) actionMappig {
return actionMappig{relation, group, resource}
func newScopedMapping(relation, group, resource, subresource string) actionMappig {
return actionMappig{relation, group, resource, subresource}
}
var (
@ -56,14 +57,14 @@ var resourceTranslations = map[string]resourceTranslation{
group: folderGroup,
resource: folderResource,
mapping: map[string]actionMappig{
"folders:read": newMapping(RelationGet),
"folders:write": newMapping(RelationUpdate),
"folders:create": newMapping(RelationCreate),
"folders:delete": newMapping(RelationDelete),
"dashboards:read": newScopedMapping(RelationGet, dashboardGroup, dashboardResource),
"dashboards:write": newScopedMapping(RelationUpdate, dashboardGroup, dashboardResource),
"dashboards:create": newScopedMapping(RelationCreate, dashboardGroup, dashboardResource),
"dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource),
"folders:read": newMapping(RelationGet, ""),
"folders:write": newMapping(RelationUpdate, ""),
"folders:create": newMapping(RelationCreate, ""),
"folders:delete": newMapping(RelationDelete, ""),
"dashboards:read": newScopedMapping(RelationGet, dashboardGroup, dashboardResource, ""),
"dashboards:write": newScopedMapping(RelationUpdate, dashboardGroup, dashboardResource, ""),
"dashboards:create": newScopedMapping(RelationCreate, dashboardGroup, dashboardResource, ""),
"dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource, ""),
},
},
KindDashboards: {
@ -71,10 +72,10 @@ var resourceTranslations = map[string]resourceTranslation{
group: dashboardGroup,
resource: dashboardResource,
mapping: map[string]actionMappig{
"dashboards:read": newMapping(RelationGet),
"dashboards:write": newMapping(RelationUpdate),
"dashboards:create": newMapping(RelationCreate),
"dashboards:delete": newMapping(RelationDelete),
"dashboards:read": newMapping(RelationGet, ""),
"dashboards:write": newMapping(RelationUpdate, ""),
"dashboards:create": newMapping(RelationCreate, ""),
"dashboards:delete": newMapping(RelationDelete, ""),
},
},
}

View File

@ -70,8 +70,6 @@ var (
ToOpenFGATuples = common.ToOpenFGATuples
ToOpenFGATupleKey = common.ToOpenFGATupleKey
ToOpenFGATupleKeyWithoutCondition = common.ToOpenFGATupleKeyWithoutCondition
FormatGroupResource = common.FormatGroupResource
)
// NewTupleEntry constructs new openfga entry type:name[#relation].
@ -98,16 +96,16 @@ func TranslateToResourceTuple(subject string, action, kind, name string) (*openf
}
if name == "*" {
return common.NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource), true
return common.NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource), true
}
if translation.typ == TypeResource {
return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, name), true
return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, m.subresource, name), true
}
if translation.typ == TypeFolder {
if m.group != "" && m.resource != "" {
return common.NewFolderResourceTuple(subject, m.relation, m.group, m.resource, name), true
return common.NewFolderResourceTuple(subject, m.relation, m.group, m.resource, m.subresource, name), true
}
return common.NewFolderTuple(subject, m.relation, name), true
@ -177,7 +175,7 @@ func TranslateToGroupResource(kind string) string {
if !ok {
return ""
}
return common.FormatGroupResource(translation.group, translation.resource)
return common.FormatGroupResource(translation.group, translation.resource, "")
}
func TranslateBasicRole(name string) string {