mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-29 17:58:15 +08:00
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:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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{
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -207,6 +207,7 @@ func NewPresetAccessRole() types.Role {
|
||||
Kind: types.Wildcard,
|
||||
Namespace: types.Wildcard,
|
||||
Name: types.Wildcard,
|
||||
Verbs: []string{types.Wildcard},
|
||||
},
|
||||
},
|
||||
Rules: []types.Rule{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user