diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 8e22f8565e..b9215fe7e7 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -4,9 +4,8 @@ import ( "context" "fmt" + "github.com/kyverno/kyverno/pkg/auth/checker" kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" - authorizationv1 "k8s.io/api/authorization/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" ) @@ -34,7 +33,7 @@ type canIOptions struct { subresource string user string discovery Discovery - sarClient authorizationv1client.SubjectAccessReviewInterface + checker checker.AuthChecker } // NewCanI returns a new instance of operation access controller evaluator @@ -46,7 +45,7 @@ func NewCanI(discovery Discovery, sarClient authorizationv1client.SubjectAccessR subresource: subresource, user: user, discovery: discovery, - sarClient: sarClient, + checker: checker.NewSubjectChecker(sarClient, user, nil), } } @@ -68,37 +67,18 @@ func (o *canIOptions) RunAccessCheck(ctx context.Context) (bool, error) { if err != nil { return false, fmt.Errorf("failed to get GVR for kind %s", o.kind) } - if gvr.Empty() { // cannot find GVR return false, fmt.Errorf("failed to get the Group Version Resource for kind %s", o.kind) } - - sar := &authorizationv1.SubjectAccessReview{ - Spec: authorizationv1.SubjectAccessReviewSpec{ - ResourceAttributes: &authorizationv1.ResourceAttributes{ - Namespace: o.namespace, - Verb: o.verb, - Group: gvr.Group, - Resource: gvr.Resource, - Subresource: o.subresource, - }, - User: o.user, - }, - } - - logger := logger.WithValues("kind", sar.Kind, "namespace", sar.Namespace, "name", sar.Name, "gvr", gvr.String()) - resp, err := o.sarClient.Create(ctx, sar, metav1.CreateOptions{}) + logger := logger.WithValues("kind", kind, "namespace", o.namespace, "gvr", gvr.String(), "verb", o.verb) + result, err := o.checker.Check(ctx, gvr.Group, gvr.Version, gvr.Resource, o.subresource, o.namespace, o.verb) if err != nil { - logger.Error(err, "failed to create resource") + logger.Error(err, "failed to check permissions") return false, err } - - if !resp.Status.Allowed { - reason := resp.Status.Reason - evaluationError := resp.Status.EvaluationError - logger.Info("disallowed operation", "reason", reason, "evaluationError", evaluationError) + if !result.Allowed { + logger.Info("disallowed operation", "reason", result.Reason, "evaluationError", result.EvaluationError) } - - return resp.Status.Allowed, nil + return result.Allowed, nil } diff --git a/pkg/auth/checker/auth.go b/pkg/auth/checker/auth.go new file mode 100644 index 0000000000..cc7e5dfed0 --- /dev/null +++ b/pkg/auth/checker/auth.go @@ -0,0 +1,34 @@ +package checker + +import ( + "context" + + authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" +) + +// AuthResult contains authorization check result +type AuthResult struct { + Allowed bool + Reason string + EvaluationError string +} + +// AuthChecker provides utility to check authorization +type AuthChecker interface { + // Check checks if the caller can perform an operation + Check(ctx context.Context, group, version, resource, subresource, namespace, verb string) (*AuthResult, error) +} + +func NewSelfChecker(client authorizationv1client.SelfSubjectAccessReviewInterface) AuthChecker { + return self{ + client: client, + } +} + +func NewSubjectChecker(client authorizationv1client.SubjectAccessReviewInterface, user string, groups []string) AuthChecker { + return subject{ + client: client, + user: user, + groups: groups, + } +} diff --git a/pkg/auth/checker/helpers.go b/pkg/auth/checker/helpers.go new file mode 100644 index 0000000000..cbc0150c01 --- /dev/null +++ b/pkg/auth/checker/helpers.go @@ -0,0 +1,18 @@ +package checker + +import ( + "context" +) + +func Check(ctx context.Context, checker AuthChecker, group, version, resource, subresource, namespace string, verbs ...string) (bool, error) { + for _, verb := range verbs { + result, err := checker.Check(ctx, group, version, resource, subresource, namespace, verb) + if err != nil { + return false, err + } + if !result.Allowed { + return false, nil + } + } + return true, nil +} diff --git a/pkg/auth/checker/self.go b/pkg/auth/checker/self.go new file mode 100644 index 0000000000..98d541b666 --- /dev/null +++ b/pkg/auth/checker/self.go @@ -0,0 +1,37 @@ +package checker + +import ( + "context" + + authorizationv1 "k8s.io/api/authorization/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" +) + +type self struct { + client authorizationv1client.SelfSubjectAccessReviewInterface +} + +func (c self) Check(ctx context.Context, group, version, resource, subresource, namespace, verb string) (*AuthResult, error) { + review := &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Group: group, + Version: version, + Resource: resource, + Subresource: subresource, + Namespace: namespace, + Verb: verb, + }, + }, + } + resp, err := c.client.Create(ctx, review, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return &AuthResult{ + Allowed: resp.Status.Allowed, + Reason: resp.Status.Reason, + EvaluationError: resp.Status.EvaluationError, + }, nil +} diff --git a/pkg/auth/checker/subject.go b/pkg/auth/checker/subject.go new file mode 100644 index 0000000000..6ee15bf663 --- /dev/null +++ b/pkg/auth/checker/subject.go @@ -0,0 +1,41 @@ +package checker + +import ( + "context" + + authorizationv1 "k8s.io/api/authorization/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" +) + +type subject struct { + client authorizationv1client.SubjectAccessReviewInterface + user string + groups []string +} + +func (c subject) Check(ctx context.Context, group, version, resource, subresource, namespace, verb string) (*AuthResult, error) { + review := &authorizationv1.SubjectAccessReview{ + Spec: authorizationv1.SubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Group: group, + Version: version, + Resource: resource, + Subresource: subresource, + Namespace: namespace, + Verb: verb, + }, + User: c.user, + Groups: c.groups, + }, + } + resp, err := c.client.Create(ctx, review, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return &AuthResult{ + Allowed: resp.Status.Allowed, + Reason: resp.Status.Reason, + EvaluationError: resp.Status.EvaluationError, + }, nil +}