Extend Teleport RBAC to support Kubernetes Verbs (#27096)

* Extend Teleport RBAC to suport Kubernetes Verbs

This PR extends Teleport per-Resource RBAC to support Kubernetes verbs
restriction. With this change it's possible to restrict certain actions
allowed by the underlying `kubernetes_users` and `kubernetes_resources`.

Supported verbs:
- `get`
- `create`
- `update`
- `patch`
- `delete`
- `list`
- `watch`
- `deletecollection`

Fixes #27095

* address timr's comments

* assign wildcard to verbs for role <7

* address marco's reviews
This commit is contained in:
Tiago Silva
2023-07-06 15:39:11 +00:00
committed by GitHub
parent ed3278817e
commit e06fc88e98
23 changed files with 2060 additions and 1587 deletions
@@ -2718,6 +2718,8 @@ message KubernetesResource {
// Name is the resource name.
// It supports wildcards.
string Name = 3 [(gogoproto.jsontag) = "name,omitempty"];
// Verbs are the allowed Kubernetes verbs for the following resource.
repeated string Verbs = 4 [(gogoproto.jsontag) = "verbs,omitempty"];
}
// SessionRequirePolicy a requirement policy that needs to be fulfilled to grant access.
+32
View File
@@ -821,6 +821,38 @@ var KubernetesResourcesKinds = []string{
KindKubeIngress,
}
const (
// KubeVerbGet is the Kubernetes verb for "get".
KubeVerbGet = "get"
// KubeVerbCreate is the Kubernetes verb for "create".
KubeVerbCreate = "create"
// KubeVerbUpdate is the Kubernetes verb for "update".
KubeVerbUpdate = "update"
// KubeVerbPatch is the Kubernetes verb for "patch".
KubeVerbPatch = "patch"
// KubeVerbDelete is the Kubernetes verb for "delete".
KubeVerbDelete = "delete"
// KubeVerbList is the Kubernetes verb for "list".
KubeVerbList = "list"
// KubeVerbWatch is the Kubernetes verb for "watch".
KubeVerbWatch = "watch"
// KubeVerbDeleteCollection is the Kubernetes verb for "deletecollection".
KubeVerbDeleteCollection = "deletecollection"
)
// KubernetesVerbs lists the supported Kubernetes verbs.
var KubernetesVerbs = []string{
Wildcard,
KubeVerbGet,
KubeVerbCreate,
KubeVerbUpdate,
KubeVerbPatch,
KubeVerbDelete,
KubeVerbList,
KubeVerbWatch,
KubeVerbDeleteCollection,
}
// KubernetesClusterWideResourceKinds is the list of supported Kubernetes cluster resource kinds
// that are not namespaced.
var KubernetesClusterWideResourceKinds = []string{
+31 -2
View File
@@ -426,7 +426,7 @@ func (r *RoleV6) convertKubernetesResourcesBetweenRoleVersions(resources []Kuber
// This check ignores the Kind field because `validateKubeResources` ensures
// that for older roles, the Kind field can only be pod.
case len(resources) == 1 && resources[0].Name == Wildcard && resources[0].Namespace == Wildcard:
return []KubernetesResource{{Kind: Wildcard, Name: Wildcard, Namespace: Wildcard}}
return []KubernetesResource{{Kind: Wildcard, Name: Wildcard, Namespace: Wildcard, Verbs: []string{Wildcard}}}
default:
for _, resource := range KubernetesResourcesKinds {
// Ignore Pod resources for older roles because Pods were already supported
@@ -434,7 +434,7 @@ func (r *RoleV6) convertKubernetesResourcesBetweenRoleVersions(resources []Kuber
if resource == KindKubePod {
continue
}
resources = append(resources, KubernetesResource{Kind: resource, Name: Wildcard, Namespace: Wildcard})
resources = append(resources, KubernetesResource{Kind: resource, Name: Wildcard, Namespace: Wildcard, Verbs: []string{Wildcard}})
}
return resources
}
@@ -997,11 +997,13 @@ func (r *RoleV6) CheckAndSetDefaults() error {
}
}
setDefaultKubernetesVerbs(&r.Spec)
if err := validateRoleSpecKubeResources(r.Version, r.Spec); err != nil {
return trace.Wrap(err)
}
case V6:
setDefaultKubernetesVerbs(&r.Spec)
if err := validateRoleSpecKubeResources(r.Version, r.Spec); err != nil {
return trace.Wrap(err)
}
@@ -1013,6 +1015,7 @@ func (r *RoleV6) CheckAndSetDefaults() error {
Kind: Wildcard,
Namespace: Wildcard,
Name: Wildcard,
Verbs: []string{Wildcard},
},
}
}
@@ -1586,6 +1589,19 @@ func validateRoleSpecKubeResources(version string, spec RoleSpecV6) error {
return nil
}
// setDefaultKubernetesVerbs sets the default verbs for each KubernetesResource
// entry if none are specified. This is necessary for backwards compatibility
// with older versions of Role: V3, V4, V5, and v6.
func setDefaultKubernetesVerbs(spec *RoleSpecV6) {
for _, kubeResources := range [][]KubernetesResource{spec.Allow.KubernetesResources, spec.Deny.KubernetesResources} {
for i := range kubeResources {
if len(kubeResources[i].Verbs) == 0 {
kubeResources[i].Verbs = []string{Wildcard}
}
}
}
}
// validateKubeResources validates the following rules for each kubeResources entry:
// - Kind belongs to KubernetesResourcesKinds
// - Name is not empty
@@ -1595,6 +1611,16 @@ func validateKubeResources(roleVersion string, kubeResources []KubernetesResourc
if !slices.Contains(KubernetesResourcesKinds, kubeResource.Kind) && kubeResource.Kind != Wildcard {
return trace.BadParameter("KubernetesResource kind %q is invalid or unsupported; Supported: %v", kubeResource.Kind, append([]string{Wildcard}, KubernetesResourcesKinds...))
}
for _, verb := range kubeResource.Verbs {
if !slices.Contains(KubernetesVerbs, verb) && verb != Wildcard {
return trace.BadParameter("KubernetesResource verb %q is invalid or unsupported; Supported: %v", verb, KubernetesVerbs)
}
if verb == Wildcard && len(kubeResource.Verbs) > 1 {
return trace.BadParameter("KubernetesResource verb %q cannot be used with other verbs", verb)
}
}
// Only Pod resources are supported in role version <=V6.
// This is mandatory because we must append the other resources to the
// kubernetes resources.
@@ -1604,6 +1630,9 @@ func validateKubeResources(roleVersion string, kubeResources []KubernetesResourc
if kubeResource.Kind != KindKubePod {
return trace.BadParameter("KubernetesResource %q is not supported in role version %q. Upgrade the role version to %q", kubeResource.Kind, roleVersion, V7)
}
if len(kubeResource.Verbs) != 1 || kubeResource.Verbs[0] != Wildcard {
return trace.BadParameter("Role version %q only supports %q verb. Upgrade the role version to %q", roleVersion, Wildcard, V7)
}
}
if len(kubeResource.Namespace) == 0 && !slices.Contains(KubernetesClusterWideResourceKinds, kubeResource.Kind) {
+5
View File
@@ -242,6 +242,7 @@ func TestRole_GetKubeResources(t *testing.T) {
Kind: KindKubePod,
Namespace: "test",
Name: "test",
Verbs: []string{Wildcard},
},
},
appendV7KubeResources()...),
@@ -265,6 +266,7 @@ func TestRole_GetKubeResources(t *testing.T) {
Kind: Wildcard,
Namespace: Wildcard,
Name: Wildcard,
Verbs: []string{Wildcard},
},
},
},
@@ -287,6 +289,7 @@ func TestRole_GetKubeResources(t *testing.T) {
Kind: KindKubePod,
Namespace: "test",
Name: "test",
Verbs: []string{Wildcard},
},
},
appendV7KubeResources()...),
@@ -304,6 +307,7 @@ func TestRole_GetKubeResources(t *testing.T) {
Kind: Wildcard,
Namespace: Wildcard,
Name: Wildcard,
Verbs: []string{Wildcard},
},
},
},
@@ -354,6 +358,7 @@ func appendV7KubeResources() []KubernetesResource {
Kind: resource,
Namespace: Wildcard,
Name: Wildcard,
Verbs: []string{Wildcard},
},
)
}
+1439 -1389
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -199,7 +199,7 @@ func testExec(t *testing.T, suite *KubeSuite, pinnedIP string, clientError strin
},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -412,7 +412,7 @@ func testKubeDeny(t *testing.T, suite *KubeSuite) {
KubeUsers: kubeUsers,
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -470,7 +470,7 @@ func testKubePortForward(t *testing.T, suite *KubeSuite) {
},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -574,7 +574,7 @@ func testKubeTrustedClustersClientCert(t *testing.T, suite *KubeSuite) {
},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -620,7 +620,7 @@ func testKubeTrustedClustersClientCert(t *testing.T, suite *KubeSuite) {
},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -848,7 +848,7 @@ func testKubeTrustedClustersSNI(t *testing.T, suite *KubeSuite) {
},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -898,7 +898,7 @@ func testKubeTrustedClustersSNI(t *testing.T, suite *KubeSuite) {
KubeGroups: auxKubeGroups,
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -1152,7 +1152,7 @@ func runKubeDisconnectTest(t *testing.T, suite *KubeSuite, tc disconnectTestCase
},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -1245,7 +1245,7 @@ func testKubeTransportProtocol(t *testing.T, suite *KubeSuite) {
},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -1572,7 +1572,7 @@ func testKubeJoin(t *testing.T, suite *KubeSuite) {
},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -1829,7 +1829,7 @@ func testExecNoAuth(t *testing.T, suite *KubeSuite) {
},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -1848,7 +1848,7 @@ func testExecNoAuth(t *testing.T, suite *KubeSuite) {
},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
RequireSessionJoin: []*types.SessionRequirePolicy{
+4 -4
View File
@@ -355,7 +355,7 @@ func TestALPNSNIProxyKube(t *testing.T) {
KubeUsers: []string{k8User},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -464,7 +464,7 @@ func TestALPNSNIProxyKubeV2Leaf(t *testing.T) {
KubeUsers: []string{k8User},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -536,7 +536,7 @@ func TestKubeIPPinning(t *testing.T) {
KubeUsers: []string{k8User},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
@@ -1574,7 +1574,7 @@ func TestALPNSNIProxyGRPCSecure(t *testing.T) {
KubeUsers: []string{k8User},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard,
Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard},
},
},
},
+14 -48
View File
@@ -460,8 +460,8 @@ type authContext struct {
// kubeResource.Namespace is the resource namespace and kubeResource.Name
// is the resource name.
kubeResource *types.KubernetesResource
// httpMethod is the request HTTP Method.
httpMethod string
// requestVerb is the Kubernetes Verb.
requestVerb string
// kubeServers are the registered agents for the kubernetes cluster the request
// is targeted to.
kubeServers []types.KubeServer
@@ -573,7 +573,7 @@ func (f *Forwarder) authenticate(req *http.Request) (*authContext, error) {
// kubeResource is the Kubernetes Resource the request is targeted at.
// Currently only supports Pods and it includes the pod name and namespace.
kubeResource, apiResource := getResourceFromRequest(req.URL.Path)
kubeResource, apiResource := getResourceFromRequest(req)
authContext, err := f.setupContext(ctx, *userContext, req, isRemoteUser, apiResource, kubeResource)
if err != nil {
f.log.WithError(err).Warn("Unable to setup context.")
@@ -862,7 +862,7 @@ func (f *Forwarder) setupContext(
remoteAddr: utils.NetAddr{AddrNetwork: "tcp", Addr: req.RemoteAddr},
isRemote: isRemoteCluster,
},
httpMethod: req.Method,
requestVerb: apiResource.getVerb(req),
kubeServers: kubeServers,
apiResource: apiResource,
}, nil
@@ -1049,7 +1049,7 @@ func (f *Forwarder) authorize(ctx context.Context, actx *authContext) error {
if actx.kubeResource != nil {
notFoundMessage = kubeResourceDeniedAccessMsg(
actx.User.GetName(),
actx.httpMethod,
actx.requestVerb,
actx.kubeResource,
)
roleMatchers = services.RoleMatchers{
@@ -2572,7 +2572,7 @@ func (f *Forwarder) listResources(authCtx *authContext, w http.ResponseWriter, r
allowedResources, deniedResources := authCtx.Checker.GetKubeResources(authCtx.kubeCluster)
// isWatch identifies if the request is long-lived watch stream based on
// HTTP connection.
isWatch := req.URL.Query().Get("watch") == "true"
isWatch := isKubeWatchRequest(req, sess.authContext.apiResource)
if !isWatch {
// List pods and return immediately.
status, err = f.listResourcesList(req, w, sess, allowedResources, deniedResources)
@@ -2606,10 +2606,11 @@ func (f *Forwarder) listResourcesList(req *http.Request, w http.ResponseWriter,
if !ok {
return http.StatusBadRequest, trace.BadParameter("unknown resource kind %q", sess.apiResource.resourceKind)
}
verb := sess.requestVerb
// filterBuffer filters the response to exclude pods the user doesn't have access to.
// The filtered payload will be written into memBuffer again.
if err := filterBuffer(
newResourceFilterer(resourceKind, allowedResources, deniedResources, f.log),
newResourceFilterer(resourceKind, verb, allowedResources, deniedResources, f.log),
memBuffer,
); err != nil {
return memBuffer.Status(), trace.Wrap(err)
@@ -2637,11 +2638,13 @@ func (f *Forwarder) listResourcesWatcher(req *http.Request, w http.ResponseWrite
if !ok {
return http.StatusBadRequest, trace.BadParameter("unknown resource kind %q", sess.apiResource.resourceKind)
}
verb := sess.requestVerb
rw, err := responsewriters.NewWatcherResponseWriter(
w,
negotiator,
newResourceFilterer(
resourceKind,
verb,
allowedResources,
deniedResources,
f.log,
@@ -3082,27 +3085,8 @@ func parseDeleteCollectionBody(r io.Reader, decoder runtime.Decoder) (metav1.Del
// kubeResourceDeniedAccessMsg creates a Kubernetes API like forbidden response.
// Logic from:
// https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/errors.go#L51
func kubeResourceDeniedAccessMsg(user, method string, kubeResource *types.KubernetesResource) string {
func kubeResourceDeniedAccessMsg(user, verb string, kubeResource *types.KubernetesResource) string {
resource, apiGroup := getKubeResourceAndAPIGroupFromType(kubeResource.Kind)
// <resource> "<name>" is forbidden: User "<user>" cannot create resource "<resource>" in API group "<api_group>"
if kubeResource.Namespace == "" {
return fmt.Sprintf(
"%s %q is forbidden: User %q cannot %s resource %q in API group %q\n"+
"Ask your Teleport admin to ensure that your Teleport role includes access to the %s in %q field.\n"+
"Check by running: kubectl auth can-i %s %s/%s",
resource,
kubeResource.Name,
user,
getRequestVerb(method),
resource,
apiGroup,
kubeResource.Kind,
kubernetesResourcesKey,
getRequestVerb(method),
resource,
kubeResource.Name,
)
}
// <resource> "<pod_name>" is forbidden: User "<user>" cannot create resource "<resource>" in API group "" in the namespace "<namespace>"
return fmt.Sprintf(
"%s %q is forbidden: User %q cannot %s resource %q in API group %q in the namespace %q\n"+
@@ -3111,37 +3095,19 @@ func kubeResourceDeniedAccessMsg(user, method string, kubeResource *types.Kubern
resource,
kubeResource.Name,
user,
getRequestVerb(method),
verb,
resource,
apiGroup,
kubeResource.Namespace,
kubeResource.Kind,
kubernetesResourcesKey,
getRequestVerb(method),
verb,
resource,
kubeResource.Name,
kubeResource.Namespace,
)
}
// getRequestVerb converts the request method into a Kubernetes Verb.
func getRequestVerb(method string) string {
apiVerb := ""
switch method {
case http.MethodPost:
apiVerb = "create"
case http.MethodGet:
apiVerb = "get"
case http.MethodPut:
apiVerb = "update"
case http.MethodPatch:
apiVerb = "patch"
case http.MethodDelete:
apiVerb = "delete"
}
return apiVerb
}
// errorToKubeStatusReason returns an appropriate StatusReason based on the
// provided error type.
func errorToKubeStatusReason(err error, code int) metav1.StatusReason {
@@ -3220,7 +3186,7 @@ func deleteResources[T kubeObjectInterface](
params.authCtx.Checker.Traits(),
),
services.NewKubernetesResourceMatcher(
getKubeResource(kind, item),
getKubeResource(kind, types.KubeVerbDeleteCollection, item),
),
)
// no match was found, we ignore the request.
+55 -50
View File
@@ -41,7 +41,7 @@ import (
// - deniedResources: excluded if (namespace,name) matches an entry even if it matches
// the allowedResources's list.
// - allowedResources: excluded if (namespace,name) not match a single entry.
func newResourceFilterer(kind string, allowedResources, deniedResources []types.KubernetesResource, log logrus.FieldLogger) responsewriters.FilterWrapper {
func newResourceFilterer(kind, verb string, allowedResources, deniedResources []types.KubernetesResource, log logrus.FieldLogger) responsewriters.FilterWrapper {
// If the list of allowed resources contains a wildcard and no deniedResources, then we
// don't need to filter anything.
if containsWildcard(allowedResources) && len(deniedResources) == 0 {
@@ -63,6 +63,7 @@ func newResourceFilterer(kind string, allowedResources, deniedResources []types.
deniedResources: deniedResources,
log: log,
kind: kind,
verb: verb,
}, nil
}
}
@@ -104,6 +105,8 @@ type resourceFilterer struct {
log logrus.FieldLogger
// kind is the type of the resource.
kind string
// verb is the kube API verb based on HTTP verb.
verb string
}
// FilterBuffer receives a byte array, decodes the response into the appropriate
@@ -168,7 +171,7 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
// should be forwarded to the user.
return true, false, nil
case *corev1.Pod:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -177,12 +180,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *corev1.PodList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *corev1.Secret:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -191,12 +194,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *corev1.SecretList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *corev1.ConfigMap:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -205,12 +208,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *corev1.ConfigMapList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *corev1.Namespace:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -219,12 +222,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *corev1.NamespaceList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *corev1.Service:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -233,12 +236,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *corev1.ServiceList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *corev1.ServiceAccount:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -247,12 +250,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *corev1.ServiceAccountList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *corev1.Node:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -261,12 +264,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *corev1.NodeList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *corev1.PersistentVolume:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -275,12 +278,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *corev1.PersistentVolumeList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *corev1.PersistentVolumeClaim:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -289,13 +292,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *corev1.PersistentVolumeClaimList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *appsv1.Deployment:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -304,13 +307,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *appsv1.DeploymentList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *appsv1.ReplicaSet:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -319,12 +322,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *appsv1.ReplicaSetList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *appsv1.StatefulSet:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -333,13 +336,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *appsv1.StatefulSetList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *appsv1.DaemonSet:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -348,12 +351,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *appsv1.DaemonSetList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *authv1.ClusterRole:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -362,12 +365,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *authv1.ClusterRoleList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *authv1.Role:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -376,13 +379,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *authv1.RoleList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *authv1.ClusterRoleBinding:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -391,13 +394,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *authv1.ClusterRoleBindingList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *authv1.RoleBinding:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -406,13 +409,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *authv1.RoleBindingList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *batchv1.CronJob:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -421,13 +424,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *batchv1.CronJobList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *batchv1.Job:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -436,13 +439,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *batchv1.JobList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *certificatesv1.CertificateSigningRequest:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -451,12 +454,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *certificatesv1.CertificateSigningRequestList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
case *networkingv1.Ingress:
result, err := filterResource(d.kind, o, d.allowedResources, d.deniedResources)
result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources)
if err != nil && !trace.IsAccessDenied(err) {
d.log.WithError(err).Warn("Unable to compile role kubernetes_resources.")
}
@@ -465,7 +468,7 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList
case *networkingv1.IngressList:
o.Items = pointerArrayToArray(
filterResourceList(
d.kind,
d.kind, d.verb,
arrayToPointerArray(o.Items), d.allowedResources, d.deniedResources, d.log),
)
return len(o.Items) > 0, true, nil
@@ -525,10 +528,10 @@ func (d *resourceFilterer) encode(obj runtime.Object, w io.Writer) error {
}
// filterResourceList excludes resources the user should not have access to.
func filterResourceList[T kubeObjectInterface](kind string, originalList []T, allowed, denied []types.KubernetesResource, log logrus.FieldLogger) []T {
func filterResourceList[T kubeObjectInterface](kind, verb string, originalList []T, allowed, denied []types.KubernetesResource, log logrus.FieldLogger) []T {
filteredList := make([]T, 0, len(originalList))
for _, resource := range originalList {
if result, err := filterResource(kind, resource, allowed, denied); err == nil && result {
if result, err := filterResource(kind, verb, resource, allowed, denied); err == nil && result {
filteredList = append(filteredList, resource)
} else if err != nil {
log.WithError(err).Warnf("Unable to compile role kubernetes_resources.")
@@ -546,19 +549,20 @@ type kubeObjectInterface interface {
}
// filterResource validates if the user should access the current resource.
func filterResource(kind string, resource kubeObjectInterface, allowed, denied []types.KubernetesResource) (bool, error) {
func filterResource(kind, verb string, resource kubeObjectInterface, allowed, denied []types.KubernetesResource) (bool, error) {
result, err := matchKubernetesResource(
getKubeResource(kind, resource),
getKubeResource(kind, verb, resource),
allowed, denied,
)
return result, trace.Wrap(err)
}
func getKubeResource(kind string, obj kubeObjectInterface) types.KubernetesResource {
func getKubeResource(kind, verb string, obj kubeObjectInterface) types.KubernetesResource {
return types.KubernetesResource{
Kind: kind,
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
Verbs: []string{verb},
}
}
@@ -571,7 +575,7 @@ func (d *resourceFilterer) filterMetaV1Table(table *metav1.Table, allowedResourc
if err := d.decodePartialObjectMetadata(row); err != nil {
return nil, trace.Wrap(err)
}
resource, err := getKubeResourcePartialMetadataObject(d.kind, row.Object.Object)
resource, err := getKubeResourcePartialMetadataObject(d.kind, d.verb, row.Object.Object)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -587,13 +591,14 @@ func (d *resourceFilterer) filterMetaV1Table(table *metav1.Table, allowedResourc
// getKubeResourcePartialMetadataObject checks if obj is of type *metav1.PartialObjectMetadata
// otherwise returns an error.
func getKubeResourcePartialMetadataObject(kind string, obj runtime.Object) (types.KubernetesResource, error) {
func getKubeResourcePartialMetadataObject(kind, verb string, obj runtime.Object) (types.KubernetesResource, error) {
switch o := obj.(type) {
case *metav1.PartialObjectMetadata:
return types.KubernetesResource{
Namespace: o.Namespace,
Name: o.Name,
Kind: kind,
Verbs: []string{verb},
}, nil
default:
return types.KubernetesResource{}, trace.BadParameter("expected *metav1.PartialObjectMetadata object, got %T", obj)
+3 -2
View File
@@ -132,6 +132,7 @@ func Test_filterBuffer(t *testing.T) {
Kind: r,
Namespace: "default",
Name: "*",
Verbs: []string{types.KubeVerbList},
},
}
t.Run(fmt.Sprintf("%s %s", r, tt.name), func(t *testing.T) {
@@ -149,7 +150,7 @@ func Test_filterBuffer(t *testing.T) {
buf, decompress := newMemoryResponseWriter(t, data.Bytes(), tt.args.contentEncoding)
err = filterBuffer(newResourceFilterer(r, allowedResources, nil, log), buf)
err = filterBuffer(newResourceFilterer(r, types.KubeVerbList, allowedResources, nil, log), buf)
require.NoError(t, err)
// Decompress the buffer to compare the result.
@@ -201,7 +202,7 @@ func Test_filterBuffer(t *testing.T) {
require.NoError(t, err)
}
resource, err := getKubeResourcePartialMetadataObject(r, row.Object.Object)
resource, err := getKubeResourcePartialMetadataObject(r, "list", row.Object.Object)
require.NoError(t, err)
resources = append(resources, resource.Namespace+"/"+resource.Name)
}
+61 -10
View File
@@ -49,11 +49,12 @@ import (
func TestListPodRBAC(t *testing.T) {
const (
usernameWithFullAccess = "full_user"
usernameWithNamespaceAccess = "default_user"
usernameWithLimitedAccess = "limited_user"
usernameWithDenyRule = "denied_user"
testPodName = "test"
usernameWithFullAccess = "full_user"
usernameWithNamespaceAccess = "default_user"
usernameWithLimitedAccess = "limited_user"
usernameWithDenyRule = "denied_user"
usernameWithoutListVerbAccess = "no_list_user"
testPodName = "test"
)
// kubeMock is a Kubernetes API mock for the session tests.
// Once a new session is created, this mock will write to
@@ -91,6 +92,7 @@ func TestListPodRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: types.Wildcard,
Namespace: types.Wildcard,
Verbs: []string{types.Wildcard},
},
})
},
@@ -113,6 +115,7 @@ func TestListPodRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: types.Wildcard,
Namespace: metav1.NamespaceDefault,
Verbs: []string{types.Wildcard},
},
})
},
@@ -136,6 +139,31 @@ func TestListPodRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: "nginx-*",
Namespace: metav1.NamespaceDefault,
Verbs: []string{types.Wildcard},
},
},
)
},
},
)
// create a moderator user with access to kubernetes
// (kubernetes_user and kubernetes_groups specified)
userWithoutListVerb, _ := testCtx.CreateUserAndRole(
testCtx.Context,
t,
usernameWithoutListVerbAccess,
RoleSpec{
Name: usernameWithoutListVerbAccess,
KubeUsers: roleKubeUsers,
KubeGroups: roleKubeGroups,
SetupRoleFunc: func(r types.Role) {
r.SetKubeResources(types.Allow,
[]types.KubernetesResource{
{
Kind: types.KindKubePod,
Name: "*",
Namespace: metav1.NamespaceDefault,
Verbs: []string{"get"},
},
},
)
@@ -160,6 +188,7 @@ func TestListPodRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: types.Wildcard,
Namespace: types.Wildcard,
Verbs: []string{types.Wildcard},
},
},
)
@@ -169,6 +198,7 @@ func TestListPodRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: types.Wildcard,
Namespace: metav1.NamespaceDefault,
Verbs: []string{types.Wildcard},
},
},
)
@@ -349,6 +379,17 @@ func TestListPodRBAC(t *testing.T) {
},
},
},
{
name: "list default namespace pods for user with limited access",
args: args{
user: userWithoutListVerb,
namespace: metav1.NamespaceDefault,
},
want: want{
listPodsResult: []string{},
},
},
}
getPodsFromPodList := func(items []corev1.Pod) []string {
pods := make([]string, 0, len(items))
@@ -441,6 +482,7 @@ func TestWatcherResponseWriter(t *testing.T) {
Kind: types.KindKubePod,
Namespace: "*",
Name: "*",
Verbs: []string{types.Wildcard},
},
},
},
@@ -454,6 +496,7 @@ func TestWatcherResponseWriter(t *testing.T) {
Kind: types.KindKubePod,
Namespace: defaultNamespace,
Name: "*",
Verbs: []string{types.Wildcard},
},
},
},
@@ -467,6 +510,7 @@ func TestWatcherResponseWriter(t *testing.T) {
Kind: types.KindKubePod,
Namespace: defaultNamespace,
Name: "*",
Verbs: []string{types.Wildcard},
},
},
denied: []types.KubernetesResource{
@@ -474,6 +518,7 @@ func TestWatcherResponseWriter(t *testing.T) {
Kind: types.KindKubePod,
Namespace: defaultNamespace,
Name: "otherPod",
Verbs: []string{types.Wildcard},
},
},
},
@@ -487,6 +532,7 @@ func TestWatcherResponseWriter(t *testing.T) {
Kind: types.KindKubePod,
Namespace: defaultNamespace,
Name: "rand*",
Verbs: []string{types.Wildcard},
},
},
},
@@ -506,7 +552,7 @@ func TestWatcherResponseWriter(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
userReader, userWriter := io.Pipe()
negotiator := newClientNegotiator()
filterWrapper := newResourceFilterer(types.KindKubePod, tt.args.allowed, tt.args.denied, log)
filterWrapper := newResourceFilterer(types.KindKubePod, types.KubeVerbWatch, tt.args.allowed, tt.args.denied, log)
// watcher parses the data written into itself and if the user is allowed to
// receive the update, it writes the event into target.
watcher, err := responsewriters.NewWatcherResponseWriter(newFakeResponseWriter(userWriter) /*target*/, negotiator, filterWrapper)
@@ -741,6 +787,7 @@ func TestDeletePodCollectionRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: types.Wildcard,
Namespace: types.Wildcard,
Verbs: []string{types.Wildcard},
},
})
},
@@ -763,6 +810,7 @@ func TestDeletePodCollectionRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: types.Wildcard,
Namespace: metav1.NamespaceDefault,
Verbs: []string{types.Wildcard},
},
})
},
@@ -786,6 +834,7 @@ func TestDeletePodCollectionRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: "nginx-*",
Namespace: metav1.NamespaceDefault,
Verbs: []string{types.Wildcard},
},
},
)
@@ -919,8 +968,9 @@ func TestListClusterRoleRBAC(t *testing.T) {
SetupRoleFunc: func(r types.Role) {
r.SetKubeResources(types.Allow, []types.KubernetesResource{
{
Kind: types.KindKubeClusterRole,
Name: types.Wildcard,
Kind: types.KindKubeClusterRole,
Name: types.Wildcard,
Verbs: []string{types.Wildcard},
},
})
},
@@ -941,8 +991,9 @@ func TestListClusterRoleRBAC(t *testing.T) {
r.SetKubeResources(types.Allow,
[]types.KubernetesResource{
{
Kind: types.KindKubeClusterRole,
Name: "nginx-*",
Kind: types.KindKubeClusterRole,
Name: "nginx-*",
Verbs: []string{types.Wildcard},
},
},
)
+3 -1
View File
@@ -144,6 +144,7 @@ func (f *Forwarder) validateSelfSubjectAccessReview(actx *authContext, w http.Re
Kind: resource,
Name: name,
Namespace: namespace,
Verbs: []string{accessReview.Spec.ResourceAttributes.Verb},
},
},
}...); {
@@ -171,7 +172,8 @@ func (f *Forwarder) validateSelfSubjectAccessReview(actx *authContext, w http.Re
"kubernetes_resources:\n"+
"- kind: %s\n"+
" name: %s\n"+
" namespace: %s\n", accessReview.Spec.ResourceAttributes.Resource, namespaceNameToString(namespace, name), kubernetesResourcesKey, resource, emptyOrWildcard(name), emptyOrWildcard(namespace)),
" namespace: %s\n"+
" verbs: [%s]\n", accessReview.Spec.ResourceAttributes.Resource, namespaceNameToString(namespace, name), kubernetesResourcesKey, resource, emptyOrWildcard(name), emptyOrWildcard(namespace), types.Wildcard),
}
responsewriters.SetContentTypeHeader(w, req.Header)
@@ -71,6 +71,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) {
Kind: types.KindKubePod,
Namespace: types.Wildcard,
Name: types.Wildcard,
Verbs: []string{types.Wildcard},
},
},
},
@@ -87,6 +88,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) {
Kind: types.KindKubePod,
Namespace: types.Wildcard,
Name: types.Wildcard,
Verbs: []string{types.Wildcard},
},
},
},
@@ -103,6 +105,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) {
Kind: types.KindKubePod,
Namespace: types.Wildcard,
Name: types.Wildcard,
Verbs: []string{types.Wildcard},
},
},
},
@@ -119,6 +122,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: "pod-2",
Namespace: "namespace-1",
Verbs: []string{types.Wildcard},
},
},
},
@@ -135,11 +139,13 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: "pod-2",
Namespace: "namespace-1",
Verbs: []string{types.Wildcard},
},
{
Kind: types.KindKubePod,
Name: "pod-1",
Namespace: "namespace-1",
Verbs: []string{types.Wildcard},
},
},
},
@@ -156,6 +162,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: "pod-2",
Namespace: "namespace-1",
Verbs: []string{types.Wildcard},
},
},
},
@@ -172,6 +179,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) {
Kind: types.KindKubePod,
Name: "pod-2",
Namespace: "namespace-1",
Verbs: []string{types.Wildcard},
},
},
},
+51 -2
View File
@@ -17,6 +17,7 @@ limitations under the License.
package proxy
import (
"net/http"
"path"
"strings"
@@ -31,6 +32,7 @@ type apiResource struct {
resourceKind string
resourceName string
skipEvent bool
isWatch bool
}
// parseResourcePath does best-effort parsing of a Kubernetes API request path.
@@ -92,6 +94,7 @@ func parseResourcePath(p string) apiResource {
// Watch API endpoints have an extra /watch/ prefix. For now, silently
// strip it from our result.
if len(parts) > 0 && parts[0] == "watch" {
r.isWatch = true
parts = parts[1:]
}
@@ -206,8 +209,8 @@ func getResourceWithKey(k allowedResourcesKey) string {
// getResourceFromRequest returns a KubernetesResource if the user tried to access
// a specific endpoint that Teleport support resource filtering. Otherwise, returns nil.
func getResourceFromRequest(requestURI string) (*types.KubernetesResource, apiResource) {
apiResource := parseResourcePath(requestURI)
func getResourceFromRequest(req *http.Request) (*types.KubernetesResource, apiResource) {
apiResource := parseResourcePath(req.URL.Path)
resourceType, ok := getTeleportResourceKindFromAPIResource(apiResource)
// if the resource is not supported, return nil.
// if the resource is supported but the resource name is not present, return nil because it's a list request.
@@ -218,6 +221,7 @@ func getResourceFromRequest(requestURI string) (*types.KubernetesResource, apiRe
Kind: resourceType,
Namespace: apiResource.namespace,
Name: apiResource.resourceName,
Verbs: []string{apiResource.getVerb(req)},
}, apiResource
}
@@ -236,3 +240,48 @@ func getResourceFromAPIResource(resourceKind string) string {
}
return resourceKind
}
// isKubeWatchRequest returns true if the request is a watch request.
func isKubeWatchRequest(req *http.Request, r apiResource) bool {
if values := req.URL.Query()["watch"]; len(values) > 0 {
switch strings.ToLower(values[0]) {
case "false", "0":
default:
return true
}
}
return r.isWatch
}
func (r apiResource) getVerb(req *http.Request) string {
verb := ""
isWatch := isKubeWatchRequest(req, r)
switch req.Method {
case http.MethodPost:
verb = types.KubeVerbCreate
case http.MethodGet, http.MethodHead, http.MethodOptions:
switch {
case isWatch:
return types.KubeVerbWatch
case r.resourceName == "":
return types.KubeVerbList
default:
return types.KubeVerbGet
}
case http.MethodPut:
verb = types.KubeVerbUpdate
case http.MethodPatch:
verb = types.KubeVerbPatch
case http.MethodDelete:
switch {
case r.resourceName != "":
verb = types.KubeVerbDelete
default:
verb = types.KubeVerbDeleteCollection
}
default:
verb = ""
}
return verb
}
+44 -42
View File
@@ -17,6 +17,8 @@ limitations under the License.
package proxy
import (
"net/http"
"net/url"
"testing"
"github.com/google/go-cmp/cmp"
@@ -43,17 +45,17 @@ func TestParseResourcePath(t *testing.T) {
{path: "/apis/apps/v1", want: apiResource{skipEvent: true, apiGroup: "apps", apiGroupVersion: "v1"}},
{path: "/apis/apps/v1/", want: apiResource{skipEvent: true, apiGroup: "apps", apiGroupVersion: "v1"}},
{path: "/api/v1/pods", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", resourceKind: "pods"}},
{path: "/api/v1/watch/pods", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", resourceKind: "pods"}},
{path: "/api/v1/watch/pods", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", resourceKind: "pods", isWatch: true}},
{path: "/api/v1/namespaces/kube-system", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", resourceKind: "namespaces", resourceName: "kube-system"}},
{path: "/api/v1/watch/namespaces/kube-system", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", resourceKind: "namespaces", resourceName: "kube-system"}},
{path: "/api/v1/watch/namespaces/kube-system", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", resourceKind: "namespaces", resourceName: "kube-system", isWatch: true}},
{path: "/apis/rbac.authorization.k8s.io/v1/clusterroles", want: apiResource{apiGroup: "rbac.authorization.k8s.io", apiGroupVersion: "v1", resourceKind: "clusterroles"}},
{path: "/apis/rbac.authorization.k8s.io/v1/watch/clusterroles", want: apiResource{apiGroup: "rbac.authorization.k8s.io", apiGroupVersion: "v1", resourceKind: "clusterroles"}},
{path: "/apis/rbac.authorization.k8s.io/v1/watch/clusterroles", want: apiResource{apiGroup: "rbac.authorization.k8s.io", apiGroupVersion: "v1", resourceKind: "clusterroles", isWatch: true}},
{path: "/apis/rbac.authorization.k8s.io/v1/clusterroles/foo", want: apiResource{apiGroup: "rbac.authorization.k8s.io", apiGroupVersion: "v1", resourceKind: "clusterroles", resourceName: "foo"}},
{path: "/apis/rbac.authorization.k8s.io/v1/watch/clusterroles/foo", want: apiResource{apiGroup: "rbac.authorization.k8s.io", apiGroupVersion: "v1", resourceKind: "clusterroles", resourceName: "foo"}},
{path: "/apis/rbac.authorization.k8s.io/v1/watch/clusterroles/foo", want: apiResource{apiGroup: "rbac.authorization.k8s.io", apiGroupVersion: "v1", resourceKind: "clusterroles", resourceName: "foo", isWatch: true}},
{path: "/api/v1/namespaces/kube-system/pods", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", namespace: "kube-system", resourceKind: "pods"}},
{path: "/api/v1/watch/namespaces/kube-system/pods", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", namespace: "kube-system", resourceKind: "pods"}},
{path: "/api/v1/watch/namespaces/kube-system/pods", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", namespace: "kube-system", resourceKind: "pods", isWatch: true}},
{path: "/api/v1/namespaces/kube-system/pods/foo", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", namespace: "kube-system", resourceKind: "pods", resourceName: "foo"}},
{path: "/api/v1/watch/namespaces/kube-system/pods/foo", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", namespace: "kube-system", resourceKind: "pods", resourceName: "foo"}},
{path: "/api/v1/watch/namespaces/kube-system/pods/foo", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", namespace: "kube-system", resourceKind: "pods", resourceName: "foo", isWatch: true}},
{path: "/api/v1/namespaces/kube-system/pods/foo/exec", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", namespace: "kube-system", resourceKind: "pods/exec", resourceName: "foo"}},
{path: "/apis/apiregistration.k8s.io/v1/apiservices/foo/status", want: apiResource{apiGroup: "apiregistration.k8s.io", apiGroupVersion: "v1", resourceKind: "apiservices/status", resourceName: "foo"}},
{path: "/api/v1/nodes/foo/proxy/bar", want: apiResource{apiGroup: "core", apiGroupVersion: "v1", resourceKind: "nodes/proxy/bar", resourceName: "foo"}},
@@ -88,104 +90,104 @@ func Test_getResourceFromRequest(t *testing.T) {
{path: "/apis/apps/v1/", want: nil},
{path: "/api/v1/pods", want: nil},
{path: "/api/v1/watch/pods", want: nil},
{path: "/api/v1/namespaces/kube-system", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "kube-system"}},
{path: "/api/v1/watch/namespaces/kube-system", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "kube-system"}},
{path: "/api/v1/namespaces/kube-system", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "kube-system", Verbs: []string{"get"}}},
{path: "/api/v1/watch/namespaces/kube-system", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "kube-system", Verbs: []string{"watch"}}},
{path: "/api/v1/namespaces/kube-system/pods", want: nil},
{path: "/api/v1/watch/namespaces/kube-system/pods", want: nil},
{path: "/api/v1/namespaces/kube-system/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo"}},
{path: "/api/v1/watch/namespaces/kube-system/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo"}},
{path: "/api/v1/namespaces/kube-system/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"get"}}},
{path: "/api/v1/watch/namespaces/kube-system/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"watch"}}},
{path: "/apis/apiregistration.k8s.io/v1/apiservices/foo/status", want: nil},
// core
// Pods
{path: "/api/v1/pods", want: nil},
{path: "/api/v1/namespaces/default/pods", want: nil},
{path: "/api/v1/namespaces/default/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo"}},
{path: "/api/v1/watch/namespaces/default/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo"}},
{path: "/api/v1/namespaces/kube-system/pods/foo/exec", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo"}},
{path: "/api/v1/namespaces/default/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/api/v1/watch/namespaces/default/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
{path: "/api/v1/namespaces/kube-system/pods/foo/exec", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"get"}}},
// Secrets
{path: "/api/v1/secrets", want: nil},
{path: "/api/v1/namespaces/default/secrets", want: nil},
{path: "/api/v1/namespaces/default/secrets/foo", want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo"}},
{path: "/api/v1/watch/namespaces/default/secrets/foo", want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo"}},
{path: "/api/v1/namespaces/default/secrets/foo", want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/api/v1/watch/namespaces/default/secrets/foo", want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// Configmaps
{path: "/api/v1/configmaps", want: nil},
{path: "/api/v1/namespaces/default/configmaps", want: nil},
{path: "/api/v1/namespaces/default/configmaps/foo", want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo"}},
{path: "/api/v1/watch/namespaces/default/configmaps/foo", want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo"}},
{path: "/api/v1/namespaces/default/configmaps/foo", want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/api/v1/watch/namespaces/default/configmaps/foo", want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// Namespaces
{path: "/api/v1/namespaces", want: nil},
{path: "/api/v1/namespaces/default", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "default"}},
{path: "/api/v1/watch/namespaces/default", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "default"}},
{path: "/api/v1/namespaces/default", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "default", Verbs: []string{"get"}}},
{path: "/api/v1/watch/namespaces/default", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "default", Verbs: []string{"watch"}}},
// Nodes
{path: "/api/v1/nodes", want: nil},
{path: "/api/v1/nodes/foo/proxy/bar", want: &types.KubernetesResource{Kind: types.KindKubeNode, Name: "foo"}},
{path: "/api/v1/nodes/foo/proxy/bar", want: &types.KubernetesResource{Kind: types.KindKubeNode, Name: "foo", Verbs: []string{"get"}}},
// Services
{path: "/api/v1/services", want: nil},
{path: "/api/v1/namespaces/default/services", want: nil},
{path: "/api/v1/namespaces/default/services/foo", want: &types.KubernetesResource{Kind: types.KindKubeService, Namespace: "default", Name: "foo"}},
{path: "/api/v1/namespaces/default/services/foo", want: &types.KubernetesResource{Kind: types.KindKubeService, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
// ServiceAccounts
{path: "/api/v1/serviceaccounts", want: nil},
{path: "/api/v1/namespaces/default/serviceaccounts", want: nil},
{path: "/api/v1/namespaces/default/serviceaccounts/foo", want: &types.KubernetesResource{Kind: types.KindKubeServiceAccount, Namespace: "default", Name: "foo"}},
{path: "/api/v1/watch/namespaces/default/serviceaccounts/foo", want: &types.KubernetesResource{Kind: types.KindKubeServiceAccount, Namespace: "default", Name: "foo"}},
{path: "/api/v1/namespaces/default/serviceaccounts/foo", want: &types.KubernetesResource{Kind: types.KindKubeServiceAccount, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/api/v1/watch/namespaces/default/serviceaccounts/foo", want: &types.KubernetesResource{Kind: types.KindKubeServiceAccount, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// PersistentVolumes
{path: "/api/v1/persistentvolumes", want: nil},
{path: "/api/v1/namespaces/default/persistentvolumes", want: nil},
{path: "/api/v1/namespaces/default/persistentvolumes/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolume, Namespace: "default", Name: "foo"}},
{path: "/api/v1/watch/namespaces/default/persistentvolumes/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolume, Namespace: "default", Name: "foo"}},
{path: "/api/v1/namespaces/default/persistentvolumes/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolume, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/api/v1/watch/namespaces/default/persistentvolumes/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolume, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// PersistentVolumeClaims
{path: "/api/v1/persistentvolumeclaims", want: nil},
{path: "/api/v1/namespaces/default/persistentvolumeclaims", want: nil},
{path: "/api/v1/namespaces/default/persistentvolumeclaims/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolumeClaim, Namespace: "default", Name: "foo"}},
{path: "/api/v1/watch/namespaces/default/persistentvolumeclaims/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolumeClaim, Namespace: "default", Name: "foo"}},
{path: "/api/v1/namespaces/default/persistentvolumeclaims/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolumeClaim, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/api/v1/watch/namespaces/default/persistentvolumeclaims/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolumeClaim, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// apis/apps
// Deployments
{path: "/apis/apps/v1/deployments", want: nil},
{path: "/apis/apps/v1/namespaces/default/deployments", want: nil},
{path: "/apis/apps/v1/namespaces/default/deployments/foo", want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo"}},
{path: "/apis/apps/v1/watch/namespaces/default/deployments/foo", want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo"}},
{path: "/apis/apps/v1/namespaces/default/deployments/foo", want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/apis/apps/v1/watch/namespaces/default/deployments/foo", want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// Statefulsets
{path: "/apis/apps/v1/statefulsets", want: nil},
{path: "/apis/apps/v1/namespaces/default/statefulsets", want: nil},
{path: "/apis/apps/v1/namespaces/default/statefulsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeStatefulset, Namespace: "default", Name: "foo"}},
{path: "/apis/apps/v1/watch/namespaces/default/statefulsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeStatefulset, Namespace: "default", Name: "foo"}},
{path: "/apis/apps/v1/namespaces/default/statefulsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeStatefulset, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/apis/apps/v1/watch/namespaces/default/statefulsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeStatefulset, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// Replicasets
{path: "/apis/apps/v1/replicasets", want: nil},
{path: "/apis/apps/v1/namespaces/default/replicasets", want: nil},
{path: "/apis/apps/v1/namespaces/default/replicasets/foo", want: &types.KubernetesResource{Kind: types.KindKubeReplicaSet, Namespace: "default", Name: "foo"}},
{path: "/apis/apps/v1/watch/namespaces/default/replicasets/foo", want: &types.KubernetesResource{Kind: types.KindKubeReplicaSet, Namespace: "default", Name: "foo"}},
{path: "/apis/apps/v1/namespaces/default/replicasets/foo", want: &types.KubernetesResource{Kind: types.KindKubeReplicaSet, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/apis/apps/v1/watch/namespaces/default/replicasets/foo", want: &types.KubernetesResource{Kind: types.KindKubeReplicaSet, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// Daemonsets
{path: "/apis/apps/v1/daemonsets", want: nil},
{path: "/apis/apps/v1/namespaces/default/daemonsets", want: nil},
{path: "/apis/apps/v1/namespaces/default/daemonsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeDaemonSet, Namespace: "default", Name: "foo"}},
{path: "/apis/apps/v1/watch/namespaces/default/daemonsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeDaemonSet, Namespace: "default", Name: "foo"}},
{path: "/apis/apps/v1/namespaces/default/daemonsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeDaemonSet, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/apis/apps/v1/watch/namespaces/default/daemonsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeDaemonSet, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// apis/batch
// Job
{path: "/apis/batch/v1/jobs", want: nil},
{path: "/apis/batch/v1/namespaces/default/jobs", want: nil},
{path: "/apis/batch/v1/namespaces/default/jobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeJob, Namespace: "default", Name: "foo"}},
{path: "/apis/batch/v1/watch/namespaces/default/jobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeJob, Namespace: "default", Name: "foo"}},
{path: "/apis/batch/v1/namespaces/default/jobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeJob, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/apis/batch/v1/watch/namespaces/default/jobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeJob, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// CronJob
{path: "/apis/batch/v1/cronjobs", want: nil},
{path: "/apis/batch/v1/namespaces/default/cronjobs", want: nil},
{path: "/apis/batch/v1/namespaces/default/cronjobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeCronjob, Namespace: "default", Name: "foo"}},
{path: "/apis/batch/v1/watch/namespaces/default/cronjobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeCronjob, Namespace: "default", Name: "foo"}},
{path: "/apis/batch/v1/namespaces/default/cronjobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeCronjob, Namespace: "default", Name: "foo", Verbs: []string{"get"}}},
{path: "/apis/batch/v1/watch/namespaces/default/cronjobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeCronjob, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}},
// apis/certificates.k8s.io
{path: "/apis/certificates.k8s.io/v1/certificatesigningrequests", want: nil},
{path: "/apis/certificates.k8s.io/v1/certificatesigningrequests/foo", want: &types.KubernetesResource{Kind: types.KindKubeCertificateSigningRequest, Name: "foo"}},
{path: "/apis/certificates.k8s.io/v1/certificatesigningrequests/foo", want: &types.KubernetesResource{Kind: types.KindKubeCertificateSigningRequest, Name: "foo", Verbs: []string{"get"}}},
// apis/networking.k8s.io
{path: "/apis/networking.k8s.io/v1/ingresses", want: nil},
{path: "/apis/networking.k8s.io/v1/ingresses/foo", want: &types.KubernetesResource{Kind: types.KindKubeIngress, Name: "foo"}},
{path: "/apis/networking.k8s.io/v1/ingresses/foo", want: &types.KubernetesResource{Kind: types.KindKubeIngress, Name: "foo", Verbs: []string{"get"}}},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
got, _ := getResourceFromRequest(tt.path)
got, _ := getResourceFromRequest(&http.Request{Method: http.MethodGet, URL: &url.URL{Path: tt.path}})
require.Equal(t, tt.want, got, "parsing path %q", tt.path)
})
}
+1 -1
View File
@@ -416,7 +416,7 @@ func (c *TestContext) CreateUserAndRole(ctx context.Context, t *testing.T, usern
role.SetSessionRequirePolicies(roleSpec.SessionRequire)
role.SetSessionJoinPolicies(roleSpec.SessionJoin)
if roleSpec.SetupRoleFunc == nil {
role.SetKubeResources(types.Allow, []types.KubernetesResource{{Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard}})
role.SetKubeResources(types.Allow, []types.KubernetesResource{{Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}}})
} else {
roleSpec.SetupRoleFunc(role)
}
+31 -18
View File
@@ -424,7 +424,7 @@ func (a *accessChecker) GetKubeResources(cluster types.KubeCluster) (allowed, de
if len(a.info.AllowedResourceIDs) == 0 {
return a.RoleSet.GetKubeResources(cluster, a.info.Traits)
}
var err error
rolesAllowed, rolesDenied := a.RoleSet.GetKubeResources(cluster, a.info.Traits)
// Allways append the denied resources from the roles. This is because
// the denied resources from the roles take precedence over the allowed
@@ -436,20 +436,33 @@ func (a *accessChecker) GetKubeResources(cluster types.KubeCluster) (allowed, de
}
switch {
case slices.Contains(types.KubernetesResourcesKinds, r.Kind):
splitted := strings.SplitN(r.SubResourceName, "/", 3)
// This condition should never happen since SubResourceName is validated
// but it's better to validate it.
if len(splitted) != 2 {
continue
namespace := ""
name := ""
if slices.Contains(types.KubernetesClusterWideResourceKinds, r.Kind) {
// Cluster wide resources do not have a namespace.
name = r.SubResourceName
} else {
splitted := strings.SplitN(r.SubResourceName, "/", 3)
// This condition should never happen since SubResourceName is validated
// but it's better to validate it.
if len(splitted) != 2 {
continue
}
namespace = splitted[0]
name = splitted[1]
}
r := types.KubernetesResource{
Kind: r.Kind,
Namespace: splitted[0],
Name: splitted[1],
Namespace: namespace,
Name: name,
}
if matchKubernetesResource(r, rolesAllowed, rolesDenied) == nil {
// matchKubernetesResource checks if the Kubernetes Resource matches the tuple
// (kind, namespace, kame) from the allowed/denied list and does not match the resource
// verbs. Verbs are not checked here because they are not included in the
// ResourceID but we collect them and set them in the returned KubernetesResource
// so that they can be matched when the resource is accessed.
if r.Verbs, err = matchKubernetesResource(r, rolesAllowed, rolesDenied); err == nil {
allowed = append(allowed, r)
}
case r.Kind == types.KindKubernetesCluster:
@@ -464,24 +477,24 @@ func (a *accessChecker) GetKubeResources(cluster types.KubeCluster) (allowed, de
// matchKubernetesResource checks if the Kubernetes Resource does not match any
// entry from the deny list and matches at least one entry from the allowed list.
func matchKubernetesResource(resource types.KubernetesResource, allowed, denied []types.KubernetesResource) error {
func matchKubernetesResource(resource types.KubernetesResource, allowed, denied []types.KubernetesResource) ([]string, error) {
// utils.KubeResourceMatchesRegex checks if the resource.Kind is strictly equal
// to each entry and validates if the Name and Namespace fields matches the
// regex allowed by each entry.
result, err := utils.KubeResourceMatchesRegex(resource, denied)
result, _, err := utils.KubeResourceMatchesRegexWithVerbsCollector(resource, denied)
if err != nil {
return trace.Wrap(err)
return nil, trace.Wrap(err)
} else if result {
return trace.AccessDenied("access to %s %q denied", resource.Kind, resource.ClusterResource())
return nil, trace.AccessDenied("access to %s %q denied", resource.Kind, resource.ClusterResource())
}
result, err = utils.KubeResourceMatchesRegex(resource, allowed)
result, verbs, err := utils.KubeResourceMatchesRegexWithVerbsCollector(resource, allowed)
if err != nil {
return trace.Wrap(err)
return nil, trace.Wrap(err)
} else if !result {
return trace.AccessDenied("access to %s %q denied", resource.Kind, resource.ClusterResource())
return nil, trace.AccessDenied("access to %s %q denied", resource.Kind, resource.ClusterResource())
}
return nil
return verbs, nil
}
// GetAllowedResourceIDs returns the list of allowed resources the identity for
+108
View File
@@ -39,6 +39,7 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "dev",
Namespace: "dev",
Verbs: []string{types.Wildcard},
},
})
rv.SetKubernetesLabels(types.Allow, kubeDevLabels)
@@ -51,11 +52,34 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.Wildcard},
},
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any2",
Verbs: []string{types.Wildcard},
},
})
rv.SetKubernetesLabels(types.Allow, kubeAnyLabels)
rv.SetKubeUsers(types.Allow, kubeUsers)
}),
)
listOnlySet := NewRoleSet(
newRole(func(rv *types.RoleV6) {
rv.SetName("list-only")
rv.SetKubeResources(types.Allow, []types.KubernetesResource{
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.KubeVerbList},
},
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any2",
Verbs: []string{types.KubeVerbList},
},
})
rv.SetKubernetesLabels(types.Allow, kubeAnyLabels)
@@ -88,6 +112,7 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.KubeVerbGet},
},
},
wantAllowed: []types.KubernetesResource{
@@ -95,11 +120,13 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.Wildcard},
},
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any2",
Verbs: []string{types.Wildcard},
},
},
wantDenied: emptySet,
@@ -117,6 +144,7 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "any1",
Namespace: "rand",
Verbs: []string{types.KubeVerbGet},
},
},
wantAllowed: []types.KubernetesResource{
@@ -124,16 +152,19 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.Wildcard},
},
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any2",
Verbs: []string{types.Wildcard},
},
{
Kind: types.KindKubePod,
Name: "dev",
Namespace: "dev",
Verbs: []string{types.Wildcard},
},
},
wantDenied: emptySet,
@@ -176,6 +207,7 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "dev",
Namespace: "dev",
Verbs: []string{types.KubeVerbGet},
},
},
wantAllowed: []types.KubernetesResource{
@@ -183,6 +215,7 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "dev",
Namespace: "dev",
Verbs: []string{types.Wildcard},
},
},
wantDenied: emptySet,
@@ -224,6 +257,7 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.KubeVerbGet},
},
},
wantAllowed: nil,
@@ -254,6 +288,7 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "dev",
Namespace: "dev",
Verbs: []string{types.KubeVerbGet},
},
},
wantAllowed: []types.KubernetesResource{
@@ -261,16 +296,19 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.Wildcard},
},
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any2",
Verbs: []string{types.Wildcard},
},
{
Kind: types.KindKubePod,
Name: "dev",
Namespace: "dev",
Verbs: []string{types.Wildcard},
},
},
wantDenied: emptySet,
@@ -301,6 +339,7 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "dev",
Namespace: "dev",
Verbs: []string{types.KubeVerbGet},
},
},
wantAllowed: []types.KubernetesResource{
@@ -308,6 +347,7 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "dev",
Namespace: "dev",
Verbs: []string{types.Wildcard},
},
},
wantDenied: emptySet,
@@ -338,6 +378,7 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "dev",
Namespace: "dev",
Verbs: []string{types.KubeVerbGet},
},
},
wantAllowed: []types.KubernetesResource{
@@ -345,11 +386,13 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.Wildcard},
},
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any2",
Verbs: []string{types.Wildcard},
},
},
wantDenied: emptySet,
@@ -375,12 +418,77 @@ func TestAccessCheckerKubeResources(t *testing.T) {
Kind: types.KindKubePod,
Name: "wrongPodName",
Namespace: "wrongNamespace",
Verbs: []string{types.KubeVerbGet},
},
},
wantAllowed: nil,
wantDenied: emptySet,
assertAccess: require.Error,
},
{
name: "prod cluster with list verb but tries to access get",
kubeCluster: prodKubeCluster,
fields: fields{
info: &AccessInfo{
Roles: []string{"list-only"},
},
roleSet: listOnlySet,
resource: types.KubernetesResource{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.KubeVerbGet},
},
},
wantAllowed: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.KubeVerbList},
},
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any2",
Verbs: []string{types.KubeVerbList},
},
},
wantDenied: emptySet,
assertAccess: require.Error,
},
{
name: "prod cluster with list verb",
kubeCluster: prodKubeCluster,
fields: fields{
info: &AccessInfo{
Roles: []string{"list-only"},
},
roleSet: listOnlySet,
resource: types.KubernetesResource{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.KubeVerbList},
},
},
wantAllowed: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any1",
Verbs: []string{types.KubeVerbList},
},
{
Kind: types.KindKubePod,
Name: "any1",
Namespace: "any2",
Verbs: []string{types.KubeVerbList},
},
},
wantDenied: emptySet,
assertAccess: require.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+1
View File
@@ -207,6 +207,7 @@ func NewPresetAccessRole() types.Role {
Kind: types.Wildcard,
Namespace: types.Wildcard,
Name: types.Wildcard,
Verbs: []string{types.Wildcard},
},
},
Rules: []types.Rule{
+9 -3
View File
@@ -2201,14 +2201,20 @@ type KubeResourcesMatcher struct {
// Match matches a Kubernetes resource against provided role and condition.
func (m *KubeResourcesMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) {
var finalResult bool
for _, pod := range m.resources {
result, err := utils.KubeResourceMatchesRegex(pod, role.GetKubeResources(condition))
for _, resource := range m.resources {
// We use utils.KubeResourceMatchesRegexWithVerbsCollector instead of utils.KubeResourceMatchesRegex
// because KubeResourcesMatcher is used to match access request resources at creation time against
// the roles specified in the `search_as_roles` field. This means that we don't have the request verb
// at this point and we need to match the resource against all the verbs specified in the role.
// If the resource matches any of the verbs, we consider the resource as matched.
// Verbs are enforced at the request time when the user is trying to access the Kubernetes Pod.
result, _, err := utils.KubeResourceMatchesRegexWithVerbsCollector(resource, role.GetKubeResources(condition))
if err != nil {
return false, trace.Wrap(err)
}
if result {
delete(m.unmatchedReqs, pod.ClusterResource())
delete(m.unmatchedReqs, resource.ClusterResource())
finalResult = true
}
}
+4
View File
@@ -257,6 +257,7 @@ func TestRoleParse(t *testing.T) {
Kind: types.KindKubePod,
Namespace: types.Wildcard,
Name: types.Wildcard,
Verbs: []string{types.Wildcard},
},
},
},
@@ -500,6 +501,7 @@ func TestRoleParse(t *testing.T) {
Kind: types.KindKubePod,
Namespace: types.Wildcard,
Name: types.Wildcard,
Verbs: []string{types.Wildcard},
},
},
Namespaces: []string{"default"},
@@ -608,6 +610,7 @@ func TestRoleParse(t *testing.T) {
Kind: types.KindKubePod,
Namespace: types.Wildcard,
Name: types.Wildcard,
Verbs: []string{types.Wildcard},
},
},
Rules: []types.Rule{
@@ -697,6 +700,7 @@ func TestRoleParse(t *testing.T) {
Kind: types.KindKubePod,
Namespace: types.Wildcard,
Name: types.Wildcard,
Verbs: []string{types.Wildcard},
},
},
NodeLabels: types.Labels{
+54 -1
View File
@@ -20,6 +20,8 @@ import (
"github.com/gravitational/trace"
lru "github.com/hashicorp/golang-lru/v2"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"github.com/gravitational/teleport/api/types"
)
@@ -94,11 +96,62 @@ type RegexpConfig struct {
// This function supports regex expressions in the Name and Namespace fields,
// but not for the Kind field.
// The wildcard (*) expansion is also supported.
func KubeResourceMatchesRegex(input types.KubernetesResource, resources []types.KubernetesResource) (bool, error) {
func KubeResourceMatchesRegexWithVerbsCollector(input types.KubernetesResource, resources []types.KubernetesResource) (bool, []string, error) {
verbs := map[string]struct{}{}
matchedAny := false
for _, resource := range resources {
if input.Kind != resource.Kind && resource.Kind != types.Wildcard {
continue
}
switch ok, err := MatchString(input.Name, resource.Name); {
case err != nil:
return false, nil, trace.Wrap(err)
case !ok:
continue
}
if ok, err := MatchString(input.Namespace, resource.Namespace); err != nil {
return false, nil, trace.Wrap(err)
} else if !ok {
continue
}
matchedAny = true
if len(resource.Verbs) > 0 && resource.Verbs[0] == types.Wildcard {
return true, []string{types.Wildcard}, nil
}
for _, verb := range resource.Verbs {
verbs[verb] = struct{}{}
}
}
return matchedAny, maps.Keys(verbs), nil
}
// KubeResourceMatchesRegex checks whether the input matches any of the given
// expressions.
// This function returns as soon as it finds the first match or when matchString
// returns an error.
// This function supports regex expressions in the Name and Namespace fields,
// but not for the Kind field.
// The wildcard (*) expansion is also supported.
func KubeResourceMatchesRegex(input types.KubernetesResource, resources []types.KubernetesResource) (bool, error) {
if len(input.Verbs) != 1 {
return false, trace.BadParameter("only one verb is supported, input: %v", input.Verbs)
}
verb := input.Verbs[0]
for _, resource := range resources {
if input.Kind != resource.Kind && resource.Kind != types.Wildcard {
continue
}
// If the resource has a wildcard verb, it matches all verbs.
// Otherwise, the resource must have the verb we're looking for otherwise
// it doesn't match.
// When the resource has a wildcard verb, we only allow one verb in the
// resource input.
if len(resource.Verbs) == 0 || resource.Verbs[0] != types.Wildcard && !slices.Contains(resource.Verbs, verb) {
continue
}
switch ok, err := MatchString(input.Name, resource.Name); {
case err != nil:
return false, trace.Wrap(err)
+88 -2
View File
@@ -162,7 +162,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) {
assert require.ErrorAssertionFunc
}{
{
name: "input matches single resource",
name: "input misses verb",
input: types.KubernetesResource{
Kind: types.KindKubePod,
Namespace: "default",
@@ -175,8 +175,83 @@ func TestKubeResourceMatchesRegex(t *testing.T) {
Name: "podname",
},
},
assert: require.Error,
matches: false,
},
{
name: "input matches single resource with wildcard verb",
input: types.KubernetesResource{
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
Verbs: []string{types.KubeVerbGet},
},
resources: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
Verbs: []string{types.Wildcard},
},
},
assert: require.NoError,
matches: true,
},
{
name: "input matches single resource with matching verb",
input: types.KubernetesResource{
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
Verbs: []string{types.KubeVerbGet},
},
resources: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
Verbs: []string{types.KubeVerbCreate, types.KubeVerbGet},
},
},
assert: require.NoError,
matches: true,
},
{
name: "input matches single resource with unmatching verb",
input: types.KubernetesResource{
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
Verbs: []string{types.KubeVerbPatch},
},
resources: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
Verbs: []string{types.KubeVerbGet, types.KubeVerbGet},
},
},
assert: require.NoError,
matches: false,
},
{
name: "input does not match single resource because missing verb",
input: types.KubernetesResource{
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
Verbs: []string{types.KubeVerbGet},
},
resources: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
},
},
assert: require.NoError,
matches: true,
matches: false,
},
{
name: "input matches last resource",
@@ -184,22 +259,26 @@ func TestKubeResourceMatchesRegex(t *testing.T) {
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
Verbs: []string{types.KubeVerbGet},
},
resources: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Namespace: "other_namespace",
Name: "podname",
Verbs: []string{types.Wildcard},
},
{
Kind: types.KindKubePod,
Namespace: "default",
Name: "other_pod",
Verbs: []string{types.Wildcard},
},
{
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
Verbs: []string{types.Wildcard},
},
},
assert: require.NoError,
@@ -211,12 +290,14 @@ func TestKubeResourceMatchesRegex(t *testing.T) {
Kind: types.KindKubePod,
Namespace: "default-5",
Name: "podname-5",
Verbs: []string{types.KubeVerbGet},
},
resources: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Namespace: "defa*",
Name: "^podname-[0-9]+$",
Verbs: []string{types.Wildcard},
},
},
assert: require.NoError,
@@ -228,12 +309,14 @@ func TestKubeResourceMatchesRegex(t *testing.T) {
Kind: types.KindKubePod,
Namespace: "default",
Name: "pod-name",
Verbs: []string{types.KubeVerbGet},
},
resources: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Namespace: "default",
Name: "^pod-[0-9]+$",
Verbs: []string{types.Wildcard},
},
},
assert: require.NoError,
@@ -245,12 +328,14 @@ func TestKubeResourceMatchesRegex(t *testing.T) {
Kind: types.KindKubePod,
Namespace: "default-5",
Name: "podname-5",
Verbs: []string{types.KubeVerbGet},
},
resources: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Namespace: "defa*",
Name: "^podname-[0-+$",
Verbs: []string{types.Wildcard},
},
},
assert: require.Error,
@@ -261,6 +346,7 @@ func TestKubeResourceMatchesRegex(t *testing.T) {
Kind: types.KindKubePod,
Namespace: "default",
Name: "podname",
Verbs: []string{types.KubeVerbGet},
},
resources: []types.KubernetesResource{
{