Zanzana: Refactor basic role write APIs (#113397)

* Zanzana: Refactor basic role write APIs

* Fix updates

* fix linter
This commit is contained in:
Alexander Zobnin
2025-11-04 16:29:56 +01:00
committed by GitHub
parent 8f8ed2bbec
commit 3fca7cf952
7 changed files with 532 additions and 340 deletions
File diff suppressed because it is too large Load Diff
@@ -32,6 +32,7 @@ message MutateOperation {
DeletePermissionOperation delete_permission = 4;
UpdateUserOrgRoleOperation update_user_org_role = 5;
DeleteUserOrgRoleOperation delete_user_org_role = 6;
AddUserOrgRoleOperation add_user_org_role = 7;
}
}
@@ -63,6 +64,14 @@ message DeletePermissionOperation {
Permission permission = 2;
}
message AddUserOrgRoleOperation {
// User UID
string user = 1;
// Role name (e.g: "Admin", "Editor", "Viewer")
string role = 2;
}
// UpdateUserOrgRoleOperation assigns the user's basic role and deletes existing basic role assignments.
message UpdateUserOrgRoleOperation {
// User UID
string user = 1;
@@ -1,6 +1,8 @@
package common
import (
"slices"
authlib "github.com/grafana/authlib/types"
dashboards "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
@@ -23,6 +25,14 @@ var basicRolesTranslations = map[string]string{
roleNone: "basic_none",
}
var basicRolesUIDs = []string{
"basic_grafana_admin",
"basic_admin",
"basic_editor",
"basic_viewer",
"basic_none",
}
type resourceTranslation struct {
typ string
group string
@@ -149,3 +159,7 @@ func TranslateToGroupResource(kind string) string {
func TranslateBasicRole(name string) string {
return basicRolesTranslations[name]
}
func IsBasicRole(name string) bool {
return slices.Contains(basicRolesUIDs, name)
}
@@ -465,3 +465,21 @@ func AddRenderContext(req *openfgav1.CheckRequest) {
),
})
}
func SplitTupleObject(object string) (string, string, string) {
var objectType, name, relation string
parts := strings.Split(object, ":")
if len(parts) < 2 {
return "", "", ""
}
objectType = parts[0]
nameRel := parts[1]
parts = strings.Split(nameRel, "#")
if len(parts) > 1 {
relation = parts[1]
}
name = parts[0]
return objectType, name, relation
}
@@ -77,7 +77,7 @@ func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, e
return OperationGroupFolder, nil
case *authzextv1.MutateOperation_CreatePermission, *authzextv1.MutateOperation_DeletePermission:
return OperationGroupPermission, nil
case *authzextv1.MutateOperation_UpdateUserOrgRole, *authzextv1.MutateOperation_DeleteUserOrgRole:
case *authzextv1.MutateOperation_UpdateUserOrgRole, *authzextv1.MutateOperation_DeleteUserOrgRole, *authzextv1.MutateOperation_AddUserOrgRole:
return OperationGroupUserOrgRole, nil
}
return OperationGroup(""), errors.New("unsupported mutate operation type")
@@ -18,18 +18,29 @@ func (s *Server) mutateOrgRoles(ctx context.Context, store *storeInfo, operation
for _, operation := range operations {
switch op := operation.Operation.(type) {
case *authzextv1.MutateOperation_UpdateUserOrgRole:
tuple, err := s.getUserOrgRoleWriteTuple(ctx, store, op.UpdateUserOrgRole)
if err != nil {
return err
case *authzextv1.MutateOperation_AddUserOrgRole:
basicRole := zanzana.TranslateBasicRole(op.AddUserOrgRole.GetRole())
tuple := &openfgav1.TupleKey{
User: zanzana.NewTupleEntry(zanzana.TypeUser, op.AddUserOrgRole.GetUser(), ""),
Relation: zanzana.RelationAssignee,
Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""),
}
writeTuples = append(writeTuples, tuple)
case *authzextv1.MutateOperation_DeleteUserOrgRole:
tuple, err := s.getUserOrgRoleDeleteTuple(ctx, store, op.DeleteUserOrgRole)
basicRole := zanzana.TranslateBasicRole(op.DeleteUserOrgRole.GetRole())
tuple := &openfgav1.TupleKeyWithoutCondition{
User: zanzana.NewTupleEntry(zanzana.TypeUser, op.DeleteUserOrgRole.GetUser(), ""),
Relation: zanzana.RelationAssignee,
Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""),
}
deleteTuples = append(deleteTuples, tuple)
case *authzextv1.MutateOperation_UpdateUserOrgRole:
writeTuple, existingTuples, err := s.getUserOrgRoleUpdateTuples(ctx, store, op.UpdateUserOrgRole)
if err != nil {
return err
}
deleteTuples = append(deleteTuples, tuple)
writeTuples = append(writeTuples, writeTuple)
deleteTuples = append(deleteTuples, existingTuples...)
default:
s.logger.Debug("unsupported mutate operation", "operation", op)
}
@@ -65,20 +76,38 @@ func (s *Server) mutateOrgRoles(ctx context.Context, store *storeInfo, operation
return nil
}
func (s *Server) getUserOrgRoleWriteTuple(ctx context.Context, store *storeInfo, req *authzextv1.UpdateUserOrgRoleOperation) (*openfgav1.TupleKey, error) {
basicRole := zanzana.TranslateBasicRole(req.GetRole())
return &openfgav1.TupleKey{
User: zanzana.NewTupleEntry(zanzana.TypeUser, req.GetUser(), ""),
Relation: zanzana.RelationAssignee,
Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""),
}, nil
}
func (s *Server) getUserOrgRoleUpdateTuples(ctx context.Context, store *storeInfo, req *authzextv1.UpdateUserOrgRoleOperation) (*openfgav1.TupleKey, []*openfgav1.TupleKeyWithoutCondition, error) {
readReq := &openfgav1.ReadRequest{
StoreId: store.ID,
TupleKey: &openfgav1.ReadRequestTupleKey{
User: zanzana.NewTupleEntry(zanzana.TypeUser, req.GetUser(), ""),
Relation: zanzana.RelationAssignee,
// read tuples by object type ("role:")
Object: zanzana.NewTupleEntry(zanzana.TypeRole, "", ""),
},
}
res, err := s.openfga.Read(ctx, readReq)
if err != nil {
return nil, nil, err
}
existingBasicRoleTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0)
for _, tuple := range res.GetTuples() {
_, roleName, _ := zanzana.SplitTupleObject(tuple.GetKey().GetObject())
if zanzana.IsBasicRole(roleName) {
existingBasicRoleTuples = append(existingBasicRoleTuples, &openfgav1.TupleKeyWithoutCondition{
User: tuple.GetKey().GetUser(),
Relation: tuple.GetKey().GetRelation(),
Object: tuple.GetKey().GetObject(),
})
}
}
func (s *Server) getUserOrgRoleDeleteTuple(ctx context.Context, store *storeInfo, req *authzextv1.DeleteUserOrgRoleOperation) (*openfgav1.TupleKeyWithoutCondition, error) {
basicRole := zanzana.TranslateBasicRole(req.GetRole())
return &openfgav1.TupleKeyWithoutCondition{
writeTuple := &openfgav1.TupleKey{
User: zanzana.NewTupleEntry(zanzana.TypeUser, req.GetUser(), ""),
Relation: zanzana.RelationAssignee,
Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""),
}, nil
}
return writeTuple, existingBasicRoleTuples, nil
}
@@ -36,14 +36,6 @@ func testMutateOrgRoles(t *testing.T, srv *Server) {
},
},
},
{
Operation: &v1.MutateOperation_DeleteUserOrgRole{
DeleteUserOrgRole: &v1.DeleteUserOrgRoleOperation{
User: "1",
Role: "Editor",
},
},
},
},
})
require.NoError(t, err)
@@ -69,4 +61,50 @@ func testMutateOrgRoles(t *testing.T, srv *Server) {
require.NoError(t, err)
require.Len(t, res.Tuples, 0)
})
t.Run("should add user org role and delete old role", func(t *testing.T) {
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
Namespace: "default",
Operations: []*v1.MutateOperation{
{
Operation: &v1.MutateOperation_AddUserOrgRole{
AddUserOrgRole: &v1.AddUserOrgRoleOperation{
User: "1",
Role: "Viewer",
},
},
},
{
Operation: &v1.MutateOperation_DeleteUserOrgRole{
DeleteUserOrgRole: &v1.DeleteUserOrgRoleOperation{
User: "1",
Role: "Admin",
},
},
},
},
})
require.NoError(t, err)
res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{
Namespace: "default",
TupleKey: &v1.ReadRequestTupleKey{
Relation: common.RelationAssignee,
Object: "role:basic_admin",
},
})
require.NoError(t, err)
require.Len(t, res.Tuples, 0)
res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{
Namespace: "default",
TupleKey: &v1.ReadRequestTupleKey{
Relation: common.RelationAssignee,
Object: "role:basic_viewer",
},
})
require.NoError(t, err)
require.Len(t, res.Tuples, 1)
require.Equal(t, "user:1", res.Tuples[0].Key.User)
})
}