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
This commit is contained in:
@@ -167,8 +167,7 @@ func managedPermissionsCollector(store db.DB, kind string) 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.
|
||||
// For resource actions on folders we need to merge the tuples into one with combined subresources.
|
||||
if zanzana.IsFolderResourceTuple(tuple) {
|
||||
key := tupleStringWithoutCondition(tuple)
|
||||
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)
|
||||
}
|
||||
|
||||
// For resource actions on folders we need to merge the tuples into one with combined
|
||||
// group_resources.
|
||||
// For resource actions on folders we need to merge the tuples into one with combined subresources.
|
||||
if zanzana.IsFolderResourceTuple(tuple) {
|
||||
key := tupleStringWithoutCondition(tuple)
|
||||
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)
|
||||
}
|
||||
|
||||
// For resource actions on folders we need to merge the tuples into one with combined
|
||||
// group_resources.
|
||||
// For resource actions on folders we need to merge the tuples into one with combined subresources.
|
||||
if zanzana.IsFolderResourceTuple(tuple) {
|
||||
key := tupleStringWithoutCondition(tuple)
|
||||
if t, ok := tuples[tuple.Object][key]; ok {
|
||||
|
||||
@@ -62,7 +62,7 @@ func (r resourceReconciler) reconcile(ctx context.Context, namespace string) err
|
||||
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() {
|
||||
deletes = append(deletes, &openfgav1.TupleKeyWithoutCondition{
|
||||
User: t.User,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -19,6 +20,11 @@ var typedResources = map[string]typeInfo{
|
||||
folderalpha1.FolderResourceInfo.GroupResource().Resource,
|
||||
"",
|
||||
): {Type: "folder", Relations: RelationsFolder},
|
||||
FormatGroupResource(
|
||||
iamalpha1.TeamResourceInfo.GroupResource().Group,
|
||||
iamalpha1.TeamResourceInfo.GroupResource().Resource,
|
||||
"",
|
||||
): {Type: "team", Relations: RelationsFolder},
|
||||
}
|
||||
|
||||
func getTypeInfo(group, resource string) (typeInfo, bool) {
|
||||
@@ -133,13 +139,10 @@ func (r ResourceInfo) Type() string {
|
||||
}
|
||||
|
||||
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()),
|
||||
"subresource": structpb.NewStringValue(r.GroupResource()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -147,3 +150,7 @@ func (r ResourceInfo) Context() *structpb.Struct {
|
||||
func (r ResourceInfo) IsValidRelation(relation string) bool {
|
||||
return isValidRelation(relation, r.relations)
|
||||
}
|
||||
|
||||
func (r ResourceInfo) HasSubresource() bool {
|
||||
return r.subresource != ""
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const (
|
||||
TypeFolderPrefix string = TypeFolder + ":"
|
||||
TypeResourcePrefix string = TypeResource + ":"
|
||||
TypeGroupResoucePrefix string = TypeGroupResouce + ":"
|
||||
TypeTeamPrefix string = TypeTeam + ":"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -50,16 +51,16 @@ const (
|
||||
RelationGetPermissions string = "get_permissions"
|
||||
RelationSetPermissions string = "set_permissions"
|
||||
|
||||
RelationFolderResourceSetView string = "resource_" + RelationSetView
|
||||
RelationFolderResourceSetEdit string = "resource_" + RelationSetEdit
|
||||
RelationFolderResourceSetAdmin string = "resource_" + RelationSetAdmin
|
||||
RelationSubresourceSetView string = "resource_" + RelationSetView
|
||||
RelationSubresourceSetEdit string = "resource_" + RelationSetEdit
|
||||
RelationSubresourceSetAdmin string = "resource_" + RelationSetAdmin
|
||||
|
||||
RelationFolderResourceGet string = "resource_" + RelationGet
|
||||
RelationFolderResourceUpdate string = "resource_" + RelationUpdate
|
||||
RelationFolderResourceCreate string = "resource_" + RelationCreate
|
||||
RelationFolderResourceDelete string = "resource_" + RelationDelete
|
||||
RelationFolderResourceGetPermissions string = "resource_" + RelationGetPermissions
|
||||
RelationFolderResourceSetPermissions string = "resource_" + RelationSetPermissions
|
||||
RelationSubresourceGet string = "resource_" + RelationGet
|
||||
RelationSubresourceUpdate string = "resource_" + RelationUpdate
|
||||
RelationSubresourceCreate string = "resource_" + RelationCreate
|
||||
RelationSubresourceDelete string = "resource_" + RelationDelete
|
||||
RelationSubresourceGetPermissions string = "resource_" + RelationGetPermissions
|
||||
RelationSubresourceSetPermissions string = "resource_" + RelationSetPermissions
|
||||
)
|
||||
|
||||
// RelationsGroupResource are relations that can be added on type "group_resource".
|
||||
@@ -81,19 +82,19 @@ var RelationsResource = []string{
|
||||
RelationSetPermissions,
|
||||
}
|
||||
|
||||
// RelationsFolderResource are relations that can be added on type "folder" for child resources.
|
||||
var RelationsFolderResource = []string{
|
||||
RelationFolderResourceGet,
|
||||
RelationFolderResourceUpdate,
|
||||
RelationFolderResourceCreate,
|
||||
RelationFolderResourceDelete,
|
||||
RelationFolderResourceGetPermissions,
|
||||
RelationFolderResourceSetPermissions,
|
||||
// RelationsSubresource are relations that can be added on typed resources for subresources.
|
||||
var RelationsSubresource = []string{
|
||||
RelationSubresourceGet,
|
||||
RelationSubresourceUpdate,
|
||||
RelationSubresourceCreate,
|
||||
RelationSubresourceDelete,
|
||||
RelationSubresourceGetPermissions,
|
||||
RelationSubresourceSetPermissions,
|
||||
}
|
||||
|
||||
// RelationsFolder are relations that can be added on type "folder".
|
||||
var RelationsFolder = append(
|
||||
RelationsFolderResource,
|
||||
RelationsSubresource,
|
||||
RelationGet,
|
||||
RelationUpdate,
|
||||
RelationCreate,
|
||||
@@ -130,8 +131,8 @@ func IsGroupResourceRelation(relation string) bool {
|
||||
return isValidRelation(relation, RelationsGroupResource)
|
||||
}
|
||||
|
||||
func IsFolderResourceRelation(relation string) bool {
|
||||
return isValidRelation(relation, RelationsFolderResource)
|
||||
func IsSubresourceRelation(relation string) bool {
|
||||
return isValidRelation(relation, RelationsSubresource)
|
||||
}
|
||||
|
||||
func isValidRelation(relation string, valid []string) bool {
|
||||
@@ -143,7 +144,7 @@ func isValidRelation(relation string, valid []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func FolderResourceRelation(relation string) string {
|
||||
func SubresourceRelation(relation string) string {
|
||||
return TypeResource + "_" + relation
|
||||
}
|
||||
|
||||
@@ -159,6 +160,10 @@ func NewFolderIdent(name string) string {
|
||||
return TypeFolderPrefix + name
|
||||
}
|
||||
|
||||
func NewTeamIdent(name string) string {
|
||||
return TypeTeamPrefix + name
|
||||
}
|
||||
|
||||
func NewGroupResourceIdent(group, resource, subresource string) string {
|
||||
return TypeGroupResoucePrefix + FormatGroupResource(group, resource, subresource)
|
||||
}
|
||||
@@ -193,21 +198,21 @@ func NewResourceTuple(subject, relation, group, resource, subresource, name stri
|
||||
}
|
||||
}
|
||||
|
||||
func isFolderResourceRelationSet(relation string) bool {
|
||||
return relation == RelationFolderResourceSetView ||
|
||||
relation == RelationFolderResourceSetEdit ||
|
||||
relation == RelationFolderResourceSetAdmin
|
||||
func isSubresourceRelationSet(relation string) bool {
|
||||
return relation == RelationSubresourceSetView ||
|
||||
relation == RelationSubresourceSetEdit ||
|
||||
relation == RelationSubresourceSetAdmin
|
||||
}
|
||||
|
||||
func NewFolderResourceTuple(subject, relation, group, resource, subresource, folder string) *openfgav1.TupleKey {
|
||||
relation = FolderResourceRelation(relation)
|
||||
relation = SubresourceRelation(relation)
|
||||
var condition *openfgav1.RelationshipCondition
|
||||
if !isFolderResourceRelationSet(relation) {
|
||||
if !isSubresourceRelationSet(relation) {
|
||||
condition = &openfgav1.RelationshipCondition{
|
||||
Name: "folder_group_filter",
|
||||
Name: "subresource_filter",
|
||||
Context: &structpb.Struct{
|
||||
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))},
|
||||
}),
|
||||
},
|
||||
@@ -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 {
|
||||
return &openfgav1.TupleKey{
|
||||
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 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
|
||||
|
||||
@@ -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>” }`.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
//go:embed schema_resource.fga
|
||||
resourceDSL string
|
||||
//go:embed schema_subresource.fga
|
||||
subresourceDSL string
|
||||
)
|
||||
|
||||
var SchemaModules = []transformer.ModuleFile{
|
||||
@@ -28,4 +30,8 @@ var SchemaModules = []transformer.ModuleFile{
|
||||
Name: "schema_resource.fga",
|
||||
Contents: resourceDSL,
|
||||
},
|
||||
{
|
||||
Name: "schema_subresource.fga",
|
||||
Contents: subresourceDSL,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
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
|
||||
relations
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
|
||||
StoreId: store.ID,
|
||||
@@ -83,7 +116,7 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation string, resou
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: subject,
|
||||
Relation: relation,
|
||||
Object: resource.ResourceIdent(),
|
||||
Object: resourceIdent,
|
||||
},
|
||||
ContextualTuples: contextuals,
|
||||
})
|
||||
@@ -105,10 +138,10 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res
|
||||
var (
|
||||
folderIdent = resource.FolderIdent()
|
||||
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
|
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
|
||||
StoreId: store.ID,
|
||||
|
||||
@@ -152,4 +152,24 @@ func testCheck(t *testing.T, server *Server) {
|
||||
require.NoError(t, err)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
StoreId: store.ID,
|
||||
@@ -68,21 +93,22 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, typedObjects(resource.Type(), res.GetObjects())...)
|
||||
|
||||
return &authzv1.ListResponse{
|
||||
Items: typedObjects(resource.Type(), res.GetObjects()),
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) listGeneric(ctx context.Context, subject, relation string, resource common.ResourceInfo, contextuals *openfgav1.ContextualTupleKeys, store *storeInfo) (*authzv1.ListResponse, error) {
|
||||
var (
|
||||
folderRelation = common.FolderResourceRelation(relation)
|
||||
folderRelation = common.SubresourceRelation(relation)
|
||||
resourceCtx = resource.Context()
|
||||
)
|
||||
|
||||
// 1. List all folders subject has access to resource type in
|
||||
var folders []string
|
||||
if common.IsFolderResourceRelation(folderRelation) {
|
||||
if common.IsSubresourceRelation(folderRelation) {
|
||||
res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
StoreId: store.ID,
|
||||
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(), "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"
|
||||
folderResource = "folders"
|
||||
|
||||
teamGroup = "iam.grafana.app"
|
||||
teamResource = "teams"
|
||||
|
||||
statusSubresource = "status"
|
||||
)
|
||||
|
||||
@@ -83,31 +86,38 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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{
|
||||
StoreId: storeInf.ID,
|
||||
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.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"),
|
||||
},
|
||||
},
|
||||
Writes: writes,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return srv
|
||||
|
||||
@@ -38,20 +38,20 @@ const (
|
||||
RelationCreate = common.RelationCreate
|
||||
RelationDelete = common.RelationDelete
|
||||
|
||||
RelationFolderResourceSetView = common.RelationFolderResourceSetView
|
||||
RelationFolderResourceSetEdit = common.RelationFolderResourceSetEdit
|
||||
RelationFolderResourceSetAdmin = common.RelationFolderResourceSetAdmin
|
||||
RelationSubresourceSetView = common.RelationSubresourceSetView
|
||||
RelationSubresourceSetEdit = common.RelationSubresourceSetEdit
|
||||
RelationSubresourceSetAdmin = common.RelationSubresourceSetAdmin
|
||||
|
||||
RelationFolderResourceRead = common.RelationFolderResourceGet
|
||||
RelationFolderResourceWrite = common.RelationFolderResourceUpdate
|
||||
RelationFolderResourceCreate = common.RelationFolderResourceCreate
|
||||
RelationFolderResourceDelete = common.RelationFolderResourceDelete
|
||||
RelationSubresourceRead = common.RelationSubresourceGet
|
||||
RelationSubresourceWrite = common.RelationSubresourceUpdate
|
||||
RelationSubresourceCreate = common.RelationSubresourceCreate
|
||||
RelationSubresourceDelete = common.RelationSubresourceDelete
|
||||
)
|
||||
|
||||
var (
|
||||
RelationsFolder = common.RelationsFolder
|
||||
RelationsResouce = common.RelationsResource
|
||||
RelationsFolderResource = common.RelationsFolderResource
|
||||
RelationsFolder = common.RelationsFolder
|
||||
RelationsResouce = common.RelationsResource
|
||||
RelationsSubresource = common.RelationsSubresource
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -118,8 +118,8 @@ func IsFolderResourceTuple(t *openfgav1.TupleKey) bool {
|
||||
}
|
||||
|
||||
func MergeFolderResourceTuples(a, b *openfgav1.TupleKey) {
|
||||
va := a.Condition.Context.Fields["group_resources"]
|
||||
vb := b.Condition.Context.Fields["group_resources"]
|
||||
va := a.Condition.Context.Fields["subresources"]
|
||||
vb := b.Condition.Context.Fields["subresources"]
|
||||
va.GetListValue().Values = append(va.GetListValue().Values, vb.GetListValue().Values...)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user