dfae5e5b4d
* Decrypt: Add namespace matches to authorizer * SecureValueClient: Add namespace matches when auth checking
316 lines
9.6 KiB
Go
316 lines
9.6 KiB
Go
package secret
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
claims "github.com/grafana/authlib/types"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
"k8s.io/apiserver/pkg/admission"
|
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
"k8s.io/client-go/dynamic"
|
|
|
|
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
|
"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"
|
|
authsvc "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
|
|
)
|
|
|
|
var (
|
|
ErrSecureValueNotFound = contracts.ErrSecureValueNotFound
|
|
ErrSecureValueAlreadyExists = contracts.ErrSecureValueAlreadyExists
|
|
)
|
|
|
|
// SecureValueClient is a CRUD client for the secure value API.
|
|
type SecureValueClient = contracts.SecureValueClient
|
|
|
|
type secureValueClient struct {
|
|
namespace string
|
|
service contracts.SecureValueService
|
|
validator contracts.SecureValueValidator
|
|
access authorizer.Authorizer
|
|
}
|
|
|
|
var _ SecureValueClient = &secureValueClient{}
|
|
|
|
func ProvideSecureValueClient(service contracts.SecureValueService, validator contracts.SecureValueValidator, access claims.AccessClient) SecureValueClient {
|
|
return &secureValueClient{
|
|
service: service,
|
|
validator: validator,
|
|
access: authsvc.NewResourceAuthorizer(access),
|
|
}
|
|
}
|
|
|
|
// Client returns a resource interface that is scoped to a specific namespace.
|
|
func (c *secureValueClient) Client(ctx context.Context, namespace string) (dynamic.ResourceInterface, error) {
|
|
return c.Namespace(namespace), nil
|
|
}
|
|
|
|
// Namespace returns a resource interface that is scoped to a specific namespace.
|
|
func (c *secureValueClient) Namespace(ns string) dynamic.ResourceInterface {
|
|
info, err := claims.ParseNamespace(ns)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(info.Value) == 0 {
|
|
panic("namespace is required")
|
|
}
|
|
|
|
ret := *c
|
|
ret.namespace = ns
|
|
return &ret
|
|
}
|
|
|
|
// Create a new secure value. Options and subresources are not supported and ignored.
|
|
func (c *secureValueClient) Create(ctx context.Context, obj *unstructured.Unstructured, _ metav1.CreateOptions, _ ...string) (*unstructured.Unstructured, error) {
|
|
if len(c.namespace) == 0 {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
|
|
if err := c.checkAccess(ctx, obj.GetName(), utils.VerbCreate); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sv, err := fromUnstructured(obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if sv.Namespace != c.namespace {
|
|
return nil, fmt.Errorf("namespace mismatch")
|
|
}
|
|
if errs := c.validator.Validate(sv, nil, admission.Create); len(errs) > 0 {
|
|
return nil, fmt.Errorf("invalid secure value: %w", errs.ToAggregate())
|
|
}
|
|
|
|
user, ok := claims.AuthInfoFrom(ctx)
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing auth info in context")
|
|
}
|
|
|
|
createdSv, err := c.service.Create(ctx, sv, user.GetUID())
|
|
if err != nil {
|
|
return nil, c.mapError(err, sv.Name)
|
|
}
|
|
|
|
return toUnstructured(createdSv)
|
|
}
|
|
|
|
// Get a secure value by name. Options and subresources are not supported and ignored.
|
|
func (c *secureValueClient) Get(ctx context.Context, name string, _ metav1.GetOptions, _ ...string) (*unstructured.Unstructured, error) {
|
|
if len(c.namespace) == 0 {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
if len(name) == 0 {
|
|
return nil, fmt.Errorf("name is required")
|
|
}
|
|
|
|
if err := c.checkAccess(ctx, name, utils.VerbGet); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sv, err := c.service.Read(ctx, xkube.Namespace(c.namespace), name)
|
|
if err != nil {
|
|
return nil, c.mapError(err, name)
|
|
}
|
|
|
|
return toUnstructured(sv)
|
|
}
|
|
|
|
// Update a secure value. Options and subresources are not supported and ignored.
|
|
func (c *secureValueClient) Update(ctx context.Context, obj *unstructured.Unstructured, _ metav1.UpdateOptions, _ ...string) (*unstructured.Unstructured, error) {
|
|
if len(c.namespace) == 0 {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
|
|
if err := c.checkAccess(ctx, obj.GetName(), utils.VerbUpdate); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
oldUnstructured, err := c.Get(ctx, obj.GetName(), metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
oldSv, err := fromUnstructured(oldUnstructured)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sv, err := fromUnstructured(obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if sv.Namespace != c.namespace {
|
|
return nil, fmt.Errorf("namespace mismatch")
|
|
}
|
|
if errs := c.validator.Validate(sv, oldSv, admission.Update); len(errs) > 0 {
|
|
return nil, fmt.Errorf("invalid secure value: %w", errs.ToAggregate())
|
|
}
|
|
|
|
user, ok := claims.AuthInfoFrom(ctx)
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing auth info in context")
|
|
}
|
|
|
|
updatedSv, _, err := c.service.Update(ctx, sv, user.GetUID())
|
|
if err != nil {
|
|
return nil, c.mapError(err, sv.Name)
|
|
}
|
|
|
|
return toUnstructured(updatedSv)
|
|
}
|
|
|
|
// Delete a secure value by name. Options and subresources are not supported and ignored.
|
|
func (c *secureValueClient) Delete(ctx context.Context, name string, _ metav1.DeleteOptions, _ ...string) error {
|
|
if len(c.namespace) == 0 {
|
|
return fmt.Errorf("namespace is required")
|
|
}
|
|
if len(name) == 0 {
|
|
return fmt.Errorf("name is required")
|
|
}
|
|
|
|
if err := c.checkAccess(ctx, name, utils.VerbDelete); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err := c.service.Delete(ctx, xkube.Namespace(c.namespace), name)
|
|
return c.mapError(err, name)
|
|
}
|
|
|
|
// List all secure values in the namespace. Options and subresources are not supported and ignored.
|
|
func (c *secureValueClient) List(ctx context.Context, _ metav1.ListOptions) (*unstructured.UnstructuredList, error) {
|
|
if len(c.namespace) == 0 {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
|
|
if err := c.checkAccess(ctx, "", utils.VerbList); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list, err := c.service.List(ctx, xkube.Namespace(c.namespace))
|
|
if err != nil {
|
|
return nil, c.mapError(err, "")
|
|
}
|
|
|
|
items := make([]unstructured.Unstructured, 0, len(list.Items))
|
|
for _, sv := range list.Items {
|
|
u, err := toUnstructured(&sv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items = append(items, *u)
|
|
}
|
|
|
|
return &unstructured.UnstructuredList{
|
|
Items: items,
|
|
}, nil
|
|
}
|
|
|
|
// DeleteCollection is not supported and returns an error.
|
|
func (c *secureValueClient) DeleteCollection(_ context.Context, _ metav1.DeleteOptions, _ metav1.ListOptions) error {
|
|
return fmt.Errorf("deleteCollection is not supported")
|
|
}
|
|
|
|
// Watch is not supported and returns an error.
|
|
func (c *secureValueClient) Watch(_ context.Context, _ metav1.ListOptions) (watch.Interface, error) {
|
|
return nil, fmt.Errorf("watch is not supported")
|
|
}
|
|
|
|
// Patch is not supported and returns an error.
|
|
func (c *secureValueClient) Patch(_ context.Context, _ string, _ types.PatchType, _ []byte, _ metav1.PatchOptions, _ ...string) (*unstructured.Unstructured, error) {
|
|
return nil, fmt.Errorf("patch is not supported")
|
|
}
|
|
|
|
// Apply is not supported and returns an error.
|
|
func (c *secureValueClient) Apply(_ context.Context, _ string, _ *unstructured.Unstructured, _ metav1.ApplyOptions, _ ...string) (*unstructured.Unstructured, error) {
|
|
return nil, fmt.Errorf("apply is not supported")
|
|
}
|
|
|
|
// UpdateStatus is not supported and returns an error.
|
|
func (c *secureValueClient) UpdateStatus(_ context.Context, _ *unstructured.Unstructured, _ metav1.UpdateOptions) (*unstructured.Unstructured, error) {
|
|
return nil, fmt.Errorf("updateStatus is not supported")
|
|
}
|
|
|
|
// ApplyStatus is not supported and returns an error.
|
|
func (c *secureValueClient) ApplyStatus(_ context.Context, _ string, _ *unstructured.Unstructured, _ metav1.ApplyOptions) (*unstructured.Unstructured, error) {
|
|
return nil, fmt.Errorf("applyStatus is not supported")
|
|
}
|
|
|
|
// Maps an error from the domain to a K8s API Status error.
|
|
func (c *secureValueClient) mapError(err error, name string) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
gr := secretv1beta1.SecureValuesResourceInfo.GroupResource()
|
|
|
|
switch {
|
|
case errors.Is(err, ErrSecureValueNotFound):
|
|
return apierrors.NewNotFound(gr, name)
|
|
case errors.Is(err, ErrSecureValueAlreadyExists):
|
|
return apierrors.NewAlreadyExists(gr, name)
|
|
}
|
|
|
|
return apierrors.NewInternalError(err)
|
|
}
|
|
|
|
func (c *secureValueClient) checkAccess(ctx context.Context, name, verb string) error {
|
|
authInfo, ok := claims.AuthInfoFrom(ctx)
|
|
if !ok {
|
|
return apierrors.NewUnauthorized("missing auth info in context")
|
|
}
|
|
|
|
gr := secretv1beta1.SecureValuesResourceInfo.GroupResource()
|
|
|
|
if !claims.NamespaceMatches(authInfo.GetNamespace(), c.namespace) {
|
|
return apierrors.NewForbidden(gr, name, fmt.Errorf("namespace mismatch: %s != %s", authInfo.GetNamespace(), c.namespace))
|
|
}
|
|
|
|
decision, reason, err := c.access.Authorize(ctx, authorizer.AttributesRecord{
|
|
Verb: verb,
|
|
Namespace: c.namespace,
|
|
APIGroup: secretv1beta1.APIGroup,
|
|
APIVersion: secretv1beta1.APIVersion,
|
|
Resource: gr.Resource,
|
|
Subresource: "",
|
|
Name: name,
|
|
ResourceRequest: true,
|
|
})
|
|
|
|
if err != nil {
|
|
return apierrors.NewForbidden(gr, name, fmt.Errorf("failed to check access: %w", err))
|
|
}
|
|
|
|
if decision != authorizer.DecisionAllow {
|
|
return apierrors.NewForbidden(gr, name, fmt.Errorf("no access to %s: %s", verb, reason))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func toUnstructured(sv *secretv1beta1.SecureValue) (*unstructured.Unstructured, error) {
|
|
unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(sv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &unstructured.Unstructured{Object: unstructuredObj}, nil
|
|
}
|
|
|
|
func fromUnstructured(u *unstructured.Unstructured) (*secretv1beta1.SecureValue, error) {
|
|
sv := new(secretv1beta1.SecureValue)
|
|
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, sv); err != nil {
|
|
return nil, err
|
|
}
|
|
return sv, nil
|
|
}
|