Zanzana: Remove create relation from generic resources (#97042)

Remove create relation from generic resources.

We cant have a create relation to a resource because they don't exist yet. So
in oder to check create we either have to have that permissions on a folder or the namespace
This commit is contained in:
Karl Persson 2024-11-27 09:02:29 +01:00 committed by GitHub
parent 21bd47e512
commit be54c69fd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 119 additions and 79 deletions

View File

@ -7,14 +7,15 @@ import (
)
type TypeInfo struct {
Type string
Type string
Relations []string
}
var typedResources = map[string]TypeInfo{
FormatGroupResource(
folderalpha1.FolderResourceInfo.GroupResource().Group,
folderalpha1.FolderResourceInfo.GroupResource().Resource,
): {Type: "folder"},
): {Type: "folder", Relations: append(ResourceRelations, RelationCreate)},
}
func GetTypeInfo(group, resource string) (TypeInfo, bool) {

View File

@ -50,7 +50,6 @@ const (
var ResourceRelations = []string{
RelationRead,
RelationWrite,
RelationCreate,
RelationDelete,
RelationPermissionsRead,
RelationPermissionsWrite,
@ -58,6 +57,7 @@ var ResourceRelations = []string{
var FolderRelations = append(
ResourceRelations,
RelationCreate,
RelationFolderResourceRead,
RelationFolderResourceWrite,
RelationFolderResourceCreate,

View File

@ -20,7 +20,6 @@ type resource
define admin: [user with group_filter, team#member with group_filter, role#assignee with group_filter]
define read: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or view
define create: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
define write: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
define delete: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
define permissions_read: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin

View File

@ -60,6 +60,8 @@ func (s *Server) batchCheckItem(
if err != nil {
return nil, err
}
allowed = res.GetAllowed()
groupResourceAccess[groupResource] = res.GetAllowed()
}

View File

@ -13,10 +13,10 @@ import (
)
func testBatchCheck(t *testing.T, server *Server) {
newReq := func(subject, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
newReq := func(subject, verb, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
for i, item := range items {
items[i] = &authzextv1.BatchCheckItem{
Verb: utils.VerbGet,
Verb: verb,
Group: group,
Resource: resource,
Name: item.GetName(),
@ -32,89 +32,116 @@ 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) {
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:1", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := zanzana.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"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 2)
require.Len(t, res.Groups[groupResource].Items, 2)
assert.True(t, res.Groups[groupPrefix].Items["1"])
assert.False(t, res.Groups[groupPrefix].Items["2"])
assert.True(t, res.Groups[groupResource].Items["1"])
assert.False(t, res.Groups[groupResource].Items["2"])
})
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through namespace", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:2", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := zanzana.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"},
}))
require.NoError(t, err)
assert.Len(t, res.Groups[groupPrefix].Items, 2)
assert.Len(t, res.Groups[groupResource].Items, 2)
})
t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:3", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := zanzana.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"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 2)
require.Len(t, res.Groups[groupResource].Items, 2)
assert.True(t, res.Groups[groupPrefix].Items["1"])
assert.False(t, res.Groups[groupPrefix].Items["2"])
assert.True(t, res.Groups[groupResource].Items["1"])
assert.False(t, res.Groups[groupResource].Items["2"])
})
t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:4", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := zanzana.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"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 3)
require.Len(t, res.Groups[groupResource].Items, 3)
assert.True(t, res.Groups[groupPrefix].Items["1"])
assert.True(t, res.Groups[groupPrefix].Items["2"])
assert.False(t, res.Groups[groupPrefix].Items["3"])
assert.True(t, res.Groups[groupResource].Items["1"])
assert.True(t, res.Groups[groupResource].Items["2"])
assert.False(t, res.Groups[groupResource].Items["3"])
})
t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:5", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
groupResource := zanzana.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"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 2)
require.Len(t, res.Groups[groupResource].Items, 2)
assert.True(t, res.Groups[groupPrefix].Items["1"])
assert.False(t, res.Groups[groupPrefix].Items["2"])
assert.True(t, res.Groups[groupResource].Items["1"])
assert.False(t, res.Groups[groupResource].Items["2"])
})
t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(folderGroup, folderResource)
res, err := server.BatchCheck(context.Background(), newReq("user:6", folderGroup, folderResource, []*authzextv1.BatchCheckItem{
groupResource := zanzana.FormatGroupResource(folderGroup, folderResource)
res, err := server.BatchCheck(context.Background(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{
{Name: "1"},
{Name: "2"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 2)
require.Len(t, res.Groups[groupResource].Items, 2)
assert.True(t, res.Groups[groupPrefix].Items["1"])
assert.False(t, res.Groups[groupPrefix].Items["2"])
assert.True(t, res.Groups[groupResource].Items["1"])
assert.False(t, res.Groups[groupResource].Items["2"])
})
t.Run("user:7 should be able to read folder {1,2} through namespace access", func(t *testing.T) {
groupPrefix := zanzana.FormatGroupResource(folderGroup, folderResource)
res, err := server.BatchCheck(context.Background(), newReq("user:7", folderGroup, folderResource, []*authzextv1.BatchCheckItem{
groupResource := zanzana.FormatGroupResource(folderGroup, folderResource)
res, err := server.BatchCheck(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{
{Name: "1"},
{Name: "2"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupPrefix].Items, 2)
require.Len(t, res.Groups[groupResource].Items, 2)
require.True(t, res.Groups[groupResource].Items["1"])
require.True(t, res.Groups[groupResource].Items["2"])
})
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{
{Name: "10", Folder: "6"},
{Name: "20", Folder: "6"},
}))
require.NoError(t, err)
require.Len(t, res.Groups[groupResource].Items, 2)
require.True(t, res.Groups[groupResource].Items["10"])
require.True(t, res.Groups[groupResource].Items["20"])
})
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{
{Name: "10", Folder: "6"},
{Name: "20", Folder: "6"},
}))
require.NoError(t, err)
t.Log(res.Groups)
require.Len(t, res.Groups[groupResource].Items, 2)
require.True(t, res.Groups[groupResource].Items["10"])
require.True(t, res.Groups[groupResource].Items["20"])
})
}

View File

@ -21,7 +21,7 @@ func (s *Server) Capabilities(ctx context.Context, r *authzextv1.CapabilitiesReq
func (s *Server) capabilitiesTyped(ctx context.Context, r *authzextv1.CapabilitiesRequest, info common.TypeInfo, store *storeInfo) (*authzextv1.CapabilitiesResponse, error) {
out := make([]string, 0, len(common.ResourceRelations))
for _, relation := range common.ResourceRelations {
for _, relation := range info.Relations {
res, err := s.checkNamespace(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
if err != nil {
return nil, err

View File

@ -50,7 +50,7 @@ func testCapabilities(t *testing.T, server *Server) {
t.Run("user:5 should be able to read, write, create and delete resource:dashboards.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
res, err := server.Capabilities(context.Background(), newReq("user:5", dashboardGroup, dashboardResource, "1", "1"))
require.NoError(t, err)
assert.Equal(t, []string{common.RelationRead, common.RelationWrite, common.RelationCreate, common.RelationDelete}, res.GetCapabilities())
assert.Equal(t, []string{common.RelationRead, common.RelationWrite, common.RelationDelete}, res.GetCapabilities())
})
t.Run("user:6 should be able to read folder 1 ", func(t *testing.T) {

View File

@ -83,28 +83,32 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation, name string,
func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, resource, name, folder string, store *storeInfo) (*authzv1.CheckResponse, error) {
groupResource := structpb.NewStringValue(common.FormatGroupResource(group, resource))
// Check if subject has direct access to resource
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject,
Relation: relation,
Object: common.NewResourceIdent(group, resource, name),
},
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"requested_group": groupResource,
// Create relation can only exist on namespace or folder level.
// So we skip direct resource access check.
if relation != common.RelationCreate {
// Check if subject has direct access to resource
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject,
Relation: relation,
Object: common.NewResourceIdent(group, resource, name),
},
},
})
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"requested_group": groupResource,
},
},
})
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
if res.GetAllowed() {
return &authzv1.CheckResponse{Allowed: true}, nil
if res.GetAllowed() {
return &authzv1.CheckResponse{Allowed: true}, nil
}
}
if folder == "" {
@ -112,7 +116,7 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res
}
// 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,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{

View File

@ -12,11 +12,11 @@ import (
)
func testCheck(t *testing.T, server *Server) {
newRead := func(subject, group, resource, folder, name string) *authzv1.CheckRequest {
newReq := func(subject, verb, group, resource, folder, name string) *authzv1.CheckRequest {
return &authzv1.CheckRequest{
Namespace: namespace,
Subject: subject,
Verb: utils.VerbGet,
Verb: verb,
Group: group,
Resource: resource,
Name: name,
@ -25,85 +25,91 @@ func testCheck(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) {
res, err := server.Check(context.Background(), newRead("user:1", 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(), newRead("user:1", 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())
})
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/1 through namespace", func(t *testing.T) {
res, err := server.Check(context.Background(), newRead("user:2", 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(), newRead("user:3", 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(), newRead("user:3", 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(), newRead("user:4", 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(), newRead("user:4", 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(), newRead("user:4", 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(), newRead("user:4", 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(), newRead("user:5", 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(), newRead("user:6", 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 namespace access", func(t *testing.T) {
res, err := server.Check(context.Background(), newRead("user:7", 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(), newRead("user:7", 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(), newRead("user:8", 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(), newRead("user:8", 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(), newRead("user:8", 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", ""))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
}

View File

@ -92,7 +92,8 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
common.NewNamespaceResourceTuple("user:7", "read", folderGroup, folderResource),
common.NewFolderParentTuple("5", "4"),
common.NewFolderParentTuple("6", "5"),
common.NewFolderResourceTuple("user:8", "view", dashboardGroup, dashboardResource, "5"),
common.NewFolderResourceTuple("user:8", "edit", dashboardGroup, dashboardResource, "5"),
common.NewFolderResourceTuple("user:9", "create", dashboardGroup, dashboardResource, "5"),
},
},
})