257 lines
8.8 KiB
Go
257 lines
8.8 KiB
Go
package inline
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/trace"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
"github.com/grafana/authlib/authn"
|
|
authlib "github.com/grafana/authlib/types"
|
|
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
|
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
|
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
|
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
|
)
|
|
|
|
type LocalInlineSecureValueService struct {
|
|
tracer trace.Tracer
|
|
secureValueService contracts.SecureValueService
|
|
accessChecker authlib.AccessChecker
|
|
}
|
|
|
|
var _ contracts.InlineSecureValueSupport = &LocalInlineSecureValueService{}
|
|
|
|
func NewLocalInlineSecureValueService(
|
|
tracer trace.Tracer,
|
|
secureValueService contracts.SecureValueService,
|
|
accessClient authlib.AccessClient,
|
|
) contracts.InlineSecureValueSupport {
|
|
return &LocalInlineSecureValueService{
|
|
tracer: tracer,
|
|
secureValueService: secureValueService,
|
|
accessChecker: accessClient,
|
|
}
|
|
}
|
|
|
|
func (s *LocalInlineSecureValueService) CanReference(ctx context.Context, owner common.ObjectReference, names ...string) error {
|
|
ctx, span := s.tracer.Start(ctx, "InlineSecureValueService.CanReference", trace.WithAttributes(
|
|
attribute.String("owner.namespace", owner.Namespace),
|
|
attribute.String("owner.apiGroup", owner.APIGroup),
|
|
attribute.String("owner.apiVersion", owner.APIVersion),
|
|
attribute.String("owner.kind", owner.Kind),
|
|
attribute.String("owner.name", owner.Name),
|
|
attribute.StringSlice("secureValueNames", names),
|
|
))
|
|
defer span.End()
|
|
|
|
authInfo, ok := authlib.AuthInfoFrom(ctx)
|
|
if !ok {
|
|
return fmt.Errorf("missing auth info in context")
|
|
}
|
|
|
|
if owner.Namespace == "" || !authlib.NamespaceMatches(authInfo.GetNamespace(), owner.Namespace) {
|
|
return fmt.Errorf("owner namespace %s does not match auth info namespace %s", owner.Namespace, authInfo.GetNamespace())
|
|
}
|
|
|
|
if owner.APIGroup == "" || owner.APIVersion == "" || owner.Kind == "" || owner.Name == "" {
|
|
return fmt.Errorf("owner reference must have a valid API group, API version, kind and name")
|
|
}
|
|
|
|
if len(names) == 0 {
|
|
return fmt.Errorf("no inline secure values provided")
|
|
}
|
|
|
|
for _, name := range names {
|
|
if name == "" {
|
|
return fmt.Errorf("empty secure value name")
|
|
}
|
|
|
|
owned, err := s.isSecureValueOwnedByResource(ctx, owner, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !owned {
|
|
if err := s.canIdentityReadSecureValue(ctx, xkube.Namespace(owner.Namespace), name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *LocalInlineSecureValueService) isSecureValueOwnedByResource(ctx context.Context, owner common.ObjectReference, name string) (bool, error) {
|
|
sv, err := s.secureValueService.Read(ctx, xkube.Namespace(owner.Namespace), name)
|
|
if err != nil {
|
|
if errors.Is(err, contracts.ErrSecureValueNotFound) {
|
|
return false, err
|
|
}
|
|
|
|
return false, fmt.Errorf("error reading secure value %s: %w", name, err)
|
|
}
|
|
|
|
secureValueOwners := sv.GetOwnerReferences()
|
|
if len(secureValueOwners) > 1 {
|
|
return false, fmt.Errorf("bug found: secure value %s with multiple owners, expected only one", name)
|
|
}
|
|
|
|
if len(secureValueOwners) == 1 {
|
|
actualOwner := secureValueOwners[0]
|
|
|
|
gv, err := schema.ParseGroupVersion(actualOwner.APIVersion)
|
|
if err != nil {
|
|
return false, fmt.Errorf("bug found: secure value %s should have valid group version here: %w", name, err)
|
|
}
|
|
if gv.Group == "" {
|
|
return false, fmt.Errorf("bug found: secure value %s should have a non-empty group in the owner reference", name)
|
|
}
|
|
|
|
sameOwner := owner.APIGroup == gv.Group && owner.Kind == actualOwner.Kind && owner.Name == actualOwner.Name
|
|
if sameOwner {
|
|
return true, nil // The secure value is owned by the same owner reference, pass!
|
|
}
|
|
|
|
return false, fmt.Errorf("secure value %s is not owned by %s/%s/%s/%s", name, owner.APIGroup, owner.APIVersion, owner.Kind, owner.Name)
|
|
}
|
|
|
|
// not owned
|
|
return false, nil
|
|
}
|
|
|
|
func (s *LocalInlineSecureValueService) canIdentityReadSecureValue(ctx context.Context, namespace xkube.Namespace, name string) error {
|
|
authInfo, ok := authlib.AuthInfoFrom(ctx)
|
|
if !ok {
|
|
return fmt.Errorf("missing auth info in context")
|
|
}
|
|
|
|
// If the secure value is shared, we always need a user/svc account in the context.
|
|
if authInfo.GetIdentityType() != authlib.TypeUser && authInfo.GetIdentityType() != authlib.TypeServiceAccount {
|
|
return fmt.Errorf("identity type %s not allowed, expected either %s or %s", authInfo.GetIdentityType(), authlib.TypeUser, authlib.TypeServiceAccount)
|
|
}
|
|
|
|
resp, err := s.accessChecker.Check(ctx, authInfo, authlib.CheckRequest{
|
|
Verb: utils.VerbGet,
|
|
Group: secretv1beta1.APIGroup,
|
|
Resource: secretv1beta1.SecureValuesResourceInfo.GroupResource().Resource,
|
|
Namespace: namespace.String(),
|
|
Name: name,
|
|
}, "")
|
|
if err != nil {
|
|
return fmt.Errorf("checking access for secure value %s: %w", name, err)
|
|
}
|
|
|
|
if !resp.Allowed {
|
|
return fmt.Errorf("identity is not allowed to reference secure value %s", name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *LocalInlineSecureValueService) verifyOwnerAndAuth(ctx context.Context, owner common.ObjectReference) (authlib.AuthInfo, error) {
|
|
// Any valid identity can create inline secure values
|
|
authInfo, ok := authlib.AuthInfoFrom(ctx)
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing auth info in context")
|
|
}
|
|
|
|
// Make sure the owner matches the identity when it is not global
|
|
if owner.Namespace == "" || !authlib.NamespaceMatches(authInfo.GetNamespace(), owner.Namespace) {
|
|
return nil, fmt.Errorf("owner namespace %s does not match auth info namespace %s", owner.Namespace, authInfo.GetNamespace())
|
|
}
|
|
|
|
if owner.Namespace == "" || owner.APIGroup == "" || owner.APIVersion == "" || owner.Kind == "" || owner.Name == "" {
|
|
return nil, fmt.Errorf("owner reference must have a valid API group, API version, kind, namespace and name")
|
|
}
|
|
|
|
return authInfo, nil
|
|
}
|
|
|
|
func (s *LocalInlineSecureValueService) CreateInline(ctx context.Context, owner common.ObjectReference, value common.RawSecureValue) (string, error) {
|
|
ctx, span := s.tracer.Start(ctx, "InlineSecureValueService.CreateInline", trace.WithAttributes(
|
|
attribute.String("owner.namespace", owner.Namespace),
|
|
attribute.String("owner.apiGroup", owner.APIGroup),
|
|
attribute.String("owner.apiVersion", owner.APIVersion),
|
|
attribute.String("owner.kind", owner.Kind),
|
|
attribute.String("owner.name", owner.Name),
|
|
))
|
|
defer span.End()
|
|
|
|
authInfo, err := s.verifyOwnerAndAuth(ctx, owner)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if value.IsZero() {
|
|
return "", fmt.Errorf("trying to create an inline secure value with empty value")
|
|
}
|
|
|
|
// TODO(2025-07-31): when we migrate to using the common type, we don't need this conversion.
|
|
secret := secretv1beta1.ExposedSecureValue(value)
|
|
|
|
// The owner group can always decrypt
|
|
decrypters := []string{owner.APIGroup}
|
|
|
|
serviceIdentity, ok := authInfo.GetExtra()[authn.ServiceIdentityKey]
|
|
if ok && len(serviceIdentity) > 0 && serviceIdentity[0] != owner.APIGroup {
|
|
decrypters = append(decrypters, serviceIdentity[0])
|
|
}
|
|
|
|
obj := &secretv1beta1.SecureValue{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "inline-",
|
|
Namespace: owner.Namespace,
|
|
OwnerReferences: []metav1.OwnerReference{owner.ToOwnerReference()},
|
|
},
|
|
Spec: secretv1beta1.SecureValueSpec{
|
|
Description: fmt.Sprintf("Inline secure value for %s/%s in %s/%s", owner.Kind, owner.Name, owner.APIGroup, owner.APIVersion),
|
|
Value: &secret,
|
|
Decrypters: decrypters,
|
|
},
|
|
}
|
|
|
|
createdSv, err := s.secureValueService.Create(ctx, obj, authInfo.GetUID())
|
|
if err != nil {
|
|
return "", fmt.Errorf("error creating secure value for owner %v: %w", owner, err)
|
|
}
|
|
|
|
return createdSv.GetName(), nil
|
|
}
|
|
|
|
func (s *LocalInlineSecureValueService) DeleteWhenOwnedByResource(ctx context.Context, owner common.ObjectReference, names ...string) error {
|
|
ctx, span := s.tracer.Start(ctx, "InlineSecureValueService.DeleteWhenOwnedByResource", trace.WithAttributes(
|
|
attribute.String("owner.namespace", owner.Namespace),
|
|
attribute.String("owner.apiGroup", owner.APIGroup),
|
|
attribute.String("owner.apiVersion", owner.APIVersion),
|
|
attribute.String("owner.kind", owner.Kind),
|
|
attribute.String("owner.name", owner.Name),
|
|
attribute.StringSlice("secureValueNames", names),
|
|
))
|
|
defer span.End()
|
|
|
|
if _, err := s.verifyOwnerAndAuth(ctx, owner); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, name := range names {
|
|
owned, err := s.isSecureValueOwnedByResource(ctx, owner, name)
|
|
if err != nil {
|
|
return fmt.Errorf("error checking if secure value %s is owned by %v: %w", name, owner, err)
|
|
}
|
|
|
|
if owned {
|
|
if _, err := s.secureValueService.Delete(ctx, xkube.Namespace(owner.Namespace), name); err != nil {
|
|
return fmt.Errorf("error deleting secure value %s for owner %v: %w", name, owner, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|