Zanzana: Implement role bindings write APIs (#114385)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,8 @@ message MutateOperation {
|
||||
UpdateUserOrgRoleOperation update_user_org_role = 5;
|
||||
DeleteUserOrgRoleOperation delete_user_org_role = 6;
|
||||
AddUserOrgRoleOperation add_user_org_role = 7;
|
||||
CreateRoleBindingOperation create_role_binding = 8;
|
||||
DeleteRoleBindingOperation delete_role_binding = 9;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +89,28 @@ message DeleteUserOrgRoleOperation {
|
||||
string role = 2;
|
||||
}
|
||||
|
||||
message CreateRoleBindingOperation {
|
||||
// kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole)
|
||||
string subject_kind = 1;
|
||||
// uid of the identity
|
||||
string subject_name = 2;
|
||||
// kind of the role (Role/CoreRole/GlobalRole)
|
||||
string role_kind = 3;
|
||||
// uid of the role
|
||||
string role_name = 4;
|
||||
}
|
||||
|
||||
message DeleteRoleBindingOperation {
|
||||
// kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole)
|
||||
string subject_kind = 1;
|
||||
// uid of the identity
|
||||
string subject_name = 2;
|
||||
// kind of the role (Role/CoreRole/GlobalRole)
|
||||
string role_kind = 3;
|
||||
// uid of the role
|
||||
string role_name = 4;
|
||||
}
|
||||
|
||||
message Resource {
|
||||
// group of the resource (e.g: "dashboard.grafana.app")
|
||||
string group = 1;
|
||||
|
||||
@@ -15,6 +15,7 @@ const (
|
||||
OperationGroupFolder OperationGroup = "folder"
|
||||
OperationGroupPermission OperationGroup = "permission"
|
||||
OperationGroupUserOrgRole OperationGroup = "user_org_role"
|
||||
OperationGroupRoleBinding OperationGroup = "role_binding"
|
||||
)
|
||||
|
||||
func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) {
|
||||
@@ -63,6 +64,10 @@ func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au
|
||||
if err := s.mutateOrgRoles(ctx, storeInf, operations); err != nil {
|
||||
return nil, fmt.Errorf("failed to mutate org roles: %w", err)
|
||||
}
|
||||
case OperationGroupRoleBinding:
|
||||
if err := s.mutateRoleBindings(ctx, storeInf, operations); err != nil {
|
||||
return nil, fmt.Errorf("failed to mutate role bindings: %w", err)
|
||||
}
|
||||
default:
|
||||
s.logger.Warn("unsupported operation group", "operationGroup", operationGroup)
|
||||
}
|
||||
@@ -79,6 +84,8 @@ func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, e
|
||||
return OperationGroupPermission, nil
|
||||
case *authzextv1.MutateOperation_UpdateUserOrgRole, *authzextv1.MutateOperation_DeleteUserOrgRole, *authzextv1.MutateOperation_AddUserOrgRole:
|
||||
return OperationGroupUserOrgRole, nil
|
||||
case *authzextv1.MutateOperation_CreateRoleBinding, *authzextv1.MutateOperation_DeleteRoleBinding:
|
||||
return OperationGroupRoleBinding, nil
|
||||
}
|
||||
return OperationGroup(""), errors.New("unsupported mutate operation type")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
func (s *Server) mutateRoleBindings(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error {
|
||||
ctx, span := s.tracer.Start(ctx, "server.mutateRoleBindings")
|
||||
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_CreateRoleBinding:
|
||||
r := op.CreateRoleBinding
|
||||
tuple, err := s.getRoleBindingTuple(ctx, r.SubjectKind, r.SubjectName, r.RoleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeTuples = append(writeTuples, tuple)
|
||||
case *authzextv1.MutateOperation_DeleteRoleBinding:
|
||||
r := op.DeleteRoleBinding
|
||||
tuple, err := s.getRoleBindingTuple(ctx, r.SubjectKind, r.SubjectName, r.RoleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeTuple := &openfgav1.TupleKeyWithoutCondition{
|
||||
User: tuple.User,
|
||||
Relation: tuple.Relation,
|
||||
Object: tuple.Object,
|
||||
}
|
||||
deleteTuples = append(deleteTuples, writeTuple)
|
||||
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
|
||||
}
|
||||
|
||||
func (s *Server) getRoleBindingTuple(ctx context.Context, subjectKind string, subjectName string, roleName string) (*openfgav1.TupleKey, error) {
|
||||
zanzanaType := ""
|
||||
subjectRelation := ""
|
||||
|
||||
switch subjectKind {
|
||||
case string(iamv0.RoleBindingSpecSubjectKindUser):
|
||||
zanzanaType = zanzana.TypeUser
|
||||
case string(iamv0.RoleBindingSpecSubjectKindTeam):
|
||||
zanzanaType = zanzana.TypeTeam
|
||||
subjectRelation = zanzana.RelationTeamMember
|
||||
case string(iamv0.RoleBindingSpecSubjectKindServiceAccount):
|
||||
zanzanaType = zanzana.TypeServiceAccount
|
||||
case string(iamv0.RoleBindingSpecSubjectKindBasicRole):
|
||||
zanzanaType = zanzana.TypeRole
|
||||
subjectRelation = zanzana.RelationAssignee
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid subject kind: %s", subjectKind)
|
||||
}
|
||||
|
||||
tuple := &openfgav1.TupleKey{
|
||||
User: zanzana.NewTupleEntry(zanzanaType, subjectName, subjectRelation),
|
||||
Relation: zanzana.RelationAssignee,
|
||||
Object: zanzana.NewTupleEntry(zanzana.TypeRole, roleName, ""),
|
||||
}
|
||||
|
||||
return tuple, nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
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 setupMutateRoleBindings(t *testing.T, srv *Server) *Server {
|
||||
t.Helper()
|
||||
|
||||
// seed tuples
|
||||
tuples := []*openfgav1.TupleKey{
|
||||
common.NewTuple("user:1", common.RelationAssignee, "role:foo_viewer"),
|
||||
}
|
||||
|
||||
return setupOpenFGADatabase(t, srv, tuples)
|
||||
}
|
||||
|
||||
func testMutateRoleBindings(t *testing.T, srv *Server) {
|
||||
setupMutateRoleBindings(t, srv)
|
||||
|
||||
t.Run("should update user role and delete old role", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
Operation: &v1.MutateOperation_CreateRoleBinding{
|
||||
CreateRoleBinding: &v1.CreateRoleBindingOperation{
|
||||
SubjectKind: "User",
|
||||
SubjectName: "1",
|
||||
RoleKind: "Role",
|
||||
RoleName: "foo_editor",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Operation: &v1.MutateOperation_DeleteRoleBinding{
|
||||
DeleteRoleBinding: &v1.DeleteRoleBindingOperation{
|
||||
SubjectKind: "User",
|
||||
SubjectName: "1",
|
||||
RoleKind: "Role",
|
||||
RoleName: "foo_viewer",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{
|
||||
Namespace: "default",
|
||||
TupleKey: &v1.ReadRequestTupleKey{
|
||||
Relation: common.RelationAssignee,
|
||||
Object: "role:foo_editor",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Tuples, 1)
|
||||
require.Equal(t, "user:1", res.Tuples[0].Key.User)
|
||||
|
||||
res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{
|
||||
Namespace: "default",
|
||||
TupleKey: &v1.ReadRequestTupleKey{
|
||||
Relation: common.RelationAssignee,
|
||||
Object: "role:foo_viewer",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Tuples, 0)
|
||||
})
|
||||
|
||||
t.Run("should assign role to basic role", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
Operation: &v1.MutateOperation_CreateRoleBinding{
|
||||
CreateRoleBinding: &v1.CreateRoleBindingOperation{
|
||||
SubjectKind: "BasicRole",
|
||||
SubjectName: "basic_viewer",
|
||||
RoleKind: "Role",
|
||||
RoleName: "foo_bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{
|
||||
Namespace: "default",
|
||||
TupleKey: &v1.ReadRequestTupleKey{
|
||||
Relation: common.RelationAssignee,
|
||||
Object: "role:foo_bar",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Tuples, 1)
|
||||
require.Equal(t, "role:basic_viewer#assignee", res.Tuples[0].Key.User)
|
||||
})
|
||||
}
|
||||
@@ -136,6 +136,10 @@ func TestIntegrationServer(t *testing.T) {
|
||||
t.Run("test query folders", func(t *testing.T) {
|
||||
testQueryFolders(t, srv)
|
||||
})
|
||||
|
||||
t.Run("test mutate role bindings", func(t *testing.T) {
|
||||
testMutateRoleBindings(t, srv)
|
||||
})
|
||||
}
|
||||
|
||||
func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
|
||||
|
||||
Reference in New Issue
Block a user