Zanzana: Support subresources for typed resources (#102470)

* Zanzana: Support subresources for folders

* refactor

* fix subresource requests

* implement listing for folders subresources

* teams subresources PoC

* re-enable tests

* use team resource def from iam

* fix tests

* remove unused code

* refactor: rename to subresource

* split resource schema

* update workspaces

* rename folder relation to subresource

* refactor: rename folder resources to subresources

* update readme

* fix listing

* rename params in subresource filter
pull/102781/head
Alexander Zobnin 3 months ago committed by GitHub
parent a13d90db45
commit c34394f385
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      pkg/services/accesscontrol/dualwrite/collectors.go
  2. 2
      pkg/services/accesscontrol/dualwrite/resource_reconciler.go
  3. 17
      pkg/services/authz/zanzana/common/info.go
  4. 87
      pkg/services/authz/zanzana/common/tuple.go
  5. 11
      pkg/services/authz/zanzana/schema/README.md
  6. 6
      pkg/services/authz/zanzana/schema/schema.go
  7. 18
      pkg/services/authz/zanzana/schema/schema_resource.fga
  8. 30
      pkg/services/authz/zanzana/schema/schema_subresource.fga
  9. 39
      pkg/services/authz/zanzana/server/server_check.go
  10. 20
      pkg/services/authz/zanzana/server/server_check_test.go
  11. 32
      pkg/services/authz/zanzana/server/server_list.go
  12. 19
      pkg/services/authz/zanzana/server/server_list_test.go
  13. 54
      pkg/services/authz/zanzana/server/server_test.go
  14. 26
      pkg/services/authz/zanzana/zanzana.go

@ -167,8 +167,7 @@ func managedPermissionsCollector(store db.DB, kind string) legacyTupleCollector
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
} }
// For resource actions on folders we need to merge the tuples into one with combined // For resource actions on folders we need to merge the tuples into one with combined subresources.
// group_resources.
if zanzana.IsFolderResourceTuple(tuple) { if zanzana.IsFolderResourceTuple(tuple) {
key := tupleStringWithoutCondition(tuple) key := tupleStringWithoutCondition(tuple)
if t, ok := tuples[tuple.Object][key]; ok { if t, ok := tuples[tuple.Object][key]; ok {
@ -386,8 +385,7 @@ func rolePermissionsCollector(store db.DB) legacyTupleCollector {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
} }
// For resource actions on folders we need to merge the tuples into one with combined // For resource actions on folders we need to merge the tuples into one with combined subresources.
// group_resources.
if zanzana.IsFolderResourceTuple(tuple) { if zanzana.IsFolderResourceTuple(tuple) {
key := tupleStringWithoutCondition(tuple) key := tupleStringWithoutCondition(tuple)
if t, ok := tuples[tuple.Object][key]; ok { if t, ok := tuples[tuple.Object][key]; ok {
@ -449,8 +447,7 @@ func fixedRolePermissionsCollector(store db.DB) legacyTupleCollector {
tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey)
} }
// For resource actions on folders we need to merge the tuples into one with combined // For resource actions on folders we need to merge the tuples into one with combined subresources.
// group_resources.
if zanzana.IsFolderResourceTuple(tuple) { if zanzana.IsFolderResourceTuple(tuple) {
key := tupleStringWithoutCondition(tuple) key := tupleStringWithoutCondition(tuple)
if t, ok := tuples[tuple.Object][key]; ok { if t, ok := tuples[tuple.Object][key]; ok {

@ -62,7 +62,7 @@ func (r resourceReconciler) reconcile(ctx context.Context, namespace string) err
continue continue
} }
// 4. For folder resource tuples we also need to compare the stored group_resources // 4. For folder resource tuples we also need to compare the stored subresources
if zanzana.IsFolderResourceTuple(t) && t.String() != stored.String() { if zanzana.IsFolderResourceTuple(t) && t.String() != stored.String() {
deletes = append(deletes, &openfgav1.TupleKeyWithoutCondition{ deletes = append(deletes, &openfgav1.TupleKeyWithoutCondition{
User: t.User, User: t.User,

@ -1,10 +1,11 @@
package common package common
import ( import (
authzv1 "github.com/grafana/authlib/authz/proto/v1"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
iamalpha1 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
) )
@ -19,6 +20,11 @@ var typedResources = map[string]typeInfo{
folderalpha1.FolderResourceInfo.GroupResource().Resource, folderalpha1.FolderResourceInfo.GroupResource().Resource,
"", "",
): {Type: "folder", Relations: RelationsFolder}, ): {Type: "folder", Relations: RelationsFolder},
FormatGroupResource(
iamalpha1.TeamResourceInfo.GroupResource().Group,
iamalpha1.TeamResourceInfo.GroupResource().Resource,
"",
): {Type: "team", Relations: RelationsFolder},
} }
func getTypeInfo(group, resource string) (typeInfo, bool) { func getTypeInfo(group, resource string) (typeInfo, bool) {
@ -133,13 +139,10 @@ func (r ResourceInfo) Type() string {
} }
func (r ResourceInfo) Context() *structpb.Struct { func (r ResourceInfo) Context() *structpb.Struct {
if !r.IsGeneric() {
return nil
}
return &structpb.Struct{ return &structpb.Struct{
Fields: map[string]*structpb.Value{ Fields: map[string]*structpb.Value{
"requested_group": structpb.NewStringValue(r.GroupResource()), "requested_group": structpb.NewStringValue(r.GroupResource()),
"subresource": structpb.NewStringValue(r.GroupResource()),
}, },
} }
} }
@ -147,3 +150,7 @@ func (r ResourceInfo) Context() *structpb.Struct {
func (r ResourceInfo) IsValidRelation(relation string) bool { func (r ResourceInfo) IsValidRelation(relation string) bool {
return isValidRelation(relation, r.relations) return isValidRelation(relation, r.relations)
} }
func (r ResourceInfo) HasSubresource() bool {
return r.subresource != ""
}

@ -30,6 +30,7 @@ const (
TypeFolderPrefix string = TypeFolder + ":" TypeFolderPrefix string = TypeFolder + ":"
TypeResourcePrefix string = TypeResource + ":" TypeResourcePrefix string = TypeResource + ":"
TypeGroupResoucePrefix string = TypeGroupResouce + ":" TypeGroupResoucePrefix string = TypeGroupResouce + ":"
TypeTeamPrefix string = TypeTeam + ":"
) )
const ( const (
@ -50,16 +51,16 @@ const (
RelationGetPermissions string = "get_permissions" RelationGetPermissions string = "get_permissions"
RelationSetPermissions string = "set_permissions" RelationSetPermissions string = "set_permissions"
RelationFolderResourceSetView string = "resource_" + RelationSetView RelationSubresourceSetView string = "resource_" + RelationSetView
RelationFolderResourceSetEdit string = "resource_" + RelationSetEdit RelationSubresourceSetEdit string = "resource_" + RelationSetEdit
RelationFolderResourceSetAdmin string = "resource_" + RelationSetAdmin RelationSubresourceSetAdmin string = "resource_" + RelationSetAdmin
RelationFolderResourceGet string = "resource_" + RelationGet RelationSubresourceGet string = "resource_" + RelationGet
RelationFolderResourceUpdate string = "resource_" + RelationUpdate RelationSubresourceUpdate string = "resource_" + RelationUpdate
RelationFolderResourceCreate string = "resource_" + RelationCreate RelationSubresourceCreate string = "resource_" + RelationCreate
RelationFolderResourceDelete string = "resource_" + RelationDelete RelationSubresourceDelete string = "resource_" + RelationDelete
RelationFolderResourceGetPermissions string = "resource_" + RelationGetPermissions RelationSubresourceGetPermissions string = "resource_" + RelationGetPermissions
RelationFolderResourceSetPermissions string = "resource_" + RelationSetPermissions RelationSubresourceSetPermissions string = "resource_" + RelationSetPermissions
) )
// RelationsGroupResource are relations that can be added on type "group_resource". // RelationsGroupResource are relations that can be added on type "group_resource".
@ -81,19 +82,19 @@ var RelationsResource = []string{
RelationSetPermissions, RelationSetPermissions,
} }
// RelationsFolderResource are relations that can be added on type "folder" for child resources. // RelationsSubresource are relations that can be added on typed resources for subresources.
var RelationsFolderResource = []string{ var RelationsSubresource = []string{
RelationFolderResourceGet, RelationSubresourceGet,
RelationFolderResourceUpdate, RelationSubresourceUpdate,
RelationFolderResourceCreate, RelationSubresourceCreate,
RelationFolderResourceDelete, RelationSubresourceDelete,
RelationFolderResourceGetPermissions, RelationSubresourceGetPermissions,
RelationFolderResourceSetPermissions, RelationSubresourceSetPermissions,
} }
// RelationsFolder are relations that can be added on type "folder". // RelationsFolder are relations that can be added on type "folder".
var RelationsFolder = append( var RelationsFolder = append(
RelationsFolderResource, RelationsSubresource,
RelationGet, RelationGet,
RelationUpdate, RelationUpdate,
RelationCreate, RelationCreate,
@ -130,8 +131,8 @@ func IsGroupResourceRelation(relation string) bool {
return isValidRelation(relation, RelationsGroupResource) return isValidRelation(relation, RelationsGroupResource)
} }
func IsFolderResourceRelation(relation string) bool { func IsSubresourceRelation(relation string) bool {
return isValidRelation(relation, RelationsFolderResource) return isValidRelation(relation, RelationsSubresource)
} }
func isValidRelation(relation string, valid []string) bool { func isValidRelation(relation string, valid []string) bool {
@ -143,7 +144,7 @@ func isValidRelation(relation string, valid []string) bool {
return false return false
} }
func FolderResourceRelation(relation string) string { func SubresourceRelation(relation string) string {
return TypeResource + "_" + relation return TypeResource + "_" + relation
} }
@ -159,6 +160,10 @@ func NewFolderIdent(name string) string {
return TypeFolderPrefix + name return TypeFolderPrefix + name
} }
func NewTeamIdent(name string) string {
return TypeTeamPrefix + name
}
func NewGroupResourceIdent(group, resource, subresource string) string { func NewGroupResourceIdent(group, resource, subresource string) string {
return TypeGroupResoucePrefix + FormatGroupResource(group, resource, subresource) return TypeGroupResoucePrefix + FormatGroupResource(group, resource, subresource)
} }
@ -193,21 +198,21 @@ func NewResourceTuple(subject, relation, group, resource, subresource, name stri
} }
} }
func isFolderResourceRelationSet(relation string) bool { func isSubresourceRelationSet(relation string) bool {
return relation == RelationFolderResourceSetView || return relation == RelationSubresourceSetView ||
relation == RelationFolderResourceSetEdit || relation == RelationSubresourceSetEdit ||
relation == RelationFolderResourceSetAdmin relation == RelationSubresourceSetAdmin
} }
func NewFolderResourceTuple(subject, relation, group, resource, subresource, folder string) *openfgav1.TupleKey { func NewFolderResourceTuple(subject, relation, group, resource, subresource, folder string) *openfgav1.TupleKey {
relation = FolderResourceRelation(relation) relation = SubresourceRelation(relation)
var condition *openfgav1.RelationshipCondition var condition *openfgav1.RelationshipCondition
if !isFolderResourceRelationSet(relation) { if !isSubresourceRelationSet(relation) {
condition = &openfgav1.RelationshipCondition{ condition = &openfgav1.RelationshipCondition{
Name: "folder_group_filter", Name: "subresource_filter",
Context: &structpb.Struct{ Context: &structpb.Struct{
Fields: map[string]*structpb.Value{ Fields: map[string]*structpb.Value{
"group_resources": structpb.NewListValue(&structpb.ListValue{ "subresources": structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{structpb.NewStringValue(FormatGroupResource(group, resource, subresource))}, Values: []*structpb.Value{structpb.NewStringValue(FormatGroupResource(group, resource, subresource))},
}), }),
}, },
@ -223,6 +228,30 @@ func NewFolderResourceTuple(subject, relation, group, resource, subresource, fol
} }
} }
func NewTeamResourceTuple(subject, relation, group, resource, subresource, name string) *openfgav1.TupleKey {
relation = SubresourceRelation(relation)
var condition *openfgav1.RelationshipCondition
if !isSubresourceRelationSet(relation) {
condition = &openfgav1.RelationshipCondition{
Name: "subresource_filter",
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"subresources": structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{structpb.NewStringValue(FormatGroupResource(group, resource, subresource))},
}),
},
},
}
}
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: NewTeamIdent(name),
Condition: condition,
}
}
func NewGroupResourceTuple(subject, relation, group, resource, subresource string) *openfgav1.TupleKey { func NewGroupResourceTuple(subject, relation, group, resource, subresource string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{ return &openfgav1.TupleKey{
User: subject, User: subject,

@ -16,7 +16,7 @@ them. This is because we want to store the folder tree relations.
To grant a user access to a specific folder we store `{ “user”: “user:1”, relation: “read”, object:”folder:<name>” }` To grant a user access to a specific folder we store `{ “user”: “user:1”, relation: “read”, object:”folder:<name>” }`
To grant a user access to sub resources of a folder we store ``{ “user”: “user:1”, relation: “resource_read”, object:”folder:<uid>”}` with additional context. To grant a user access to sub resources of a folder we store ``{ “user”: “user:1”, relation: “resource_read”, object:”folder:<uid>”}` with additional context.
This context holds all GroupResources in a list e.g. `{ "group_resources": ["dashboard.grafana.app/dashboards", "alerting.grafana.app/rules" ] }`. This context holds all GroupResources in a list e.g. `{ "subresources": ["dashboard.grafana.app/dashboards", "alerting.grafana.app/rules" ] }`.
## Resource level permissions ## Resource level permissions
@ -33,6 +33,15 @@ To grant a user access to the subresource of the specific resource we store foll
It's also possible to grant user access to all subresources for specific resource type. It can be done with following tuple: `{ “user”: “user:1”, relation: “read”, object:”resource:dashboard.grafana.app/dashboards/<subresource>” }`. It's also possible to grant user access to all subresources for specific resource type. It can be done with following tuple: `{ “user”: “user:1”, relation: “read”, object:”resource:dashboard.grafana.app/dashboards/<subresource>” }`.
For the typed resources (like folders, users, teams, etc) subresources work in a little bit different way. Since typed resources only have ID in the name, subresources are added to the `subresource_filter`. For example, to grant user access to folder subresource, following tuple will be created:
```
{ “user”: “user:1”, relation: “resource_read”, object:”folder:<uid>” }
context: { "subresource_filter": ["folder.grafana.app/folders/<subresource>"] }
```
Note that relation is translated from `read` to `resource_read`. This is required to distinguish access between resource and its subresources. When check request is performed, we check if request contains subresource. If so, context filter and translated relation are used.
## Managed permissions ## Managed permissions
In the RBAC model managed permissions stored as a special "managed" role permissions. OpenFGA model allows to assign permissions directly to users, so it produces following tuples: In the RBAC model managed permissions stored as a special "managed" role permissions. OpenFGA model allows to assign permissions directly to users, so it produces following tuples:

@ -13,6 +13,8 @@ var (
folderDSL string folderDSL string
//go:embed schema_resource.fga //go:embed schema_resource.fga
resourceDSL string resourceDSL string
//go:embed schema_subresource.fga
subresourceDSL string
) )
var SchemaModules = []transformer.ModuleFile{ var SchemaModules = []transformer.ModuleFile{
@ -28,4 +30,8 @@ var SchemaModules = []transformer.ModuleFile{
Name: "schema_resource.fga", Name: "schema_resource.fga",
Contents: resourceDSL, Contents: resourceDSL,
}, },
{
Name: "schema_subresource.fga",
Contents: subresourceDSL,
},
} }

@ -1,19 +1,5 @@
module resource module resource
extend type folder
relations
define resource_view: [user, service-account, team#member, role#assignee] or resource_edit or resource_view from parent
define resource_edit: [user, service-account, team#member, role#assignee] or resource_admin or resource_edit from parent
define resource_admin: [user, service-account, team#member, role#assignee] or resource_admin from parent
define resource_get: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_view or resource_get from parent
define resource_create: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_create from parent
define resource_update: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_update from parent
define resource_delete: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_delete from parent
define resource_get_permissions: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_get_permissions from parent
define resource_set_permissions: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_set_permissions from parent
type group_resource type group_resource
relations relations
define view: [user, service-account, render, team#member, role#assignee] or edit define view: [user, service-account, render, team#member, role#assignee] or edit
@ -44,7 +30,3 @@ type resource
condition group_filter(requested_group: string, group_resource: string) { condition group_filter(requested_group: string, group_resource: string) {
requested_group == group_resource requested_group == group_resource
} }
condition folder_group_filter(requested_group: string, group_resources: list<string>) {
requested_group in group_resources
}

@ -0,0 +1,30 @@
module resource
extend type folder
relations
define resource_view: [user, service-account, team#member, role#assignee] or resource_edit or resource_view from parent
define resource_edit: [user, service-account, team#member, role#assignee] or resource_admin or resource_edit from parent
define resource_admin: [user, service-account, team#member, role#assignee] or resource_admin from parent
define resource_get: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_view or resource_get from parent
define resource_create: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit or resource_create from parent
define resource_update: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit or resource_update from parent
define resource_delete: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit or resource_delete from parent
define resource_get_permissions: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_admin or resource_get_permissions from parent
define resource_set_permissions: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_admin or resource_set_permissions from parent
extend type team
relations
define resource_view: [user, service-account, team#member, role#assignee] or resource_edit
define resource_edit: [user, service-account, team#member, role#assignee] or resource_admin
define resource_admin: [user, service-account, team#member, role#assignee]
define resource_get: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_view
define resource_create: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit
define resource_update: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit
define resource_delete: [user with subresource_filter, service-account with subresource_filter, team#member with subresource_filter, role#assignee with subresource_filter] or resource_edit
condition subresource_filter(subresource: string, subresources: list<string>) {
subresource in subresources
}

@ -76,6 +76,39 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation string, resou
return &authzv1.CheckResponse{Allowed: false}, nil return &authzv1.CheckResponse{Allowed: false}, nil
} }
var (
resourceIdent = resource.ResourceIdent()
resourceCtx = resource.Context()
subresourceRelation = common.SubresourceRelation(relation)
)
if resource.HasSubresource() {
// Check if subject has access as a subresource
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject,
Relation: subresourceRelation,
Object: resourceIdent,
},
Context: resourceCtx,
ContextualTuples: contextuals,
})
if err != nil {
return nil, err
}
if res.GetAllowed() {
return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil
}
}
if resourceIdent == "" {
return &authzv1.CheckResponse{Allowed: false}, nil
}
// Check if subject has direct access to resource // Check if subject has direct access to resource
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID, StoreId: store.ID,
@ -83,7 +116,7 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation string, resou
TupleKey: &openfgav1.CheckRequestTupleKey{ TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject, User: subject,
Relation: relation, Relation: relation,
Object: resource.ResourceIdent(), Object: resourceIdent,
}, },
ContextualTuples: contextuals, ContextualTuples: contextuals,
}) })
@ -105,10 +138,10 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res
var ( var (
folderIdent = resource.FolderIdent() folderIdent = resource.FolderIdent()
resourceCtx = resource.Context() resourceCtx = resource.Context()
folderRelation = common.FolderResourceRelation(relation) folderRelation = common.SubresourceRelation(relation)
) )
if folderIdent != "" && common.IsFolderResourceRelation(folderRelation) { if folderIdent != "" && common.IsSubresourceRelation(folderRelation) {
// Check if subject has access as a sub resource for the folder // Check if subject has access as a sub resource for the folder
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{ res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID, StoreId: store.ID,

@ -152,4 +152,24 @@ func testCheck(t *testing.T, server *Server) {
require.NoError(t, err) require.NoError(t, err)
assert.False(t, res.GetAllowed()) assert.False(t, res.GetAllowed())
}) })
t.Run("user:13 should be able to read folder status for all subfolders of folder 5", func(t *testing.T) {
res, err := server.Check(newContextWithNamespace(), newReq("user:13", utils.VerbGet, folderGroup, folderResource, statusSubresource, "", "5"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(newContextWithNamespace(), newReq("user:13", utils.VerbGet, folderGroup, folderResource, statusSubresource, "", "6"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(newContextWithNamespace(), newReq("user:13", utils.VerbGet, folderGroup, folderResource, statusSubresource, "", "4"))
require.NoError(t, err)
assert.False(t, res.GetAllowed())
})
t.Run("user:14 should be able to read team subresources for team 1", func(t *testing.T) {
res, err := server.Check(newContextWithNamespace(), newReq("user:14", utils.VerbGet, "iam.grafana.app", "teams", statusSubresource, "", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
} }

@ -56,6 +56,31 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour
return &authzv1.ListResponse{}, nil return &authzv1.ListResponse{}, nil
} }
var (
subresourceRelation = common.SubresourceRelation(relation)
resourceCtx = resource.Context()
)
var items []string
if resource.HasSubresource() && common.IsSubresourceRelation(subresourceRelation) {
// List requested subresources
res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
Type: resource.Type(),
Relation: subresourceRelation,
User: subject,
Context: resourceCtx,
ContextualTuples: contextuals,
})
if err != nil {
return nil, err
}
items = append(items, typedObjects(resource.Type(), res.GetObjects())...)
}
// List all resources user has access too // List all resources user has access too
res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{ res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: store.ID, StoreId: store.ID,
@ -68,21 +93,22 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour
if err != nil { if err != nil {
return nil, err return nil, err
} }
items = append(items, typedObjects(resource.Type(), res.GetObjects())...)
return &authzv1.ListResponse{ return &authzv1.ListResponse{
Items: typedObjects(resource.Type(), res.GetObjects()), Items: items,
}, nil }, nil
} }
func (s *Server) listGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, contextuals *openfgav1.ContextualTupleKeys, 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 ( var (
folderRelation = common.FolderResourceRelation(relation) folderRelation = common.SubresourceRelation(relation)
resourceCtx = resource.Context() resourceCtx = resource.Context()
) )
// 1. List all folders subject has access to resource type in // 1. List all folders subject has access to resource type in
var folders []string var folders []string
if common.IsFolderResourceRelation(folderRelation) { if common.IsSubresourceRelation(folderRelation) {
res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{ res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: store.ID, StoreId: store.ID,
AuthorizationModelId: store.ModelID, AuthorizationModelId: store.ModelID,

@ -118,4 +118,23 @@ func testList(t *testing.T, server *Server) {
assert.Contains(t, res.GetFolders(), "5") assert.Contains(t, res.GetFolders(), "5")
assert.Contains(t, res.GetFolders(), "6") assert.Contains(t, res.GetFolders(), "6")
}) })
t.Run("user:13 should be able to list all subresources in folder 5 and 6", func(t *testing.T) {
res, err := server.List(newContextWithNamespace(), newList("user:13", folderGroup, folderResource, statusSubresource))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 2)
assert.Len(t, res.GetFolders(), 0)
assert.Contains(t, res.GetItems(), "5")
assert.Contains(t, res.GetItems(), "6")
})
t.Run("user:14 should be able to list all subresources for team 1", func(t *testing.T) {
res, err := server.List(newContextWithNamespace(), newList("user:14", teamGroup, teamResource, statusSubresource))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 1)
assert.Len(t, res.GetFolders(), 0)
assert.Contains(t, res.GetItems(), "1")
})
} }

@ -29,6 +29,9 @@ const (
folderGroup = "folder.grafana.app" folderGroup = "folder.grafana.app"
folderResource = "folders" folderResource = "folders"
teamGroup = "iam.grafana.app"
teamResource = "teams"
statusSubresource = "status" statusSubresource = "status"
) )
@ -83,31 +86,38 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
require.NoError(t, err) require.NoError(t, err)
// seed tuples // seed tuples
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.NewFolderTuple("user:6", common.RelationGet, "1"),
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", 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"),
common.NewFolderResourceTuple("user:13", common.RelationGet, folderGroup, folderResource, statusSubresource, "5"),
common.NewTeamResourceTuple("user:14", common.RelationGet, teamGroup, teamResource, statusSubresource, "1"),
},
}
for _, w := range writes.TupleKeys {
t.Log(w.String())
}
_, err = openfga.Write(context.Background(), &openfgav1.WriteRequest{ _, err = openfga.Write(context.Background(), &openfgav1.WriteRequest{
StoreId: storeInf.ID, StoreId: storeInf.ID,
AuthorizationModelId: storeInf.ModelID, AuthorizationModelId: storeInf.ModelID,
Writes: &openfgav1.WriteRequestWrites{ Writes: writes,
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.NewFolderTuple("user:6", common.RelationGet, "1"),
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", 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"),
},
},
}) })
require.NoError(t, err) require.NoError(t, err)
return srv return srv

@ -38,20 +38,20 @@ const (
RelationCreate = common.RelationCreate RelationCreate = common.RelationCreate
RelationDelete = common.RelationDelete RelationDelete = common.RelationDelete
RelationFolderResourceSetView = common.RelationFolderResourceSetView RelationSubresourceSetView = common.RelationSubresourceSetView
RelationFolderResourceSetEdit = common.RelationFolderResourceSetEdit RelationSubresourceSetEdit = common.RelationSubresourceSetEdit
RelationFolderResourceSetAdmin = common.RelationFolderResourceSetAdmin RelationSubresourceSetAdmin = common.RelationSubresourceSetAdmin
RelationFolderResourceRead = common.RelationFolderResourceGet RelationSubresourceRead = common.RelationSubresourceGet
RelationFolderResourceWrite = common.RelationFolderResourceUpdate RelationSubresourceWrite = common.RelationSubresourceUpdate
RelationFolderResourceCreate = common.RelationFolderResourceCreate RelationSubresourceCreate = common.RelationSubresourceCreate
RelationFolderResourceDelete = common.RelationFolderResourceDelete RelationSubresourceDelete = common.RelationSubresourceDelete
) )
var ( var (
RelationsFolder = common.RelationsFolder RelationsFolder = common.RelationsFolder
RelationsResouce = common.RelationsResource RelationsResouce = common.RelationsResource
RelationsFolderResource = common.RelationsFolderResource RelationsSubresource = common.RelationsSubresource
) )
const ( const (
@ -118,8 +118,8 @@ func IsFolderResourceTuple(t *openfgav1.TupleKey) bool {
} }
func MergeFolderResourceTuples(a, b *openfgav1.TupleKey) { func MergeFolderResourceTuples(a, b *openfgav1.TupleKey) {
va := a.Condition.Context.Fields["group_resources"] va := a.Condition.Context.Fields["subresources"]
vb := b.Condition.Context.Fields["group_resources"] vb := b.Condition.Context.Fields["subresources"]
va.GetListValue().Values = append(va.GetListValue().Values, vb.GetListValue().Values...) va.GetListValue().Values = append(va.GetListValue().Values, vb.GetListValue().Values...)
} }

Loading…
Cancel
Save