diff --git a/docs/content/concepts/authorization/authorizers.md b/docs/content/concepts/authorization/authorizers.md index da8961e3fe6..53fe4b74a46 100644 --- a/docs/content/concepts/authorization/authorizers.md +++ b/docs/content/concepts/authorization/authorizers.md @@ -330,3 +330,34 @@ When impersonating a user in a logical cluster, the resulting user identity is scoped to the logical cluster the impersonation is happening in. A scope mismatch does not invalidate the warrants (see next section) of a user. + +### Warrants + +Warrants are a way to grant extra access to a user. It can be limited by the scope of a logical cluster. +A warrant is attached by adding a `authorization.kcp.io/warrant` extra field to the user identity +with a JSON-encoded user info, and the limiting logical cluster set as `authentication.kcp.io/scopes: cluster:`. +in the embedded user info's extra. The warrant is then checked by the authorizers in the chain of every step +if the primary users is not allowed. For example: + +```yaml +user: user1 +groups: ["group1"] +extra: + authorization.kcp.io/warrant: | + { + "user": "user2", + "groups": ["group2"], + "extra": { + "authentication.kcp.io/scopes": "cluster:logical-cluster-1" + } + } +``` + +This warrant allows `user1` to act under the permissions of `user2` in +`logical-cluster-1` if `user1` is not allowed to act as `user2` in the first place. + +Note that a warrant only allow to act under the permissions of the warrant user, +but not to act as the warrant user itself. E.g. in auditing or admission control, +the primary user is still the one that is acting. + +Warrants can be nested, i.e. a warrant can contain another warrant. diff --git a/go.mod b/go.mod index 89bda71325a..2f7a8adfed4 100644 --- a/go.mod +++ b/go.mod @@ -168,36 +168,36 @@ require ( replace ( github.com/kcp-dev/kcp/sdk => ./sdk - k8s.io/api => github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20250116190004-350c910bca8d - k8s.io/apiextensions-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20250116190004-350c910bca8d - k8s.io/apimachinery => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20250116190004-350c910bca8d - k8s.io/apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250116190004-350c910bca8d - k8s.io/cli-runtime => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20250116190004-350c910bca8d - k8s.io/client-go => github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20250116190004-350c910bca8d - k8s.io/cloud-provider => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20250116190004-350c910bca8d - k8s.io/cluster-bootstrap => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20250116190004-350c910bca8d - k8s.io/code-generator => github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20250116190004-350c910bca8d - k8s.io/component-base => github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20250116190004-350c910bca8d - k8s.io/component-helpers => github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20250116190004-350c910bca8d - k8s.io/controller-manager => github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20250116190004-350c910bca8d - k8s.io/cri-api => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20250116190004-350c910bca8d - k8s.io/cri-client => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-client v0.0.0-20250116190004-350c910bca8d - k8s.io/csi-translation-lib => github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20250116190004-350c910bca8d - k8s.io/dynamic-resource-allocation => github.com/kcp-dev/kubernetes/staging/src/k8s.io/dynamic-resource-allocation v0.0.0-20250116190004-350c910bca8d - k8s.io/endpointslice => github.com/kcp-dev/kubernetes/staging/src/k8s.io/endpointslice v0.0.0-20250116190004-350c910bca8d - k8s.io/kms => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kms v0.0.0-20250116190004-350c910bca8d - k8s.io/kube-aggregator => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20250116190004-350c910bca8d - k8s.io/kube-controller-manager => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20250116190004-350c910bca8d - k8s.io/kube-proxy => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20250116190004-350c910bca8d - k8s.io/kube-scheduler => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20250116190004-350c910bca8d - k8s.io/kubectl => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20250116190004-350c910bca8d - k8s.io/kubelet => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20250116190004-350c910bca8d - k8s.io/kubernetes => github.com/kcp-dev/kubernetes v0.0.0-20250116190004-350c910bca8d - k8s.io/legacy-cloud-providers => github.com/kcp-dev/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20250116190004-350c910bca8d - k8s.io/metrics => github.com/kcp-dev/kubernetes/staging/src/k8s.io/metrics v0.0.0-20250116190004-350c910bca8d - k8s.io/mount-utils => github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20250116190004-350c910bca8d - k8s.io/pod-security-admission => github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20250116190004-350c910bca8d - k8s.io/sample-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20250116190004-350c910bca8d - k8s.io/sample-cli-plugin => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-cli-plugin v0.0.0-20250116190004-350c910bca8d - k8s.io/sample-controller => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-controller v0.0.0-20250116190004-350c910bca8d + k8s.io/api => github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20250125214723-819c32dd4b3d + k8s.io/apiextensions-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20250125214723-819c32dd4b3d + k8s.io/apimachinery => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20250125214723-819c32dd4b3d + k8s.io/apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250125214723-819c32dd4b3d + k8s.io/cli-runtime => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20250125214723-819c32dd4b3d + k8s.io/client-go => github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20250125214723-819c32dd4b3d + k8s.io/cloud-provider => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20250125214723-819c32dd4b3d + k8s.io/cluster-bootstrap => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20250125214723-819c32dd4b3d + k8s.io/code-generator => github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20250125214723-819c32dd4b3d + k8s.io/component-base => github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20250125214723-819c32dd4b3d + k8s.io/component-helpers => github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20250125214723-819c32dd4b3d + k8s.io/controller-manager => github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20250125214723-819c32dd4b3d + k8s.io/cri-api => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20250125214723-819c32dd4b3d + k8s.io/cri-client => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-client v0.0.0-20250125214723-819c32dd4b3d + k8s.io/csi-translation-lib => github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20250125214723-819c32dd4b3d + k8s.io/dynamic-resource-allocation => github.com/kcp-dev/kubernetes/staging/src/k8s.io/dynamic-resource-allocation v0.0.0-20250125214723-819c32dd4b3d + k8s.io/endpointslice => github.com/kcp-dev/kubernetes/staging/src/k8s.io/endpointslice v0.0.0-20250125214723-819c32dd4b3d + k8s.io/kms => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kms v0.0.0-20250125214723-819c32dd4b3d + k8s.io/kube-aggregator => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20250125214723-819c32dd4b3d + k8s.io/kube-controller-manager => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20250125214723-819c32dd4b3d + k8s.io/kube-proxy => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20250125214723-819c32dd4b3d + k8s.io/kube-scheduler => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20250125214723-819c32dd4b3d + k8s.io/kubectl => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20250125214723-819c32dd4b3d + k8s.io/kubelet => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20250125214723-819c32dd4b3d + k8s.io/kubernetes => github.com/kcp-dev/kubernetes v0.0.0-20250125214723-819c32dd4b3d + k8s.io/legacy-cloud-providers => github.com/kcp-dev/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20250125214723-819c32dd4b3d + k8s.io/metrics => github.com/kcp-dev/kubernetes/staging/src/k8s.io/metrics v0.0.0-20250125214723-819c32dd4b3d + k8s.io/mount-utils => github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20250125214723-819c32dd4b3d + k8s.io/pod-security-admission => github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20250125214723-819c32dd4b3d + k8s.io/sample-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20250125214723-819c32dd4b3d + k8s.io/sample-cli-plugin => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-cli-plugin v0.0.0-20250125214723-819c32dd4b3d + k8s.io/sample-controller => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-controller v0.0.0-20250125214723-819c32dd4b3d ) diff --git a/go.sum b/go.sum index b43fda393a3..78e5746d6be 100644 --- a/go.sum +++ b/go.sum @@ -148,50 +148,50 @@ github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20240817110845-a9eb9752bfeb h1:W11F/ github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20240817110845-a9eb9752bfeb/go.mod h1:mEDD1K5BVUXJ4CP6wcJ0vZUf+7tbFMjkCFzBKsUNj18= github.com/kcp-dev/client-go v0.0.0-20240912145314-f5949d81732a h1:O9SNM3MqMlwoEAPSWxk/yw4JU211KpVsAFjTXWQcMEk= github.com/kcp-dev/client-go v0.0.0-20240912145314-f5949d81732a/go.mod h1:h5jC8rEbkyGUgV86+sgtMMcl950ooGzk+iLrQnbCR6o= -github.com/kcp-dev/kubernetes v0.0.0-20250116190004-350c910bca8d h1:iLQ5IrzKF/v1p2/EPFBCwa4u48xVEB3ZxUwdGHOU4rM= -github.com/kcp-dev/kubernetes v0.0.0-20250116190004-350c910bca8d/go.mod h1:oRHZZzd7fEOSk0mY8fxM++8QPpafz47cbtQUxWENogQ= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20250116190004-350c910bca8d h1:Qwm/JCruyHm8S3pUup/2gi/yu9YXTGQ+sZs6fqfDDOI= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20250116190004-350c910bca8d/go.mod h1:6X07YVZkpyT/6XVz4cwyYM2oYH3A3k2QR54H7JXMD90= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20250116190004-350c910bca8d h1:YUIpkiwsDJKmbndcob8LPOMgMNTZd4Z/t62VMcAwdp8= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20250116190004-350c910bca8d/go.mod h1:8EZw4zqlExmz7lUTE/P7V0vdAyfiYL84i4ZUHY6qyrk= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20250116190004-350c910bca8d h1:lf+94FUA8ApYz8SXPuNtRVP6zBJBGdpgu61vVLM0ZaM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20250116190004-350c910bca8d/go.mod h1:5F0wbie5xX1jDEg5sk5dr+KF8rwFkYtZFHDhSF/UsG4= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250116190004-350c910bca8d h1:s+xQx5yJ6xUOwnW5mhVHubsHNsvB/JpL3VhF6V9Ek+Y= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250116190004-350c910bca8d/go.mod h1:EC5je+P5ix2QCV4zbwxlY8Zk5MJe4e1eKXxjCMd/7Eo= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20250116190004-350c910bca8d h1:0fBOEVHrGgyMCANVO77IRUk2oE6BQBNSQsUgKrqRGI0= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20250116190004-350c910bca8d/go.mod h1:l7HaB8VBHdNA72/wtAohDsemuLiVNdW6hx9lNB5J088= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20250116190004-350c910bca8d h1:O0uvhuHVhHiUNHjZjQN2oYIjRRxxKY1r0sHiZ5KtifE= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20250116190004-350c910bca8d/go.mod h1:Z1By/ZJf4qFPOirsblzPEI/p61GMTqgsLITfY2hffQo= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20250116190004-350c910bca8d h1:AzRUjuVzRhv9I5CcdMOqoJv7BUsANbCrCpP4n9h/XKY= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20250116190004-350c910bca8d/go.mod h1:A1M/anf3QTVOO3C4oma0zIeBekzHJtgO3Ght7QLBOV8= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20250116190004-350c910bca8d h1:wWSxgLmFaikqzisHDIZiMMnxUs6uctQKrs6+2ovS61I= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20250116190004-350c910bca8d/go.mod h1:FeckrMB5SHLGBJWSRr79xheTG7il5LcGhzdx/v88Jus= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20250116190004-350c910bca8d h1:PpTIqE/3XKDixngXLVuRxUjTe/UsiBqGweQIkRco8G0= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20250116190004-350c910bca8d/go.mod h1:pgdjhgz6QeKjIVxzIYq4JFZ7VBJRutg/n5W9OKX33qA= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20250116190004-350c910bca8d h1:3rIuJ38nTwnkCgJfcID+86OYosMd4TaCzQyA+ZosCTg= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20250116190004-350c910bca8d/go.mod h1:Mx6U6lQkq013rHloS+AAq00nd0b3kkI8cuIu+o2SA7g= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20250116190004-350c910bca8d h1:L0MC4OqwVdDTkoV+BfZoOqSSlJ/R6TwNWRq+P7wp+zM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20250116190004-350c910bca8d/go.mod h1:Dc3aSKcPoKmbdnOXAHq/V4Bavxxht9+1bTU8R5ap9yM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20250116190004-350c910bca8d h1:8VdsADAQhgEYBwuvKmQh65nY43gXDbSv2tWKpkCZD7I= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20250116190004-350c910bca8d/go.mod h1:e2pTb6psrP2AtdW24SxJaesf2402rQ0YjNa7qYssoi0= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-client v0.0.0-20250116190004-350c910bca8d h1:Dr8qyhlnz0x6OjRoVWIeL8zErEAls5u6aluKiTMIGgw= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-client v0.0.0-20250116190004-350c910bca8d/go.mod h1:iz2L6DSY4Ur/Bc/gwXkKZlTnkd/97jgITpRAnuZE2jU= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20250116190004-350c910bca8d h1:lAV/VxHE1+rzTwum+uRhHkgGqhtPNOg6L6s92RJTo9k= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20250116190004-350c910bca8d/go.mod h1:9M1LHui5W3M9sY3jRBxHpPt30VQ+GdJLgWyGH27/DaI= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/dynamic-resource-allocation v0.0.0-20250116190004-350c910bca8d h1:FzQWcTG/LwxQcZw1IXqklk8OAFzlZ0wOqH6XeiPKfrs= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/dynamic-resource-allocation v0.0.0-20250116190004-350c910bca8d/go.mod h1:85Fs9bUmpkY/tUQh6zXjvI5BbN7EfiQYQGj07Zf9Dg0= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kms v0.0.0-20250116190004-350c910bca8d h1:u1xl5aqX/DZLRiOS4S4zcjT5q0SK7jUrYzU2s6+OPzo= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kms v0.0.0-20250116190004-350c910bca8d/go.mod h1:gClzb5q8LLAagWlaL9S/rt8IcU3iY6gRARKN09DY4o8= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20250116190004-350c910bca8d h1:8cmBmWas85BcLuO7tiSUhMoZZSfVEuaTYcPFhLCrJqw= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20250116190004-350c910bca8d/go.mod h1:npcdPN5G2BMexN5GsPN/xjRN57Ars6h7O7wpjjLJuck= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20250116190004-350c910bca8d h1:rHD9U1022a0ypOJYP9HERpYWMd0xs21yhRAtTfcOZzM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20250116190004-350c910bca8d/go.mod h1:vMjQhLaEOvaFZvq0RLwFPFF2i660WpvyRAYzBLtXByI= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20250116190004-350c910bca8d h1:wwh54guBNRtN3TspKPY5hgWq2VtKLYu/YmBW+6A4s/k= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20250116190004-350c910bca8d/go.mod h1:W3k9YOX2gYkx8IbOyQ9mTgWjJHqIZ6/2fBVKLQOiW/E= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20250116190004-350c910bca8d h1:54Ucyz/KOumxXJgOMZWRSkeZfB3AgUW0HBQSII6N3Pw= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20250116190004-350c910bca8d/go.mod h1:p5r0u2M9KzooTgHDz4zRsUt02y4Yx7/5uPwgr0nSGqg= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20250116190004-350c910bca8d h1:I3MIHT8Peij21bwuZLuGu2343OjId3ONROfWa1PFW/o= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20250116190004-350c910bca8d/go.mod h1:kgTU85Q97g45QWn61Zi55b8iDTap0ZcsXxRSlUMt8o4= +github.com/kcp-dev/kubernetes v0.0.0-20250125214723-819c32dd4b3d h1:KDPr/lXil+3tub9niKP7oa1R0AUVu4RJDwNBMTHfCtY= +github.com/kcp-dev/kubernetes v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:oRHZZzd7fEOSk0mY8fxM++8QPpafz47cbtQUxWENogQ= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20250125214723-819c32dd4b3d h1:L3ePEgpTvwQnohVCsf8317ys0aIo1Z30K8bMvsoUeD4= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:6X07YVZkpyT/6XVz4cwyYM2oYH3A3k2QR54H7JXMD90= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20250125214723-819c32dd4b3d h1:4FgA92zj2b/FYQOUesaLuuuN3Ph4T8EHnQa29HNcW1g= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:8EZw4zqlExmz7lUTE/P7V0vdAyfiYL84i4ZUHY6qyrk= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20250125214723-819c32dd4b3d h1:u0OLo/u5kQ+QEHmPfwFdbJ7cSZs04Qc3DOBh+V/TsA0= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:5F0wbie5xX1jDEg5sk5dr+KF8rwFkYtZFHDhSF/UsG4= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250125214723-819c32dd4b3d h1:Gt2nH/dwSp/za1dEHb88pHMIyvGSMHfwWXABwRaYIyg= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:EC5je+P5ix2QCV4zbwxlY8Zk5MJe4e1eKXxjCMd/7Eo= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20250125214723-819c32dd4b3d h1:o3jUkC6dDsvUusG07S+3OFXBdSfQk6ycEsNUH6FDBfw= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:l7HaB8VBHdNA72/wtAohDsemuLiVNdW6hx9lNB5J088= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20250125214723-819c32dd4b3d h1:sJaTGB95+lagfbAs64KXVak5oXncrsSSCArHjiF1iKY= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:Z1By/ZJf4qFPOirsblzPEI/p61GMTqgsLITfY2hffQo= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20250125214723-819c32dd4b3d h1:mWLE0dym0BBhOIC6RSQxxNXd9DcsoxhTeDxkC8IulbQ= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:A1M/anf3QTVOO3C4oma0zIeBekzHJtgO3Ght7QLBOV8= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20250125214723-819c32dd4b3d h1:O6ltK16Pv256HBkT5xjNO6GLEXZPApDodQJoidtVNV0= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:FeckrMB5SHLGBJWSRr79xheTG7il5LcGhzdx/v88Jus= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20250125214723-819c32dd4b3d h1:QIlevvp9pOzY0WwhsXK1hVQxkRg5+umeb5lv2lQk1aw= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:pgdjhgz6QeKjIVxzIYq4JFZ7VBJRutg/n5W9OKX33qA= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20250125214723-819c32dd4b3d h1:QGPNZ0bNBzQWJce/cNNalaUQCvqe9LdQ0ejsDKnHRII= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:Mx6U6lQkq013rHloS+AAq00nd0b3kkI8cuIu+o2SA7g= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20250125214723-819c32dd4b3d h1:bDmOQmsP1Vv5PZHS8StV0LuHfyBgPPIAtdYCVA0aPHo= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:Dc3aSKcPoKmbdnOXAHq/V4Bavxxht9+1bTU8R5ap9yM= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20250125214723-819c32dd4b3d h1:5iWsDj5IZzP9gLYz/dBSMnfEPsjA15iSYrErGWrwm3o= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:e2pTb6psrP2AtdW24SxJaesf2402rQ0YjNa7qYssoi0= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-client v0.0.0-20250125214723-819c32dd4b3d h1:NHaifVKiWmoxzsPFDc2mz/eOJIDPBh/yePDAIGEUU8w= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-client v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:iz2L6DSY4Ur/Bc/gwXkKZlTnkd/97jgITpRAnuZE2jU= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20250125214723-819c32dd4b3d h1:r0Nyw8R0Ea8oLLIMHIDN+ehSbjAKQhK8UbDsKXlDKNk= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:9M1LHui5W3M9sY3jRBxHpPt30VQ+GdJLgWyGH27/DaI= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/dynamic-resource-allocation v0.0.0-20250125214723-819c32dd4b3d h1:dd9qBlJV9kuks0yeky2a0sIEzKd7vI6k/EjSYskyH1A= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/dynamic-resource-allocation v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:85Fs9bUmpkY/tUQh6zXjvI5BbN7EfiQYQGj07Zf9Dg0= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/kms v0.0.0-20250125214723-819c32dd4b3d h1:AD0E8bW1JgVKEUJDpgXFJ3T2q9YixftDmuxivIVrhek= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/kms v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:gClzb5q8LLAagWlaL9S/rt8IcU3iY6gRARKN09DY4o8= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20250125214723-819c32dd4b3d h1:w4+3Ys7tayvEIFEPUeys9AFbeow13C1KKtu68GrRHPY= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:npcdPN5G2BMexN5GsPN/xjRN57Ars6h7O7wpjjLJuck= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20250125214723-819c32dd4b3d h1:NFhs90+rQOrqUn4yfsF5DeT8gSLGyo8PKE7ZrZa7GAs= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:vMjQhLaEOvaFZvq0RLwFPFF2i660WpvyRAYzBLtXByI= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20250125214723-819c32dd4b3d h1:H3PM2BTkvdwr8cP5e3NmldvjQfbzpS9gpP+IhN7JLXg= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:W3k9YOX2gYkx8IbOyQ9mTgWjJHqIZ6/2fBVKLQOiW/E= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20250125214723-819c32dd4b3d h1:XgYvvMJExRYxoGWAQzjgOp9Ccl8lqF9k+aMu0LN+uRE= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:p5r0u2M9KzooTgHDz4zRsUt02y4Yx7/5uPwgr0nSGqg= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20250125214723-819c32dd4b3d h1:CWtWCza0WGY/gFdPxY1Pd/xGFgSBhI+HW1v2vr6p7j4= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20250125214723-819c32dd4b3d/go.mod h1:kgTU85Q97g45QWn61Zi55b8iDTap0ZcsXxRSlUMt8o4= github.com/kcp-dev/logicalcluster/v3 v3.0.5 h1:JbYakokb+5Uinz09oTXomSUJVQsqfxEvU4RyHUYxHOU= github.com/kcp-dev/logicalcluster/v3 v3.0.5/go.mod h1:EWBUBxdr49fUB1cLMO4nOdBWmYifLbP1LfoL20KkXYY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= diff --git a/pkg/authorization/deep_sar.go b/pkg/authorization/deep_sar.go index b45c81be2d4..1fb0f9fe0a6 100644 --- a/pkg/authorization/deep_sar.go +++ b/pkg/authorization/deep_sar.go @@ -22,12 +22,12 @@ import ( "net/http" authorizationv1 "k8s.io/api/authorization/v1" - "k8s.io/apimachinery/pkg/util/sets" kuser "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/client-go/rest" + rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation" ) type deepSARKeyType int @@ -97,7 +97,7 @@ func WithDeepSubjectAccessReview(handler http.Handler) http.Handler { responsewriters.InternalError(w, r, fmt.Errorf("cannot get user")) return } - if !sets.New[string](user.GetGroups()...).Has(kuser.SystemPrivilegedGroup) { + if !rbacregistryvalidation.EffectiveGroups(r.Context(), user).Has(kuser.SystemPrivilegedGroup) { handler.ServeHTTP(w, r) return } diff --git a/pkg/authorization/maximal_permission_policy_authorizer.go b/pkg/authorization/maximal_permission_policy_authorizer.go index 74ad3ac4bcd..689a6e56395 100644 --- a/pkg/authorization/maximal_permission_policy_authorizer.go +++ b/pkg/authorization/maximal_permission_policy_authorizer.go @@ -30,6 +30,7 @@ import ( genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/client-go/tools/cache" controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver" + rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation" "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac" "github.com/kcp-dev/kcp/pkg/indexers" @@ -166,12 +167,7 @@ func (a *MaximalPermissionPolicyAuthorizer) Authorize(ctx context.Context, attr // If bound, create a rbac authorizer filtered to the cluster. clusterAuthorizer := a.newAuthorizer(logicalcluster.From(apiExport)) prefixedAttr := deepCopyAttributes(attr) - userInfo := prefixedAttr.User.(*user.DefaultInfo) - userInfo.Name = apisv1alpha1.MaximalPermissionPolicyRBACUserGroupPrefix + userInfo.Name - userInfo.Groups = make([]string, 0, len(attr.GetUser().GetGroups())) - for _, g := range attr.GetUser().GetGroups() { - userInfo.Groups = append(userInfo.Groups, apisv1alpha1.MaximalPermissionPolicyRBACUserGroupPrefix+g) - } + prefixedAttr.User = rbacregistryvalidation.PrefixUser(prefixedAttr.GetUser(), apisv1alpha1.MaximalPermissionPolicyRBACUserGroupPrefix) dec, reason, err := clusterAuthorizer.Authorize(ctx, prefixedAttr) reason = fmt.Sprintf("API export %q|%q policy: %v", logicalcluster.From(apiExport), apiExport.Name, reason) if err != nil { diff --git a/pkg/authorization/requiredgroups_authorizer.go b/pkg/authorization/requiredgroups_authorizer.go index dbb8073d8be..9ea54ad9400 100644 --- a/pkg/authorization/requiredgroups_authorizer.go +++ b/pkg/authorization/requiredgroups_authorizer.go @@ -24,10 +24,9 @@ import ( "github.com/kcp-dev/logicalcluster/v3" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/authorization/authorizer" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/kubernetes/pkg/registry/rbac/validation" + rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation" "github.com/kcp-dev/kcp/pkg/authorization/bootstrap" corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" @@ -75,12 +74,13 @@ func (a *requiredGroupsAuthorizer) Authorize(ctx context.Context, attr authorize return authorizer.DecisionNoOpinion, "empty cluster name", nil } - if sets.New[string](attr.GetUser().GetGroups()...).Has(bootstrap.SystemLogicalClusterAdmin) { + effGroups := rbacregistryvalidation.EffectiveGroups(ctx, attr.GetUser()) + if effGroups.Has(bootstrap.SystemLogicalClusterAdmin) { return DelegateAuthorization("logical cluster admin access", a.delegate).Authorize(ctx, attr) } switch { - case validation.IsServiceAccount(attr.GetUser()): + case rbacregistryvalidation.IsServiceAccount(attr.GetUser()): // service accounts are always allowed return DelegateAuthorization("service account access to logical cluster", a.delegate).Authorize(ctx, attr) @@ -95,7 +95,7 @@ func (a *requiredGroupsAuthorizer) Authorize(ctx context.Context, attr authorize } // always let external-logical-cluster-admins through - if sets.New[string](attr.GetUser().GetGroups()...).Has(bootstrap.SystemExternalLogicalClusterAdmin) { + if effGroups.Has(bootstrap.SystemExternalLogicalClusterAdmin) { return DelegateAuthorization("external logical cluster admin access", a.delegate).Authorize(ctx, attr) } @@ -107,7 +107,7 @@ func (a *requiredGroupsAuthorizer) Authorize(ctx context.Context, attr authorize disjunctiveClauses := append(strings.Split(value, ";"), bootstrap.SystemKcpAdminGroup, bootstrap.SystemKcpWorkspaceBootstrapper) for _, set := range disjunctiveClauses { groups := strings.Split(set, ",") - if sets.New[string](attr.GetUser().GetGroups()...).HasAll(groups...) { + if effGroups.HasAll(groups...) { return DelegateAuthorization(fmt.Sprintf("user is member of required groups: %s", logicalCluster.Annotations[RequiredGroupsAnnotationKey]), a.delegate).Authorize(ctx, attr) } } diff --git a/pkg/authorization/resolver_test.go b/pkg/authorization/resolver_test.go new file mode 100644 index 00000000000..bf9084d9aaa --- /dev/null +++ b/pkg/authorization/resolver_test.go @@ -0,0 +1,318 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authorization + +import ( + "context" + "fmt" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + authserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/request" + rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation" + "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac" +) + +// TestResolverWithWarrants is a smoke test testing RBAC with kcp extensions: +// - scopes +// - warrants +// - globally valid service accounts. +// Everything of this is already tested in the kube carry patch. But as this +// is important functionality, we want to make sure it is not broken. +func TestResolverWithWarrants(t *testing.T) { + getPods := &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"get"}, + APIGroups: []string{""}, + Resources: []string{"pods"}, + } + getServices := &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"get"}, + APIGroups: []string{""}, + Resources: []string{"services"}, + } + getNodes := &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"get"}, + APIGroups: []string{""}, + Resources: []string{"nodes"}, + } + getHealthz := &authorizer.DefaultNonResourceRuleInfo{ + Verbs: []string{"get"}, + NonResourceURLs: []string{"/healthz"}, + } + getReadyz := &authorizer.DefaultNonResourceRuleInfo{ + Verbs: []string{"get"}, + NonResourceURLs: []string{"/readyz"}, + } + getMetrics := &authorizer.DefaultNonResourceRuleInfo{ + Verbs: []string{"get"}, + NonResourceURLs: []string{"/metrics"}, + } + getRoot := &authorizer.DefaultNonResourceRuleInfo{ + Verbs: []string{"get"}, + NonResourceURLs: []string{"/"}, + } + + tests := []struct { + name string + user user.Info + wantResourceRules []authorizer.ResourceRuleInfo + wantNonResourceRules []authorizer.NonResourceRuleInfo + wantError bool + }{ + { + name: "base without warrants", + user: &user.DefaultInfo{Name: "user-a"}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz}, + }, + { + name: "base with same warrant", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"user-a"}`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz}, + }, + { + name: "base with different warrant", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"user-b"}`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes, getServices}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz, getReadyz}, + }, + { + name: "unknown base with warrant", + user: &user.DefaultInfo{Name: "user-c", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"user-b"}`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getServices}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getReadyz}, + }, + { + name: "base with unknown warrant", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"user-c"}`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz}, + }, + { + name: "base with multiple warrants", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"user-b"}`, `{"user":"user-b"}`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes, getServices}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz, getReadyz}, + }, + { + name: "base with invalid warrant, ignored", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`invalid`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz}, + }, + { + name: "service account without cluster", + user: &user.DefaultInfo{Name: "system:serviceaccount:default:sa", Groups: []string{"system:serviceaccounts", user.AllAuthenticated}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getServices}, + wantNonResourceRules: nil, // global service accounts do no work without a cluster. + }, + { + name: "service account with this cluster", + user: &user.DefaultInfo{Name: "system:serviceaccount:default:sa", Groups: []string{"system:serviceaccounts", user.AllAuthenticated}, Extra: map[string][]string{authserviceaccount.ClusterNameKey: {"this"}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getServices}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getReadyz}, + }, + { + name: "service account with other cluster", + user: &user.DefaultInfo{Name: "system:serviceaccount:default:sa", Groups: []string{"system:serviceaccounts", user.AllAuthenticated}, Extra: map[string][]string{authserviceaccount.ClusterNameKey: {"other"}}}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getMetrics}, + }, + { + name: "base with service account warrant without cluster, ignored", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"system:serviceaccount:default:sa"}`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz}, + }, + { + name: "base with service account warrant with this cluster", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"system:serviceaccount:default:sa","extra":{"authentication.kcp.io/scopes": ["cluster:this"]}}`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz}, + }, + { + name: "base with service account warrant with other cluster", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"system:serviceaccount:default:sa","extra":{"authentication.kcp.io/scopes": ["cluster:other"]}}`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz}, + }, + { + name: "base with out of scope warrant", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"user-b", "extra":{"authentication.kcp.io/scopes": ["cluster:another"]}}`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz}, + }, + { + name: "out of scope base with warrant", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"user-b"}`}, rbacregistryvalidation.ScopeExtraKey: {"cluster:another"}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getServices}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getReadyz}, + }, + { + name: "base with foreign service account warrant", + user: &user.DefaultInfo{Name: "user-a", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"system:serviceaccount:default:foo","extra":{"authentication.kubernetes.io/cluster-name": ["another"]}}`}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getNodes}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getHealthz}, + }, + { + name: "foreign service account with warrant", + user: &user.DefaultInfo{Name: "system:serviceaccount:default:foo", Extra: map[string][]string{rbacregistryvalidation.WarrantExtraKey: {`{"user":"user-b"}`}, authserviceaccount.ClusterNameKey: {"another"}}}, + wantResourceRules: []authorizer.ResourceRuleInfo{getPods, getServices}, + wantNonResourceRules: []authorizer.NonResourceRuleInfo{getRoot, getReadyz}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, sr := rbacregistryvalidation.NewTestRuleResolver(nil, nil, + []*rbacv1.ClusterRole{{ + ObjectMeta: metav1.ObjectMeta{Name: "get-pods"}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}, APIGroups: []string{""}, Resources: []string{"pods"}}}, + }, { + ObjectMeta: metav1.ObjectMeta{Name: "get-nodes"}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}, APIGroups: []string{""}, Resources: []string{"nodes"}}}, + }, { + ObjectMeta: metav1.ObjectMeta{Name: "get-services"}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}, APIGroups: []string{""}, Resources: []string{"services"}}}, + }, { + ObjectMeta: metav1.ObjectMeta{Name: "get-healthz"}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}, NonResourceURLs: []string{"/healthz"}}}, + }, { + ObjectMeta: metav1.ObjectMeta{Name: "get-readyz"}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}, NonResourceURLs: []string{"/readyz"}}}, + }, { + ObjectMeta: metav1.ObjectMeta{Name: "get-metrics"}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}, NonResourceURLs: []string{"/metrics"}}}, + }, { + ObjectMeta: metav1.ObjectMeta{Name: "get-root"}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}, NonResourceURLs: []string{"/"}}}, + }}, + []*rbacv1.ClusterRoleBinding{ + { + ObjectMeta: metav1.ObjectMeta{Name: "get-pods"}, + Subjects: []rbacv1.Subject{ + {Kind: "User", APIGroup: "rbac.authorization.k8s.io", Name: "user-a"}, + {Kind: "User", APIGroup: "rbac.authorization.k8s.io", Name: "user-b"}, + }, + RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: "get-pods"}, + }, { + ObjectMeta: metav1.ObjectMeta{Name: "get-nodes"}, + Subjects: []rbacv1.Subject{ + {Kind: "User", APIGroup: "rbac.authorization.k8s.io", Name: "user-a"}, + }, + RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: "get-nodes"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "get-services"}, + Subjects: []rbacv1.Subject{ + {Kind: "User", APIGroup: "rbac.authorization.k8s.io", Name: "user-b"}, + {Kind: "ServiceAccount", APIGroup: "", Namespace: "default", Name: "sa"}, + }, + RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: "get-services"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "get-healthz"}, + Subjects: []rbacv1.Subject{ + {Kind: "User", APIGroup: "rbac.authorization.k8s.io", Name: "user-a"}, + }, + RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: "get-healthz"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "get-readyz"}, + Subjects: []rbacv1.Subject{ + {Kind: "User", APIGroup: "rbac.authorization.k8s.io", Name: "user-b"}, + {Kind: "User", APIGroup: "", Name: "system:kcp:serviceaccount:this:default:sa"}, + }, + RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: "get-readyz"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "get-metrics"}, + Subjects: []rbacv1.Subject{ + {Kind: "User", APIGroup: "", Name: "system:kcp:serviceaccount:other:default:sa"}, + }, + RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: "get-metrics"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "get-root"}, + Subjects: []rbacv1.Subject{ + {Kind: "User", APIGroup: "rbac.authorization.k8s.io", Name: "user-a"}, + {Kind: "User", APIGroup: "rbac.authorization.k8s.io", Name: "user-b"}, + }, + RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: "get-root"}, + }, + }) + resolver := rbac.New(sr, sr, sr, sr) + + ctx := request.WithCluster(context.Background(), request.Cluster{Name: "this"}) + resourceRules, nonResourceRules, _, err := resolver.RulesFor(ctx, tt.user, "") + + if (err != nil) != tt.wantError { + t.Fatalf("unexpected error: %v", err) + } + + sort.Sort(sortedResourceRules(tt.wantResourceRules)) + sort.Sort(sortedNonResourceRules(tt.wantNonResourceRules)) + + sort.Sort(sortedResourceRules(resourceRules)) + sort.Sort(sortedNonResourceRules(nonResourceRules)) + + if !tt.wantError { + if diff := cmp.Diff(resourceRules, tt.wantResourceRules); diff != "" { + t.Errorf("resourceRules differs: +want -got:\n%s", diff) + } + if diff := cmp.Diff(nonResourceRules, tt.wantNonResourceRules); diff != "" { + t.Errorf("nonResourceRules differs: +want -got:\n%s", diff) + } + } + }) + } +} + +type sortedResourceRules []authorizer.ResourceRuleInfo + +func (s sortedResourceRules) Len() int { + return len(s) +} + +func (s sortedResourceRules) Less(i, j int) bool { + return fmt.Sprintf("%#v", s[i]) < fmt.Sprintf("%#v", s[j]) +} + +func (s sortedResourceRules) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +type sortedNonResourceRules []authorizer.NonResourceRuleInfo + +func (s sortedNonResourceRules) Len() int { + return len(s) +} + +func (s sortedNonResourceRules) Less(i, j int) bool { + return fmt.Sprintf("%#v", s[i]) < fmt.Sprintf("%#v", s[j]) +} + +func (s sortedNonResourceRules) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} diff --git a/pkg/authorization/warrants.go b/pkg/authorization/warrants.go new file mode 100644 index 00000000000..0f5af2ffa58 --- /dev/null +++ b/pkg/authorization/warrants.go @@ -0,0 +1,75 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authorization + +import ( + "context" + "strings" + + "github.com/kcp-dev/logicalcluster/v3" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/request" + rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation" +) + +// WithWarrantsAndScopes flattens the user's warrants and applies scopes. It +// then calls the underlying authorizer for each users in the result. +func WithWarrantsAndScopes(authz authorizer.Authorizer) authorizer.Authorizer { + return authorizer.AuthorizerFunc(func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + // flatten and scope. + var clusterName logicalcluster.Name + if cluster := request.ClusterFrom(ctx); cluster != nil { + clusterName = cluster.Name + } + eus := rbacregistryvalidation.EffectiveUsers(clusterName, a.GetUser()) + + // authorize like union authorizer does. + var ( + errlist []error + reasonlist []string + ) + for _, eu := range eus { + decision, reason, err := authz.Authorize(ctx, withOtherUser{Attributes: a, user: eu}) + if err != nil { + errlist = append(errlist, err) + } + if len(reason) != 0 { + reasonlist = append(reasonlist, reason) + } + switch decision { + case authorizer.DecisionAllow, authorizer.DecisionDeny: + return decision, reason, err + case authorizer.DecisionNoOpinion: + // continue to the next authorizer + } + } + + return authorizer.DecisionNoOpinion, strings.Join(reasonlist, "\n"), utilerrors.NewAggregate(errlist) + }) +} + +type withOtherUser struct { + authorizer.Attributes + user user.Info +} + +func (w withOtherUser) GetUser() user.Info { + return w.user +} diff --git a/pkg/authorization/workspace_content_authorizer.go b/pkg/authorization/workspace_content_authorizer.go index eb524002976..1ab6c3e766d 100644 --- a/pkg/authorization/workspace_content_authorizer.go +++ b/pkg/authorization/workspace_content_authorizer.go @@ -26,7 +26,6 @@ import ( "github.com/kcp-dev/logicalcluster/v3" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/authorization/authorizer" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver" @@ -96,7 +95,8 @@ func (a *workspaceContentAuthorizer) Authorize(ctx context.Context, attr authori } // always let logical-cluster-admins through - if !isServiceAccount && isInScope && sets.New[string](attr.GetUser().GetGroups()...).Has(bootstrap.SystemLogicalClusterAdmin) { + effGroups := rbacregistryvalidation.EffectiveGroups(ctx, attr.GetUser()) + if effGroups.Has(bootstrap.SystemLogicalClusterAdmin) { return DelegateAuthorization("logical cluster admin access", a.delegate).Authorize(ctx, attr) } diff --git a/pkg/server/options/authorization.go b/pkg/server/options/authorization.go index 8892eb1b079..8a4f76aa42a 100644 --- a/pkg/server/options/authorization.go +++ b/pkg/server/options/authorization.go @@ -173,6 +173,7 @@ func (s *Authorization) ApplyTo(ctx context.Context, config *genericapiserver.Co if err != nil { return err } + authorizer = authz.WithWarrantsAndScopes(authorizer) authorizers = append(authorizers, authorizer) } diff --git a/pkg/virtual/apiexport/builder/build.go b/pkg/virtual/apiexport/builder/build.go index 16bf42603b7..890c1bda40e 100644 --- a/pkg/virtual/apiexport/builder/build.go +++ b/pkg/virtual/apiexport/builder/build.go @@ -18,6 +18,7 @@ package builder import ( "context" + "encoding/json" "errors" "fmt" "strings" @@ -34,6 +35,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" + "k8s.io/kubernetes/pkg/registry/rbac/validation" "github.com/kcp-dev/kcp/pkg/authorization" "github.com/kcp-dev/kcp/pkg/authorization/bootstrap" @@ -109,28 +111,42 @@ func BuildVirtualWorkspace( return nil, fmt.Errorf("error getting valid cluster from context: %w", err) } + user, found := genericapirequest.UserFrom(ctx) + if !found { + return nil, fmt.Errorf("error getting user from context") + } + // Wildcard requests cannot be impersonated against a concrete cluster. if cluster.Wildcard { return dynamicClient, nil } - impersonationConfig := rest.CopyConfig(cfg) - impersonationConfig.Impersonate = rest.ImpersonationConfig{ - UserName: "system:serviceaccount:default:rest", - Groups: []string{bootstrap.SystemKcpAdminGroup}, + // Add a warrant of a fake local service account giving full access + warrant := validation.Warrant{ + User: "system:serviceaccount:default:rest", + Groups: []string{bootstrap.SystemKcpAdminGroup}, Extra: map[string][]string{ serviceaccount.ClusterNameKey: {cluster.Name.Path().String()}, }, } - if user, ok := genericapirequest.UserFrom(ctx); ok { - // We pass the original user and groups as extra fields to - // the impersonation config so that the receiver can make - // decisions based on the original user/groups. - impersonationConfig.Impersonate.Extra[OriginalUserAnnotationKey] = []string{user.GetName()} - impersonationConfig.Impersonate.Extra[OriginalGroupsAnnotationKey] = user.GetGroups() + bs, err := json.Marshal(warrant) + if err != nil { + return nil, fmt.Errorf("error marshaling warrant: %w", err) } + // Impersonate the request user and add the warrant as an extra + impersonationConfig := rest.CopyConfig(cfg) + impersonationConfig.Impersonate = rest.ImpersonationConfig{ + UserName: user.GetName(), + Groups: user.GetGroups(), + UID: user.GetUID(), + Extra: user.GetExtra(), + } + if impersonationConfig.Impersonate.Extra == nil { + impersonationConfig.Impersonate.Extra = map[string][]string{} + } + impersonationConfig.Impersonate.Extra[validation.WarrantExtraKey] = append(impersonationConfig.Impersonate.Extra[validation.WarrantExtraKey], string(bs)) impersonatedClient, err := kcpdynamic.NewForConfig(impersonationConfig) if err != nil { return nil, fmt.Errorf("error generating dynamic client: %w", err) diff --git a/test/e2e/authorizer/scopes_test.go b/test/e2e/authorizer/scopes_test.go index 9171dd85b70..c9a7d933ee2 100644 --- a/test/e2e/authorizer/scopes_test.go +++ b/test/e2e/authorizer/scopes_test.go @@ -119,10 +119,17 @@ func TestSubjectAccessReview(t *testing.T) { user: "system:serviceaccount:default:default", groups: []string{"system:kcp:admin"}, extra: map[string]authorizationv1.ExtraValue{ - serviceaccount.ClusterNameKey: {"root"}, + serviceaccount.ClusterNameKey: {"other"}, }, wantAllowed: false, }, + { + name: "service account with other cluster and warrant", + user: "system:serviceaccount:default:default", groups: []string{"system:kcp:admin"}, extra: map[string]authorizationv1.ExtraValue{ + serviceaccount.ClusterNameKey: {"other"}, + rbacregistryvalidation.WarrantExtraKey: {`{"user":"user","groups":["system:kcp:admin"]}`}, + }, + wantAllowed: true}, } { t.Run(tt.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) @@ -209,7 +216,6 @@ func TestSelfSubjectRulesReview(t *testing.T) { groups []string extra map[string][]string wantRules []authorizationv1.ResourceRule - wantError bool } for _, tt := range []tests{ { @@ -245,6 +251,11 @@ func TestSelfSubjectRulesReview(t *testing.T) { {Verbs: []string{"*"}, APIGroups: []string{"*"}, Resources: []string{"*"}}, }, authenticatedBaseRules...), }, + { + name: "service account with other cluster", + user: "system:serviceaccount:default:default", groups: []string{"system:kcp:admin"}, extra: map[string][]string{serviceaccount.ClusterNameKey: {"other"}}, + wantRules: authenticatedBaseRules, + }, { name: "admin scoped to other cluster", user: "user", groups: []string{"system:kcp:admin", user.AllAuthenticated}, extra: map[string][]string{rbacregistryvalidation.ScopeExtraKey: {"cluster:other"}}, @@ -258,6 +269,16 @@ func TestSelfSubjectRulesReview(t *testing.T) { }, wantRules: authenticatedBaseRules, }, + { + name: "service account with other cluster and warrant", + user: "system:serviceaccount:default:default", groups: []string{"system:kcp:admin"}, extra: map[string][]string{ + serviceaccount.ClusterNameKey: {"other"}, + rbacregistryvalidation.WarrantExtraKey: {`{"user":"user","groups":["system:kcp:admin"]}`}, + }, + wantRules: append([]authorizationv1.ResourceRule{ + {Verbs: []string{"*"}, APIGroups: []string{"*"}, Resources: []string{"*"}}, + }, authenticatedBaseRules...), + }, } { t.Run(tt.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) @@ -282,12 +303,7 @@ func TestSelfSubjectRulesReview(t *testing.T) { }, } resp, err := impersonatedClient.Cluster(wsPath).AuthorizationV1().SelfSubjectRulesReviews().Create(ctx, req, metav1.CreateOptions{}) - if tt.wantError { - require.Error(t, err) - return - } else { - require.NoError(t, err) - } + require.NoError(t, err) sort.Sort(sortedResourceRules(resp.Status.ResourceRules)) sort.Sort(sortedResourceRules(tt.wantRules)) diff --git a/test/e2e/virtual/apiexport/binding_test.go b/test/e2e/virtual/apiexport/binding_test.go index 4b2771970eb..ded73305112 100644 --- a/test/e2e/virtual/apiexport/binding_test.go +++ b/test/e2e/virtual/apiexport/binding_test.go @@ -44,20 +44,24 @@ func TestBinding(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) + t.Logf("Creating two service workspaces and a consumer workspace") org, _ := framework.NewOrganizationFixture(t, server) serviceWorkspacePath, _ := framework.NewWorkspaceFixture(t, server, org, framework.WithName("service")) restrictedWorkspacePath, _ := framework.NewWorkspaceFixture(t, server, org, framework.WithName("restricted-service")) - consumerWorkspacePath, consumerWorkspace := framework.NewWorkspaceFixture(t, server, org, framework.WithName("consumer-workspace")) + consumerWorkspacePath, consumerWorkspace := framework.NewWorkspaceFixture(t, server, org, framework.WithName("consumer")) cfg := server.BaseConfig(t) kubeClient, err := kcpkubernetesclientset.NewForConfig(rest.CopyConfig(cfg)) require.NoError(t, err) kcpClient, err := kcpclientset.NewForConfig(rest.CopyConfig(cfg)) require.NoError(t, err) - serviceProviderUser := server.ClientCAUserConfig(t, rest.CopyConfig(cfg), "service-provider") + t.Logf("Giving service user admin access to service-provider and consumer workspace") + serviceProviderUser := server.ClientCAUserConfig(t, rest.CopyConfig(cfg), "service-provider") framework.AdmitWorkspaceAccess(ctx, t, kubeClient, serviceWorkspacePath, []string{"service-provider"}, nil, true) + framework.AdmitWorkspaceAccess(ctx, t, kubeClient, consumerWorkspacePath, []string{"service-provider"}, nil, true) + t.Logf("Creating 'api-manager' APIExport in service-provider workspace with apibindings permission claim") require.NoError(t, apply(t, ctx, serviceWorkspacePath, cfg, ` apiVersion: apis.kcp.io/v1alpha1 kind: APIExport @@ -68,34 +72,9 @@ spec: - group: "apis.kcp.io" resource: "apibindings" all: true -`, ` -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: api-manager-role -rules: -- apiGroups: - - apis.kcp.io - resources: - - apiexports/content - verbs: - - '*' - resourceNames: - - 'api-manager' -`, ` -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: api-manager-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: api-manager-role -subjects: -- kind: User - name: service-provider `)) + t.Logf("Creating APIExport in restricted workspace without anybody allowed to bind") require.NoError(t, apply(t, ctx, restrictedWorkspacePath, cfg, ` apiVersion: apis.kcp.io/v1alpha1 kind: APIExport @@ -104,8 +83,9 @@ metadata: spec: {} `)) - framework.Eventually(t, func() (bool, string) { - err := apply(t, ctx, consumerWorkspacePath, cfg, fmt.Sprintf(` + t.Logf("Binding to 'api-manager' APIExport succeeds because service-provider user is admin in 'service-provider' workspace") + framework.Eventually(t, func() (success bool, reason string) { + err = apply(t, ctx, consumerWorkspacePath, serviceProviderUser, fmt.Sprintf(` apiVersion: apis.kcp.io/v1alpha1 kind: APIBinding metadata: @@ -122,11 +102,32 @@ spec: path: %v `, serviceWorkspacePath.String())) if err != nil { - return false, fmt.Sprintf("error creating API binding %v", err.Error()) + return false, fmt.Sprintf("Waiting on binding 'api-manager' export: %v", err.Error()) + } + return true, "" + }, wait.ForeverTestTimeout, 1000*time.Millisecond, "waiting on binding 'api-manager' export") + + t.Logf("Binding directly to 'restricted-service' APIExport should be forbidden") + framework.Eventually(t, func() (success bool, reason string) { + err = apply(t, ctx, consumerWorkspacePath, serviceProviderUser, fmt.Sprintf(` +apiVersion: apis.kcp.io/v1alpha1 +kind: APIBinding +metadata: + name: should-fail +spec: + reference: + export: + name: restricted-service + path: %v`, restrictedWorkspacePath.String())) + require.Error(t, err) + want := "unable to create APIBinding: no permission to bind to export" + if got := err.Error(); !strings.Contains(got, want) { + return false, fmt.Sprintf("Waiting on binding to 'restricted-service' to fail because of 'bind' permissions: want %q, got %q", want, got) } return true, "" - }, wait.ForeverTestTimeout, 1000*time.Millisecond, "waiting on virtual workspace to be ready") + }, wait.ForeverTestTimeout, 1000*time.Millisecond, "waiting on binding to 'restricted-service' to fail because of 'bind' permissions") + t.Logf("Waiting for 'api-manager' APIExport virtual workspace URL") serviceProviderVirtualWorkspaceConfig := rest.CopyConfig(serviceProviderUser) framework.Eventually(t, func() (bool, string) { apiExport, err := kcpClient.Cluster(serviceWorkspacePath).ApisV1alpha1().APIExports().Get(ctx, "api-manager", metav1.GetOptions{}) @@ -140,6 +141,7 @@ spec: return found, fmt.Sprintf("waiting for virtual workspace URLs to be available: %v", apiExport.Status.VirtualWorkspaces) }, wait.ForeverTestTimeout, 100*time.Millisecond, "waiting on virtual workspace to be ready") + t.Logf("Binding to 'restricted-service' APIExport through 'api-manager' APIExport virtual workspace is forbidden") framework.Eventually(t, func() (success bool, reason string) { err = apply(t, ctx, logicalcluster.Name(consumerWorkspace.Spec.Cluster).Path(), serviceProviderVirtualWorkspaceConfig, fmt.Sprintf(` apiVersion: apis.kcp.io/v1alpha1 @@ -154,16 +156,17 @@ spec: require.Error(t, err) want := "unable to create APIBinding: no permission to bind to export" if got := err.Error(); !strings.Contains(got, want) { - return false, fmt.Sprintf("want %q, got %q", want, got) + return false, fmt.Sprintf("Waiting on binding to 'restricted-service' APIExport fail because it is forbidden: want %q, got %q", want, got) } return true, "" - }, wait.ForeverTestTimeout, 1000*time.Millisecond, "waiting on virtual workspace to be ready") + }, wait.ForeverTestTimeout, 1000*time.Millisecond, "waiting on binding to 'restricted-service' APIExport to fail because it is forbidden") + t.Logf("Giving service-provider 'bind' access to 'restricted-service' APIExport") require.NoError(t, apply(t, ctx, restrictedWorkspacePath, cfg, ` apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: anonymous-binder + name: restricted-service:bind rules: - apiGroups: - apis.kcp.io @@ -177,16 +180,17 @@ rules: kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: anonymous-binder + name: service-provider:restricted-service:bind subjects: - kind: User - name: system:anonymous + name: service-provider roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: anonymous-binder + name: restricted-service:bind `)) + t.Logf("Binding to 'restricted-service' APIExport through 'api-manager' APIExport virtual workspace succeeds, proving that the service provider identity is used through the APIExport virtual workspace") framework.Eventually(t, func() (bool, string) { err := apply(t, ctx, logicalcluster.Name(consumerWorkspace.Spec.Cluster).Path(), serviceProviderVirtualWorkspaceConfig, fmt.Sprintf(` apiVersion: apis.kcp.io/v1alpha1