Zanzana: Use modular schema (#92001)

* Zanzana: Use modular schema

* Fix tests

* Add module transform tests
This commit is contained in:
Alexander Zobnin 2024-08-19 11:10:51 +02:00 committed by GitHub
parent dcdef1a02d
commit 87c4f2448c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 395 additions and 169 deletions

View File

@ -9,6 +9,7 @@ import (
"google.golang.org/protobuf/types/known/wrapperspb"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/openfga/language/pkg/go/transformer"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/authz/zanzana/schema"
@ -28,16 +29,16 @@ func WithLogger(logger log.Logger) ClientOption {
}
}
func WithSchema(dsl string) ClientOption {
func WithSchema(modules []transformer.ModuleFile) ClientOption {
return func(c *Client) {
c.dsl = dsl
c.modules = modules
}
}
type Client struct {
logger log.Logger
client openfgav1.OpenFGAServiceClient
dsl string
modules []transformer.ModuleFile
tenantID string
storeID string
modelID string
@ -60,8 +61,8 @@ func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption)
c.tenantID = "stack-default"
}
if c.dsl == "" {
c.dsl = schema.DSL
if c.modules == nil || len(c.modules) == 0 {
c.modules = schema.SchemaModules
}
store, err := c.getOrCreateStore(ctx, c.tenantID)
@ -71,7 +72,7 @@ func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption)
c.storeID = store.GetId()
modelID, err := c.loadModel(ctx, c.storeID, c.dsl)
modelID, err := c.loadModel(ctx, c.storeID, c.modules)
if err != nil {
return nil, err
}
@ -151,9 +152,14 @@ func (c *Client) getStore(ctx context.Context, name string) (*openfgav1.Store, e
}
}
func (c *Client) loadModel(ctx context.Context, storeID string, dsl string) (string, error) {
func (c *Client) loadModel(ctx context.Context, storeID string, modules []transformer.ModuleFile) (string, error) {
var continuationToken string
model, err := schema.TransformModulesToModel(modules)
if err != nil {
return "", err
}
for {
// ReadAuthorizationModels returns authorization models for a store sorted in descending order of creation.
// So with a pageSize of 1 we will get the latest model.
@ -167,16 +173,10 @@ func (c *Client) loadModel(ctx context.Context, storeID string, dsl string) (str
return "", fmt.Errorf("failed to load authorization model: %w", err)
}
for _, model := range res.GetAuthorizationModels() {
// We need to first convert stored model into dsl and compare it to provided dsl.
storedDSL, err := schema.TransformToDSL(model)
if err != nil {
return "", err
}
for _, m := range res.GetAuthorizationModels() {
// If provided dsl is equal to a stored dsl we use that as the authorization id
if schema.EqualModels(dsl, storedDSL) {
return model.GetId(), nil
if schema.EqualModels(m, model) {
return m.GetId(), nil
}
}
@ -188,11 +188,6 @@ func (c *Client) loadModel(ctx context.Context, storeID string, dsl string) (str
continuationToken = res.GetContinuationToken()
}
model, err := schema.TransformToModel(dsl)
if err != nil {
return "", err
}
writeRes, err := c.client.WriteAuthorizationModel(ctx, &openfgav1.WriteAuthorizationModelRequest{
StoreId: c.storeID,
TypeDefinitions: model.GetTypeDefinitions(),

View File

@ -5,13 +5,15 @@ import (
"testing"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/openfga/language/pkg/go/transformer"
"github.com/fullstorydev/grpchan/inprocgrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
zserver "github.com/grafana/grafana/pkg/services/authz/zanzana/server"
zstore "github.com/grafana/grafana/pkg/services/authz/zanzana/store"
@ -63,12 +65,14 @@ func TestIntegrationClient(t *testing.T) {
t.Run("should update authorization model if it has new changes", func(t *testing.T) {
dsl := `
model
schema 1.1
module core
type user
`
c, err := New(context.Background(), conn, WithTenantID("new"), WithSchema(dsl))
modules := []transformer.ModuleFile{
{Name: "core.fga", Contents: dsl},
}
c, err := New(context.Background(), conn, WithTenantID("new"), WithSchema(modules))
require.NoError(t, err)
assert.Equal(t, prevStoreID, c.storeID)

View File

@ -0,0 +1,37 @@
module core
type instance
type user
type org
relations
define instance: [instance]
define member: [user]
# team management
define team_create: [role#assignee]
define team_read: [role#assignee]
define team_write: [role#assignee] or team_create
define team_delete: [role#assignee] or team_write
define team_permissions_write: [role#assignee]
define team_permissions_read: [role#assignee] or team_permissions_write
type role
relations
define org: [org]
define instance: [instance]
define assignee: [user, team#member, role#assignee]
type team
relations
define org: [org]
define admin: [user]
define member: [user] or admin
define read: [role#assignee] or member or team_read from org
define write: [role#assignee] or admin or team_write from org
define delete: [role#assignee] or admin or team_delete from org
define permissions_read: [role#assignee] or admin or team_permissions_read from org
define permissions_write: [role#assignee] or admin or team_permissions_write from org

View File

@ -0,0 +1,34 @@
module dashboard
extend type org
relations
define dashboard_annotations_create: [role#assignee]
define dashboard_annotations_read: [role#assignee]
define dashboard_annotations_write: [role#assignee]
define dashboard_annotations_delete: [role#assignee]
define dashboard_create: [role#assignee]
define dashboard_delete: [role#assignee]
define dashboard_permissions_read: [role#assignee]
define dashboard_permissions_write: [role#assignee]
define dashboard_public_write: [role#assignee] or dashboard_write
define dashboard_read: [role#assignee]
define dashboard_write: [role#assignee]
type dashboard
relations
define org: [org]
define parent: [folder]
define read: [user, team#member, role#assignee] or dashboard_read from parent or dashboard_read from org
define write: [user, team#member, role#assignee] or dashboard_write from parent or dashboard_write from org
define delete: [user, team#member, role#assignee] or dashboard_delete from parent or dashboard_delete from org
define create: [user, team#member, role#assignee] or dashboard_create from parent or dashboard_create from org
define permissions_read: [user, team#member, role#assignee] or dashboard_permissions_read from parent or dashboard_permissions_read from org
define permissions_write: [user, team#member, role#assignee] or dashboard_permissions_write from parent or dashboard_permissions_write from org
define public_write: [user, team#member, role#assignee] or dashboard_public_write from parent or dashboard_public_write from org or write
define annotations_create: [user, team#member, role#assignee] or dashboard_annotations_create from parent or dashboard_annotations_create from org
define annotations_read: [user, team#member, role#assignee] or dashboard_annotations_read from parent or dashboard_annotations_read from org
define annotations_write: [user, team#member, role#assignee] or dashboard_annotations_write from parent or dashboard_annotations_write from org
define annotations_delete: [user, team#member, role#assignee] or dashboard_annotations_delete from parent or dashboard_annotations_delete from org

View File

@ -0,0 +1,62 @@
module folder
extend type org
relations
define folder_create: [role#assignee]
define folder_read: [role#assignee] or folder_delete
define folder_write: [role#assignee] or folder_create
define folder_delete: [role#assignee] or folder_write
define folder_permissions_write: [role#assignee]
define folder_permissions_read: [role#assignee] or folder_permissions_write
define library_panel_create: [role#assignee]
define library_panel_read: [role#assignee] or library_panel_write
define library_panel_write: [role#assignee] or library_panel_create
define library_panel_delete: [role#assignee] or library_panel_create
define alert_rule_create: [role#assignee]
define alert_rule_read: [role#assignee] or alert_rule_write
define alert_rule_write: [role#assignee] or alert_rule_create
define alert_rule_delete: [role#assignee] or alert_rule_write
define alert_silence_create: [role#assignee]
define alert_silence_delete: [role#assignee] or alert_silence_write
define alert_silence_read: [role#assignee] or alert_silence_write
define alert_silence_write: [role#assignee] or alert_silence_create
type folder
relations
define parent: [folder]
define org: [org]
define create: [user, team#member, role#assignee] or create from parent or folder_create from org
define read: [user, team#member, role#assignee] or read from parent or folder_read from org
define write: [user, team#member, role#assignee] or write from parent or folder_write from org
define delete: [user, team#member, role#assignee] or delete from parent or folder_delete from org
define permissions_read: [user, team#member, role#assignee] or permissions_read from parent or folder_permissions_read from org
define permissions_write: [user, team#member, role#assignee] or permissions_write from parent or folder_permissions_write from org
define dashboard_create: [user, team#member, role#assignee] or dashboard_create from parent or dashboard_create from org
define dashboard_read: [user, team#member, role#assignee] or dashboard_read from parent or dashboard_read from org
define dashboard_write: [user, team#member, role#assignee] or dashboard_write from parent or dashboard_write from org
define dashboard_delete: [user, team#member, role#assignee] or dashboard_delete from parent or dashboard_delete from org
define dashboard_permissions_read: [user, team#member, role#assignee] or dashboard_permissions_read from parent or dashboard_permissions_read from org
define dashboard_permissions_write: [user, team#member, role#assignee] or dashboard_permissions_write from parent or dashboard_permissions_write from org
define dashboard_public_write: [user, team#member, role#assignee] or dashboard_public_write from parent or dashboard_public_write from org or dashboard_write
define dashboard_annotations_create: [user, team#member, role#assignee] or dashboard_annotations_create from parent or dashboard_annotations_create from org
define dashboard_annotations_read: [user, team#member, role#assignee] or dashboard_annotations_read from parent or dashboard_annotations_read from org
define dashboard_annotations_write: [user, team#member, role#assignee] or dashboard_annotations_write from parent or dashboard_annotations_write from org
define dashboard_annotations_delete: [user, team#member, role#assignee] or dashboard_annotations_delete from parent or dashboard_annotations_delete from org
define library_panel_create: [user, team#member, role#assignee] or library_panel_create from parent or library_panel_create from org
define library_panel_read: [user, team#member, role#assignee] or library_panel_read from parent or library_panel_read from org or library_panel_write
define library_panel_write: [user, team#member, role#assignee] or library_panel_write from parent or library_panel_write from org or library_panel_create
define library_panel_delete: [user, team#member, role#assignee] or library_panel_delete from parent or library_panel_delete from org or library_panel_create
define alert_rule_create: [user, team#member, role#assignee] or alert_rule_create from parent or alert_rule_create from org
define alert_rule_read: [user, team#member, role#assignee] or alert_rule_read from parent or alert_rule_read from org or alert_rule_write
define alert_rule_write: [user, team#member, role#assignee] or alert_rule_write from parent or alert_rule_write from org or alert_rule_create
define alert_rule_delete: [user, team#member, role#assignee] or alert_rule_delete from parent or alert_rule_delete from org or alert_rule_write
define alert_silence_create: [user, team#member, role#assignee] or alert_silence_create from parent or alert_silence_create from org
define alert_silence_read: [user, team#member, role#assignee] or alert_silence_read from parent or alert_silence_read from org or alert_silence_write
define alert_silence_write: [user, team#member, role#assignee] or alert_silence_write from parent or alert_silence_write from org or alert_silence_create

View File

@ -1,126 +0,0 @@
model
schema 1.1
type instance
type user
type org
relations
define instance: [instance]
define member: [user]
# team management
define team_create: [role#assignee]
define team_read: [role#assignee]
define team_write: [role#assignee] or team_create
define team_delete: [role#assignee] or team_write
define team_permissions_write: [role#assignee]
define team_permissions_read: [role#assignee] or team_permissions_write
define folder_create: [role#assignee]
define folder_read: [role#assignee] or folder_delete
define folder_write: [role#assignee] or folder_create
define folder_delete: [role#assignee] or folder_write
define folder_permissions_write: [role#assignee]
define folder_permissions_read: [role#assignee] or folder_permissions_write
define dashboard_annotations_create: [role#assignee]
define dashboard_annotations_read: [role#assignee]
define dashboard_annotations_write: [role#assignee]
define dashboard_annotations_delete: [role#assignee]
define dashboard_create: [role#assignee]
define dashboard_delete: [role#assignee]
define dashboard_permissions_read: [role#assignee]
define dashboard_permissions_write: [role#assignee]
define dashboard_public_write: [role#assignee] or dashboard_write
define dashboard_read: [role#assignee]
define dashboard_write: [role#assignee]
define library_panel_create: [role#assignee]
define library_panel_read: [role#assignee] or library_panel_write
define library_panel_write: [role#assignee] or library_panel_create
define library_panel_delete: [role#assignee] or library_panel_create
define alert_rule_create: [role#assignee]
define alert_rule_read: [role#assignee] or alert_rule_write
define alert_rule_write: [role#assignee] or alert_rule_create
define alert_rule_delete: [role#assignee] or alert_rule_write
define alert_silence_create: [role#assignee]
define alert_silence_delete: [role#assignee] or alert_silence_write
define alert_silence_read: [role#assignee] or alert_silence_write
define alert_silence_write: [role#assignee] or alert_silence_create
type role
relations
define org: [org]
define instance: [instance]
define assignee: [user, team#member, role#assignee]
type team
relations
define org: [org]
define admin: [user]
define member: [user] or admin
define read: [role#assignee] or member or team_read from org
define write: [role#assignee] or admin or team_write from org
define delete: [role#assignee] or admin or team_delete from org
define permissions_read: [role#assignee] or admin or team_permissions_read from org
define permissions_write: [role#assignee] or admin or team_permissions_write from org
type folder
relations
define parent: [folder]
define org: [org]
define create: [user, team#member, role#assignee] or create from parent or folder_create from org
define read: [user, team#member, role#assignee] or read from parent or folder_read from org
define write: [user, team#member, role#assignee] or write from parent or folder_write from org
define delete: [user, team#member, role#assignee] or delete from parent or folder_delete from org
define permissions_read: [user, team#member, role#assignee] or permissions_read from parent or folder_permissions_read from org
define permissions_write: [user, team#member, role#assignee] or permissions_write from parent or folder_permissions_write from org
define dashboard_create: [user, team#member, role#assignee] or dashboard_create from parent or dashboard_create from org
define dashboard_read: [user, team#member, role#assignee] or dashboard_read from parent or dashboard_read from org
define dashboard_write: [user, team#member, role#assignee] or dashboard_write from parent or dashboard_write from org
define dashboard_delete: [user, team#member, role#assignee] or dashboard_delete from parent or dashboard_delete from org
define dashboard_permissions_read: [user, team#member, role#assignee] or dashboard_permissions_read from parent or dashboard_permissions_read from org
define dashboard_permissions_write: [user, team#member, role#assignee] or dashboard_permissions_write from parent or dashboard_permissions_write from org
define dashboard_public_write: [user, team#member, role#assignee] or dashboard_public_write from parent or dashboard_public_write from org or dashboard_write
define dashboard_annotations_create: [user, team#member, role#assignee] or dashboard_annotations_create from parent or dashboard_annotations_create from org
define dashboard_annotations_read: [user, team#member, role#assignee] or dashboard_annotations_read from parent or dashboard_annotations_read from org
define dashboard_annotations_write: [user, team#member, role#assignee] or dashboard_annotations_write from parent or dashboard_annotations_write from org
define dashboard_annotations_delete: [user, team#member, role#assignee] or dashboard_annotations_delete from parent or dashboard_annotations_delete from org
define library_panel_create: [user, team#member, role#assignee] or library_panel_create from parent or library_panel_create from org
define library_panel_read: [user, team#member, role#assignee] or library_panel_read from parent or library_panel_read from org or library_panel_write
define library_panel_write: [user, team#member, role#assignee] or library_panel_write from parent or library_panel_write from org or library_panel_create
define library_panel_delete: [user, team#member, role#assignee] or library_panel_delete from parent or library_panel_delete from org or library_panel_create
define alert_rule_create: [user, team#member, role#assignee] or alert_rule_create from parent or alert_rule_create from org
define alert_rule_read: [user, team#member, role#assignee] or alert_rule_read from parent or alert_rule_read from org or alert_rule_write
define alert_rule_write: [user, team#member, role#assignee] or alert_rule_write from parent or alert_rule_write from org or alert_rule_create
define alert_rule_delete: [user, team#member, role#assignee] or alert_rule_delete from parent or alert_rule_delete from org or alert_rule_write
define alert_silence_create: [user, team#member, role#assignee] or alert_silence_create from parent or alert_silence_create from org
define alert_silence_read: [user, team#member, role#assignee] or alert_silence_read from parent or alert_silence_read from org or alert_silence_write
define alert_silence_write: [user, team#member, role#assignee] or alert_silence_write from parent or alert_silence_write from org or alert_silence_create
# Dashboard
type dashboard
relations
define org: [org]
define parent: [folder]
define read: [user, team#member, role#assignee] or dashboard_read from parent or dashboard_read from org
define write: [user, team#member, role#assignee] or dashboard_write from parent or dashboard_write from org
define delete: [user, team#member, role#assignee] or dashboard_delete from parent or dashboard_delete from org
define create: [user, team#member, role#assignee] or dashboard_create from parent or dashboard_create from org
define permissions_read: [user, team#member, role#assignee] or dashboard_permissions_read from parent or dashboard_permissions_read from org
define permissions_write: [user, team#member, role#assignee] or dashboard_permissions_write from parent or dashboard_permissions_write from org
define public_write: [user, team#member, role#assignee] or dashboard_public_write from parent or dashboard_public_write from org or write
define annotations_create: [user, team#member, role#assignee] or dashboard_annotations_create from parent or dashboard_annotations_create from org
define annotations_read: [user, team#member, role#assignee] or dashboard_annotations_read from parent or dashboard_annotations_read from org
define annotations_write: [user, team#member, role#assignee] or dashboard_annotations_write from parent or dashboard_annotations_write from org
define annotations_delete: [user, team#member, role#assignee] or dashboard_annotations_delete from parent or dashboard_annotations_delete from org

View File

@ -2,7 +2,30 @@ package schema
import (
_ "embed"
"github.com/openfga/language/pkg/go/transformer"
)
//go:embed schema.fga
var DSL string
//go:embed core.fga
var coreDSL string
//go:embed dashboard.fga
var dashboardDSL string
//go:embed folder.fga
var folderDSL string
var SchemaModules = []transformer.ModuleFile{
{
Name: "core.fga",
Contents: coreDSL,
},
{
Name: "dashboard.fga",
Contents: dashboardDSL,
},
{
Name: "folder.fga",
Contents: folderDSL,
},
}

View File

@ -6,9 +6,19 @@ import (
openfgav1 "github.com/openfga/api/proto/openfga/v1"
language "github.com/openfga/language/pkg/go/transformer"
"google.golang.org/protobuf/encoding/protojson"
)
func TransformToModel(dsl string) (*openfgav1.AuthorizationModel, error) {
func TransformModulesToModel(modules []language.ModuleFile) (*openfgav1.AuthorizationModel, error) {
parsedAuthModel, err := language.TransformModuleFilesToModel(modules, "1.2")
if err != nil {
return nil, fmt.Errorf("failed to transform dsl to model: %w", err)
}
return parsedAuthModel, nil
}
func TransformDSLToModel(dsl string) (*openfgav1.AuthorizationModel, error) {
parsedAuthModel, err := language.TransformDSLToProto(dsl)
if err != nil {
return nil, fmt.Errorf("failed to transform dsl to model: %w", err)
@ -17,27 +27,32 @@ func TransformToModel(dsl string) (*openfgav1.AuthorizationModel, error) {
return parsedAuthModel, nil
}
func TransformToDSL(model *openfgav1.AuthorizationModel) (string, error) {
return language.TransformJSONProtoToDSL(model)
func TransformToDSL(model *openfgav1.AuthorizationModel, opts ...language.TransformOption) (string, error) {
return language.TransformJSONProtoToDSL(model, opts...)
}
// FIXME(kalleep): We need to figure out a better way to compare equality of two different
// authorization model. For now the easiest way I found to comparing different schemas was
// to convert them into their json representation but this requires us to first convert dsl into
// openfgav1.AuthorizationModel and then later parse it as json.
// Comparing parsed authorization model with authorization model from store directly by parsing them as
// as json won't work because stored model will have some fields set such as id that are not present in a parsed
// dsl from disk.
func EqualModels(a, b string) bool {
astr, err := language.TransformDSLToJSON(a)
// EqualModels compares two authorization models.
// Id is not comparing since model loaded from disk doesn't contain Id.
func EqualModels(a, b *openfgav1.AuthorizationModel) bool {
aCopy := openfgav1.AuthorizationModel{
SchemaVersion: a.SchemaVersion,
TypeDefinitions: a.TypeDefinitions,
Conditions: a.Conditions,
}
aJSONBytes, err := protojson.Marshal(&aCopy)
if err != nil {
return false
}
bstr, err := language.TransformDSLToJSON(b)
bCopy := openfgav1.AuthorizationModel{
SchemaVersion: b.SchemaVersion,
TypeDefinitions: b.TypeDefinitions,
Conditions: b.Conditions,
}
bJSONBytes, err := protojson.Marshal(&bCopy)
if err != nil {
return false
}
return astr == bstr
return string(aJSONBytes) == string(bJSONBytes)
}

View File

@ -3,6 +3,7 @@ package schema
import (
"testing"
"github.com/openfga/language/pkg/go/transformer"
"github.com/stretchr/testify/assert"
)
@ -125,7 +126,188 @@ type role
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
assert.Equal(t, tt.expected, EqualModels(tt.a, tt.b))
modelA, err := transformer.TransformDSLToProto(tt.a)
assert.NoError(t, err)
modelB, err := transformer.TransformDSLToProto(tt.b)
assert.NoError(t, err)
assert.Equal(t, tt.expected, EqualModels(modelA, modelB))
})
}
}
func TestModulesEqualModels(t *testing.T) {
type testCase struct {
desc string
a []transformer.ModuleFile
b []transformer.ModuleFile
expected bool
}
tests := []testCase{
{
desc: "should be equal",
a: []transformer.ModuleFile{
{
Name: "core.fga",
Contents: `
module core
type instance
type user
type org
relations
define instance: [instance]
define member: [user]
define viewer: [user]
type role
relations
define org: [org]
define instance: [instance]
define assignee: [user, team#member, role#assignee]
`,
},
{
Name: "team.fga",
Contents: `
module team
type team
relations
define org: [org]
define admin: [user]
define member: [user] or org
`,
},
},
b: []transformer.ModuleFile{
{
Name: "core.fga",
Contents: `
module core
type instance
type user
type org
relations
define instance: [instance]
define member: [user]
define viewer: [user]
type role
relations
define org: [org]
define instance: [instance]
define assignee: [user, team#member, role#assignee]
`,
},
{
Name: "team.fga",
Contents: `
module team
type team
relations
define org: [org]
define admin: [user]
define member: [user] or org
`,
},
},
expected: true,
},
{
desc: "should not be equal",
a: []transformer.ModuleFile{
{
Name: "core.fga",
Contents: `
module core
type instance
type user
type org
relations
define instance: [instance]
define member: [user]
define viewer: [user]
type role
relations
define org: [org]
define instance: [instance]
define assignee: [user, team#member, role#assignee]
`,
},
{
Name: "team.fga",
Contents: `
module team
type team
relations
define org: [org]
define admin: [user]
define member: [user] or org
`,
},
},
b: []transformer.ModuleFile{
{
Name: "core.fga",
Contents: `
module core
type instance
type user
type org
relations
define instance: [instance]
define member: [user]
define viewer: [user]
type role
relations
define org: [org]
define instance: [instance]
define assignee: [user, team#member, role#assignee]
`,
},
{
Name: "folder.fga",
Contents: `
module folder
type folder
relations
define parent: [folder]
define org: [org]
`,
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
modelA, err := TransformModulesToModel(tt.a)
assert.NoError(t, err)
modelB, err := TransformModulesToModel(tt.b)
assert.NoError(t, err)
assert.Equal(t, tt.expected, EqualModels(modelA, modelB))
})
}
}