157 lines
4.9 KiB
Go
157 lines
4.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
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"
|
|
)
|
|
|
|
var (
|
|
errEmptyName = errors.New("name cannot be empty")
|
|
errInvalidBasicRole = errors.New("invalid basic role")
|
|
errUnknownKind = errors.New("unknown permission kind")
|
|
)
|
|
|
|
func (s *Server) mutateResourcePermissions(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error {
|
|
ctx, span := s.tracer.Start(ctx, "server.mutateResourcePermissions")
|
|
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_CreatePermission:
|
|
tuple, err := s.getPermissionWriteTuple(ctx, op.CreatePermission)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeTuples = append(writeTuples, tuple)
|
|
case *authzextv1.MutateOperation_DeletePermission:
|
|
tuple, err := s.getPermissionDeleteTuple(ctx, op.DeletePermission)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
deleteTuples = append(deleteTuples, tuple)
|
|
default:
|
|
s.logger.Debug("unsupported mutate operation", "operation", op)
|
|
}
|
|
}
|
|
|
|
err := s.writeTuples(ctx, store, writeTuples, deleteTuples)
|
|
if err != nil {
|
|
s.logger.Error("failed to write resource permission tuples", "error", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) getPermissionWriteTuple(ctx context.Context, req *authzextv1.CreatePermissionOperation) (*openfgav1.TupleKey, error) {
|
|
resource := req.GetResource()
|
|
permission := req.GetPermission()
|
|
object := zanzana.NewObjectEntry(toZanzanaType(resource.GetGroup()), resource.GetGroup(), resource.GetResource(), "", resource.GetName())
|
|
tuple, err := NewResourceTuple(object, resource, permission)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tuple, nil
|
|
}
|
|
|
|
func (s *Server) getPermissionDeleteTuple(ctx context.Context, req *authzextv1.DeletePermissionOperation) (*openfgav1.TupleKeyWithoutCondition, error) {
|
|
resource := req.GetResource()
|
|
permission := req.GetPermission()
|
|
object := zanzana.NewObjectEntry(toZanzanaType(resource.GetGroup()), resource.GetGroup(), resource.GetResource(), "", resource.GetName())
|
|
tuple, err := NewResourceTuple(object, resource, permission)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &openfgav1.TupleKeyWithoutCondition{
|
|
User: tuple.GetUser(),
|
|
Relation: tuple.GetRelation(),
|
|
Object: tuple.GetObject(),
|
|
}, nil
|
|
}
|
|
|
|
func toZanzanaType(apiGroup string) string {
|
|
if apiGroup == "folder.grafana.app" {
|
|
return zanzana.TypeFolder
|
|
}
|
|
return zanzana.TypeResource
|
|
}
|
|
|
|
func NewResourceTuple(object string, resource *authzextv1.Resource, perm *authzextv1.Permission) (*openfgav1.TupleKey, error) {
|
|
// Typ is "folder" or "resource"
|
|
typ := toZanzanaType(resource.Group)
|
|
|
|
// subject
|
|
subject, err := toZanzanaSubject(perm.GetKind(), perm.GetName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := &openfgav1.TupleKey{
|
|
// e.g. "user:{uid}", "serviceaccount:{uid}", "team:{uid}", "basicrole:{viewer|editor|admin}"
|
|
User: subject,
|
|
// "view", "edit", "admin"
|
|
Relation: strings.ToLower(perm.Verb),
|
|
// e.g. "folder:{name}" or "resource:{apiGroup}/{resource}/{name}"
|
|
Object: object,
|
|
}
|
|
|
|
// For resources we add a condition to filter by apiGroup/resource
|
|
// e.g "group_filter": {"group_resource": "dashboards.grafana.app/dashboards"}
|
|
if typ == zanzana.TypeResource {
|
|
key.Condition = &openfgav1.RelationshipCondition{
|
|
Name: "group_filter",
|
|
Context: &structpb.Struct{
|
|
Fields: map[string]*structpb.Value{
|
|
"group_resource": structpb.NewStringValue(
|
|
resource.GetGroup() + "/" + resource.GetResource(),
|
|
),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
func toZanzanaSubject(kind string, name string) (string, error) {
|
|
if name == "" {
|
|
return "", errEmptyName
|
|
}
|
|
iamKind := iamv0.ResourcePermissionSpecPermissionKind(kind)
|
|
switch iamKind {
|
|
case iamv0.ResourcePermissionSpecPermissionKindUser:
|
|
return zanzana.NewTupleEntry(zanzana.TypeUser, name, ""), nil
|
|
case iamv0.ResourcePermissionSpecPermissionKindServiceAccount:
|
|
return zanzana.NewTupleEntry(zanzana.TypeServiceAccount, name, ""), nil
|
|
case iamv0.ResourcePermissionSpecPermissionKindTeam:
|
|
return zanzana.NewTupleEntry(zanzana.TypeTeam, name, ""), nil
|
|
case iamv0.ResourcePermissionSpecPermissionKindBasicRole:
|
|
basicRole := zanzana.TranslateBasicRole(name)
|
|
if basicRole == "" {
|
|
return "", fmt.Errorf("%w: %s", errInvalidBasicRole, name)
|
|
}
|
|
|
|
// e.g role:basic_viewer#assignee
|
|
return zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, zanzana.RelationAssignee), nil
|
|
}
|
|
|
|
// should not happen since we are after create
|
|
// validation webhook should have caught invalid kinds
|
|
return "", errUnknownKind
|
|
}
|