Zanzana: Role write APIs (#114533)
* Zanzana: Role write APIs * Add tests * Update pkg/services/authz/zanzana/server/server_mutate_roles.go Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * fix func usage --------- Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
co-authored by
Gabriel MABILLE
parent
8e7ba60b93
commit
358d0eb266
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,8 @@ message MutateOperation {
|
||||
DeleteRoleBindingOperation delete_role_binding = 9;
|
||||
CreateTeamBindingOperation create_team_binding = 10;
|
||||
DeleteTeamBindingOperation delete_team_binding = 11;
|
||||
CreateRoleOperation create_role = 12;
|
||||
DeleteRoleOperation delete_role = 13;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +133,29 @@ message DeleteTeamBindingOperation {
|
||||
string permission = 3;
|
||||
}
|
||||
|
||||
message CreateRoleOperation {
|
||||
// kind of the role (Role/CoreRole/GlobalRole)
|
||||
string role_kind = 1;
|
||||
// uid of the role
|
||||
string role_name = 2;
|
||||
// permissions of the role
|
||||
repeated RolePermission permissions = 3;
|
||||
}
|
||||
|
||||
message DeleteRoleOperation {
|
||||
// kind of the role (Role/CoreRole/GlobalRole)
|
||||
string role_kind = 1;
|
||||
// uid of the role
|
||||
string role_name = 2;
|
||||
// permissions of the role
|
||||
repeated RolePermission permissions = 3;
|
||||
}
|
||||
|
||||
message RolePermission {
|
||||
string action = 1;
|
||||
string scope = 2;
|
||||
}
|
||||
|
||||
message Resource {
|
||||
// group of the resource (e.g: "dashboard.grafana.app")
|
||||
string group = 1;
|
||||
|
||||
@@ -447,6 +447,14 @@ func ToOpenFGATuples(tuples []*authzextv1.Tuple) []*openfgav1.Tuple {
|
||||
return result
|
||||
}
|
||||
|
||||
func ToOpenFGADeleteTupleKey(tuples *openfgav1.TupleKey) *openfgav1.TupleKeyWithoutCondition {
|
||||
return &openfgav1.TupleKeyWithoutCondition{
|
||||
User: tuples.GetUser(),
|
||||
Relation: tuples.GetRelation(),
|
||||
Object: tuples.GetObject(),
|
||||
}
|
||||
}
|
||||
|
||||
func AddRenderContext(req *openfgav1.CheckRequest) {
|
||||
if req.ContextualTuples == nil {
|
||||
req.ContextualTuples = &openfgav1.ContextualTupleKeys{}
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
OperationGroupUserOrgRole OperationGroup = "user_org_role"
|
||||
OperationGroupRoleBinding OperationGroup = "role_binding"
|
||||
OperationGroupTeamBinding OperationGroup = "team_binding"
|
||||
OperationGroupRole OperationGroup = "role"
|
||||
)
|
||||
|
||||
func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) {
|
||||
@@ -73,6 +74,10 @@ func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au
|
||||
if err := s.mutateTeamBindings(ctx, storeInf, operations); err != nil {
|
||||
return nil, fmt.Errorf("failed to mutate team bindings: %w", err)
|
||||
}
|
||||
case OperationGroupRole:
|
||||
if err := s.mutateRoles(ctx, storeInf, operations); err != nil {
|
||||
return nil, fmt.Errorf("failed to mutate roles: %w", err)
|
||||
}
|
||||
default:
|
||||
s.logger.Warn("unsupported operation group", "operationGroup", operationGroup)
|
||||
}
|
||||
@@ -93,6 +98,8 @@ func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, e
|
||||
return OperationGroupRoleBinding, nil
|
||||
case *authzextv1.MutateOperation_CreateTeamBinding, *authzextv1.MutateOperation_DeleteTeamBinding:
|
||||
return OperationGroupTeamBinding, nil
|
||||
case *authzextv1.MutateOperation_CreateRole, *authzextv1.MutateOperation_DeleteRole:
|
||||
return OperationGroupRole, nil
|
||||
}
|
||||
return OperationGroup(""), errors.New("unsupported mutate operation type")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
func (s *Server) mutateRoles(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error {
|
||||
ctx, span := s.tracer.Start(ctx, "server.mutateRoles")
|
||||
defer span.End()
|
||||
|
||||
writeTuples := make([]*openfgav1.TupleKey, 0)
|
||||
deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0)
|
||||
|
||||
for _, operation := range operations {
|
||||
switch op := operation.Operation.(type) {
|
||||
case *authzextv1.MutateOperation_CreateRole:
|
||||
tuples, err := convertRoleToTuples(op.CreateRole.RoleName, op.CreateRole.Permissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeTuples = append(writeTuples, tuples...)
|
||||
case *authzextv1.MutateOperation_DeleteRole:
|
||||
tuples, err := convertRoleToTuples(op.DeleteRole.RoleName, op.DeleteRole.Permissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deletes := make([]*openfgav1.TupleKeyWithoutCondition, 0, len(tuples))
|
||||
for _, tuple := range tuples {
|
||||
deletes = append(deletes, common.ToOpenFGADeleteTupleKey(tuple))
|
||||
}
|
||||
deleteTuples = append(deleteTuples, deletes...)
|
||||
default:
|
||||
s.logger.Debug("unsupported mutate operation", "operation", op)
|
||||
}
|
||||
}
|
||||
|
||||
writeReq := &openfgav1.WriteRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
}
|
||||
if len(writeTuples) > 0 {
|
||||
writeReq.Writes = &openfgav1.WriteRequestWrites{
|
||||
TupleKeys: writeTuples,
|
||||
OnDuplicate: "ignore",
|
||||
}
|
||||
}
|
||||
if len(deleteTuples) > 0 {
|
||||
writeReq.Deletes = &openfgav1.WriteRequestDeletes{
|
||||
TupleKeys: deleteTuples,
|
||||
OnMissing: "ignore",
|
||||
}
|
||||
}
|
||||
|
||||
_, err := s.openfga.Write(ctx, writeReq)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to write resource role binding tuples", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertRoleToTuples converts role and its permissions (action/scope) to v1 TupleKey format
|
||||
// using the shared zanzana.ConvertRolePermissionsToTuples utility and common.ToAuthzExtTupleKeys
|
||||
func convertRoleToTuples(roleUID string, permissions []*authzextv1.RolePermission) ([]*openfgav1.TupleKey, error) {
|
||||
// Convert to zanzana.RolePermission
|
||||
rolePerms := make([]zanzana.RolePermission, 0, len(permissions))
|
||||
for _, perm := range permissions {
|
||||
// Split the scope to get kind, attribute, identifier
|
||||
kind, _, identifier := splitScope(perm.Scope)
|
||||
rolePerms = append(rolePerms, zanzana.RolePermission{
|
||||
Action: perm.Action,
|
||||
Kind: kind,
|
||||
Identifier: identifier,
|
||||
})
|
||||
}
|
||||
|
||||
// Translate to Zanzana tuples
|
||||
tuples, err := zanzana.ConvertRolePermissionsToTuples(roleUID, rolePerms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tuples, nil
|
||||
}
|
||||
|
||||
func splitScope(scope string) (string, string, string) {
|
||||
if scope == "" {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
fragments := strings.Split(scope, ":")
|
||||
switch l := len(fragments); l {
|
||||
case 1: // Splitting a wildcard scope "*" -> kind: "*"; attribute: "*"; identifier: "*"
|
||||
return fragments[0], fragments[0], fragments[0]
|
||||
case 2: // Splitting a wildcard scope with specified kind "dashboards:*" -> kind: "dashboards"; attribute: "*"; identifier: "*"
|
||||
return fragments[0], fragments[1], fragments[1]
|
||||
default: // Splitting a scope with all fields specified "dashboards:uid:my_dash" -> kind: "dashboards"; attribute: "uid"; identifier: "my_dash"
|
||||
return fragments[0], fragments[1], strings.Join(fragments[2:], ":")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
func setupMutateRoles(t *testing.T, srv *Server) *Server {
|
||||
t.Helper()
|
||||
|
||||
// seed tuples
|
||||
tuples := []*openfgav1.TupleKey{
|
||||
common.NewTuple("role:foo_viewer#assignee", "view", "group_resource:dashboard.grafana.app/dashboards"),
|
||||
}
|
||||
|
||||
return setupOpenFGADatabase(t, srv, tuples)
|
||||
}
|
||||
|
||||
func testMutateRoles(t *testing.T, srv *Server) {
|
||||
setupMutateRoles(t, srv)
|
||||
|
||||
t.Run("should update role and delete old role permissions", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
Operation: &v1.MutateOperation_CreateRole{
|
||||
CreateRole: &v1.CreateRoleOperation{
|
||||
RoleName: "foo_viewer",
|
||||
RoleKind: "Role",
|
||||
Permissions: []*v1.RolePermission{
|
||||
{
|
||||
Action: "dashboards:edit",
|
||||
Scope: "dashboards:*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Operation: &v1.MutateOperation_DeleteRole{
|
||||
DeleteRole: &v1.DeleteRoleOperation{
|
||||
RoleName: "foo_viewer",
|
||||
RoleKind: "Role",
|
||||
Permissions: []*v1.RolePermission{
|
||||
{
|
||||
Action: "dashboards:view",
|
||||
Scope: "dashboards:*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{
|
||||
Namespace: "default",
|
||||
TupleKey: &v1.ReadRequestTupleKey{
|
||||
User: "role:foo_viewer#assignee",
|
||||
Relation: "edit",
|
||||
Object: "group_resource:",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Tuples, 1)
|
||||
require.Equal(t, "role:foo_viewer#assignee", res.Tuples[0].Key.User)
|
||||
require.Equal(t, "group_resource:dashboard.grafana.app/dashboards", res.Tuples[0].Key.Object)
|
||||
require.Equal(t, "edit", res.Tuples[0].Key.Relation)
|
||||
})
|
||||
}
|
||||
@@ -144,6 +144,10 @@ func TestIntegrationServer(t *testing.T) {
|
||||
t.Run("test mutate team bindings", func(t *testing.T) {
|
||||
testMutateTeamBindings(t, srv)
|
||||
})
|
||||
|
||||
t.Run("test mutate roles", func(t *testing.T) {
|
||||
testMutateRoles(t, srv)
|
||||
})
|
||||
}
|
||||
|
||||
func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
|
||||
|
||||
Reference in New Issue
Block a user