Skip to content

Commit 52b1103

Browse files
authored
Merge pull request #3287 from cnvergence/auth-order-followup
✨ authorizer: swap Webhook and RBAC in the default order
2 parents 3299700 + 031e5fd commit 52b1103

File tree

10 files changed

+142
-151
lines changed

10 files changed

+142
-151
lines changed

docs/content/concepts/authorization/authorizers.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,17 @@ In kcp, a request has four different ways of being admitted:
1313
* It can be permitted by an external HTTPS webhook backend.
1414

1515
They are related in the following way:
16-
1716
``` mermaid
1817
graph TD
1918
start(Request):::state --> main_alt[/one of\]:::or
2019
main_alt --> aaga[Always Allow Groups Auth]
2120
main_alt --> aapa[Always Allow Paths Auth]
22-
main_alt --> wa[Webhook Auth]
2321
main_alt --> rga[Required Groups Auth]
22+
main_alt --> wa[Webhook Auth]
23+
2424
2525
aaga --> decision(Decision):::state
2626
aapa --> decision
27-
wa --> decision
2827
2928
subgraph "RBAC"
3029
rga --> wca[Workspace Content Auth]
@@ -40,6 +39,7 @@ graph TD
4039
lpa --> decision
4140
gpa --> decision
4241
bpa --> decision
42+
wa --> decision
4343
4444
classDef state color:#F77
4545
classDef or fill:none,stroke:none
@@ -276,9 +276,25 @@ The webhook will receive JSON-marshalled `SubjectAccessReview` objects, that (co
276276
}
277277
```
278278

279-
280279
The extra field will contain the logical cluster _name_ (e.g. o43u2gh528rtfg721rg92), not the human-readable path. Webhooks need to resolve the name to a path themselves if necessary.
281280

281+
### Authorizer Order
282+
283+
By default, the authorizers are evaluated in the following order:
284+
285+
1. **Always Allow Groups Authorizer** (`AlwaysAllowGroups`)
286+
2. **Always Allow Paths Authorizer** (`AlwaysAllowPaths`)
287+
3. **RBAC Chain** (`RBAC`)
288+
4. **Webhook Authorizer** (`Webhook`)
289+
290+
The order in which authorizers are evaluated can be configured. This allows administrators to customize the authorization flow.
291+
It can be done by setting `--authorization-order` flag in the kcp server. This flag accepts a comma-separated list of authorizer names, which will be evaluated in the specified order.
292+
293+
Here is an example on how to enable the Webhook to be the first one in the authorizers chain:
294+
```sh
295+
--authorization-order=Webhook,AlwaysAllowGroups,AlwaysAllowPaths,RBAC
296+
```
297+
282298
### Scopes
283299

284300
Scopes are a way to limit the access of a user to a specific logical cluster.

pkg/server/options/authorization.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type Authorization struct {
5252
Webhook *kubeoptions.BuiltInAuthorizationOptions
5353

5454
// AuthorizationOrder is the list of authorizers that allows to rearrange the default order.
55-
// The default is four authorizers in a union: AlwaysAllowGroups, AlwaysAllowPaths, Webhook and RBAC.
55+
// The default is four authorizers in a union: AlwaysAllowGroups, AlwaysAllowPaths, RBAC and Webhook.
5656
AuthorizationOrder []string
5757
}
5858

@@ -67,7 +67,7 @@ const (
6767
authorizerRBAC string = "RBAC"
6868
)
6969

70-
var defaultAuthorizers = []string{authorizerAlwaysAllowGroups, authorizerAlwaysAllowPaths, authorizerWebhook, authorizerRBAC}
70+
var defaultAuthorizers = []string{authorizerAlwaysAllowGroups, authorizerAlwaysAllowPaths, authorizerRBAC, authorizerWebhook}
7171

7272
func isValidAuthorizer(authorizer string) bool {
7373
return sets.NewString(defaultAuthorizers...).Has(authorizer)
@@ -147,7 +147,7 @@ func (s *Authorization) AddFlags(fs *pflag.FlagSet) {
147147

148148
fs.StringSliceVar(&s.AuthorizationOrder, "authorization-order", s.AuthorizationOrder,
149149
"A list of authorizers that should be enabled, allowing administrator rearrange the default order."+
150-
"The default order is: AlwaysAllowGroups, AlwaysAllowPaths, Webhook, RBAC")
150+
"The default order is: AlwaysAllowGroups,AlwaysAllowPaths,RBAC,Webhook")
151151

152152
// Only surface selected, webhook-related CLI flags
153153

test/e2e/authorizer/authorizationorder_test.go

Lines changed: 101 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,103 @@ import (
3434

3535
func TestAuthorizationOrder(t *testing.T) {
3636
framework.Suite(t, "control-plane")
37-
webhookPort := "8081"
38-
ctx, cancelFunc := context.WithCancel(context.Background())
39-
t.Cleanup(cancelFunc)
40-
// start a webhook that allows kcp to boot up
41-
webhookStop := RunWebhook(ctx, t, webhookPort, "kubernetes:authz:allow")
42-
t.Cleanup(webhookStop)
43-
44-
server := framework.PrivateKcpServer(t, framework.WithCustomArguments(
45-
"--authorization-order",
46-
"Webhook,AlwaysAllowPaths,AlwaysAllowGroups,RBAC",
47-
"--authorization-webhook-config-file",
48-
"authzorder.kubeconfig",
49-
))
50-
51-
// create clients
37+
t.Parallel()
38+
t.Run("Authorization order 1", func(t *testing.T) {
39+
webhookPort := "8080"
40+
ctx, cancelFunc := context.WithCancel(context.Background())
41+
t.Cleanup(cancelFunc)
42+
webhookStop := RunWebhook(ctx, t, webhookPort, "kubernetes:authz:allow")
43+
t.Cleanup(webhookStop)
44+
45+
server, kcpClusterClient, kubeClusterClient := setupTest(t, "AlwaysAllowGroups,AlwaysAllowPaths,Webhook,RBAC", "testdata/webhook1.kubeconfig")
46+
47+
t.Log("Admin should be allowed to list Workspaces.")
48+
_, err := kcpClusterClient.Cluster(logicalcluster.NewPath("root")).TenancyV1alpha1().Workspaces().List(ctx, metav1.ListOptions{})
49+
require.NoError(t, err)
50+
51+
// stop the webhook and switch to a deny policy
52+
webhookStop()
53+
RunWebhook(ctx, t, webhookPort, "kubernetes:authz:deny")
54+
55+
t.Log("Admin should not be allowed to list ConfigMaps.")
56+
_, err = kubeClusterClient.Cluster(logicalcluster.NewPath("root")).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{})
57+
require.Error(t, err)
58+
// access to health endpoints should still be granted based on --always-allow-paths,
59+
// even if the webhook rejects the request
60+
t.Log("Verify that it is allowed to access one of AllowAllPaths endpoints.")
61+
verifyEndpointAccess(ctx, t, server, "/healthz", true)
62+
})
63+
64+
t.Run("Authorization order 2", func(t *testing.T) {
65+
webhookPort := "8081"
66+
ctx, cancelFunc := context.WithCancel(context.Background())
67+
t.Cleanup(cancelFunc)
68+
webhookStop := RunWebhook(ctx, t, webhookPort, "kubernetes:authz:allow")
69+
t.Cleanup(webhookStop)
70+
71+
server, kcpClusterClient, kubeClusterClient := setupTest(t, "Webhook,AlwaysAllowGroups,AlwaysAllowPaths,RBAC", "testdata/webhook2.kubeconfig")
72+
73+
t.Log("Verify that it is allowed to access one of AllowAllPaths endpoints.")
74+
verifyEndpointAccess(ctx, t, server, "/livez", true)
75+
76+
t.Log("Admin should be allowed now to list Workspaces.")
77+
_, err := kcpClusterClient.Cluster(logicalcluster.NewPath("root")).TenancyV1alpha1().Workspaces().List(ctx, metav1.ListOptions{})
78+
require.NoError(t, err)
79+
80+
// stop the webhook and switch to a deny policy
81+
webhookStop()
82+
RunWebhook(ctx, t, webhookPort, "kubernetes:authz:deny")
83+
84+
t.Log("Admin should not be allowed now to list Logical clusters.")
85+
_, err = kcpClusterClient.Cluster(logicalcluster.NewPath("root")).CoreV1alpha1().LogicalClusters().List(ctx, metav1.ListOptions{})
86+
require.Error(t, err)
87+
88+
t.Log("Admin should not be allowed to list Services.")
89+
_, err = kubeClusterClient.Cluster(logicalcluster.NewPath("root")).CoreV1().Services("default").List(ctx, metav1.ListOptions{})
90+
require.Error(t, err)
91+
92+
t.Log("Verify that it is not allowed to access one of AllowAllPaths endpoints.")
93+
verifyEndpointAccess(ctx, t, server, "/readyz", false)
94+
})
95+
96+
t.Run("Default authorization order", func(t *testing.T) {
97+
webhookPort := "8082"
98+
ctx, cancelFunc := context.WithCancel(context.Background())
99+
t.Cleanup(cancelFunc)
100+
webhookStop := RunWebhook(ctx, t, webhookPort, "kubernetes:authz:deny")
101+
t.Cleanup(webhookStop)
102+
// This will setup the test with the default authorization order: AlwaysAllowGroups,AlwaysAllowPaths,RBAC,Webhook
103+
server, kcpClusterClient, _ := setupTest(t, "", "testdata/webhook3.kubeconfig")
104+
105+
t.Log("Verify that it is allowed to access one of AllowAllPaths endpoints.")
106+
verifyEndpointAccess(ctx, t, server, "/healthz", true)
107+
108+
t.Log("Admin should be allowed to list Workspaces.")
109+
_, err := kcpClusterClient.Cluster(logicalcluster.NewPath("root")).TenancyV1alpha1().Workspaces().List(ctx, metav1.ListOptions{})
110+
require.NoError(t, err)
111+
})
112+
}
113+
114+
func setupTest(t *testing.T, authOrder, webhookConfigFile string) (framework.RunningServer, kcpclientset.ClusterInterface, kcpkubernetesclientset.ClusterInterface) {
115+
args := []string{
116+
"--authorization-webhook-config-file", webhookConfigFile,
117+
}
118+
if authOrder != "" {
119+
args = append(args, "--authorization-order", authOrder)
120+
}
121+
122+
server := framework.PrivateKcpServer(t, framework.WithCustomArguments(args...))
123+
52124
kcpConfig := server.BaseConfig(t)
53125
kubeClusterClient, err := kcpkubernetesclientset.NewForConfig(kcpConfig)
54-
require.NoError(t, err, "failed to construct client for server")
126+
require.NoError(t, err)
55127
kcpClusterClient, err := kcpclientset.NewForConfig(kcpConfig)
56-
require.NoError(t, err, "failed to construct client for server")
128+
require.NoError(t, err)
129+
130+
return server, kcpClusterClient, kubeClusterClient
131+
}
57132

58-
// access to health endpoints should not be granted, as webhook is first
59-
// in the order of authorizers and rejects the request
133+
func verifyEndpointAccess(ctx context.Context, t *testing.T, server framework.RunningServer, endpoint string, shouldSucceed bool) {
60134
rootShardCfg := server.RootShardSystemMasterBaseConfig(t)
61135
if rootShardCfg.NegotiatedSerializer == nil {
62136
rootShardCfg.NegotiatedSerializer = kubernetesscheme.Codecs.WithoutConversion()
@@ -65,35 +139,16 @@ func TestAuthorizationOrder(t *testing.T) {
65139
// in a reloadable authorizer that also always injects a privilegedGroup authorizer
66140
// that lets system:masters users in.
67141
rootShardCfg.BearerToken = ""
68-
restClient, err := rest.UnversionedRESTClientFor(rootShardCfg)
69-
require.NoError(t, err)
70-
71-
t.Log("Verify that you are allowed to access one of AllowAllPaths endpoints.")
72-
req := rest.NewRequest(restClient).RequestURI("/livez")
73-
t.Logf("%s should not be accessible.", req.URL().String())
74-
_, err = req.Do(ctx).Raw()
75-
require.NoError(t, err)
76142

77-
t.Log("Admin should be allowed now to list Workspaces.")
78-
_, err = kcpClusterClient.Cluster(logicalcluster.NewPath("root")).TenancyV1alpha1().Workspaces().List(ctx, metav1.ListOptions{})
143+
restClient, err := rest.UnversionedRESTClientFor(rootShardCfg)
79144
require.NoError(t, err)
80145

81-
webhookStop()
82-
// run the webhook with deny policy
83-
webhookStop = RunWebhook(ctx, t, webhookPort, "kubernetes:authz:deny")
84-
t.Cleanup(webhookStop)
85-
86-
t.Log("Admin should not be allowed now to list Logical clusters.")
87-
_, err = kcpClusterClient.Cluster(logicalcluster.NewPath("root")).CoreV1alpha1().LogicalClusters().List(ctx, metav1.ListOptions{})
88-
require.Error(t, err)
89-
90-
t.Log("Admin should not be allowed to list Services.")
91-
_, err = kubeClusterClient.Cluster(logicalcluster.NewPath("root")).CoreV1().Services("default").List(ctx, metav1.ListOptions{})
92-
require.Error(t, err)
93-
94-
t.Log("Verify that it is not allowed to access AllowAllPaths endpoints.")
95-
req = rest.NewRequest(restClient).RequestURI("/healthz")
96-
t.Logf("%s should not be accessible.", req.URL().String())
146+
req := rest.NewRequest(restClient).RequestURI(endpoint)
147+
t.Logf("Verifying access to: %s", req.URL().String())
97148
_, err = req.Do(ctx).Raw()
98-
require.Error(t, err)
149+
if shouldSucceed {
150+
require.NoError(t, err)
151+
} else {
152+
require.Error(t, err)
153+
}
99154
}

test/e2e/authorizer/authorizer_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import (
4949
"github.com/kcp-dev/kcp/test/e2e/framework"
5050
)
5151

52-
//go:embed *.yaml
52+
//go:embed testdata/*.yaml
5353
var embeddedResources embed.FS
5454

5555
func TestAuthorizer(t *testing.T) {
@@ -78,8 +78,8 @@ func TestAuthorizer(t *testing.T) {
7878
_, org2Workspace1 := framework.NewWorkspaceFixture(t, server, org2, framework.WithName("workspace1"), framework.WithRootShard()) // on root for deep SAR test
7979
framework.NewWorkspaceFixture(t, server, org2, framework.WithName("workspace2"))
8080

81-
createResources(ctx, t, dynamicClusterClient, kubeDiscoveryClient, org1.Join("workspace1"), "workspace1-resources.yaml")
82-
createResources(ctx, t, dynamicClusterClient, kubeDiscoveryClient, org2.Join("workspace1"), "workspace1-resources.yaml")
81+
createResources(ctx, t, dynamicClusterClient, kubeDiscoveryClient, org1.Join("workspace1"), "testdata/workspace1-resources.yaml")
82+
createResources(ctx, t, dynamicClusterClient, kubeDiscoveryClient, org2.Join("workspace1"), "testdata/workspace1-resources.yaml")
8383

8484
framework.AdmitWorkspaceAccess(ctx, t, kubeClusterClient, org1, []string{"user-1", "user-2", "user-3", "user-4", "user-5"}, nil, false)
8585

test/e2e/authorizer/webhook.kubeconfig renamed to test/e2e/authorizer/testdata/webhook1.kubeconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ kind: Config
33
clusters:
44
- name: httest
55
cluster:
6-
certificate-authority: .TestWebhook/ca.crt
6+
certificate-authority: .TestAuthorizationOrder/Authorization_order_1/ca.crt
77
server: https://localhost:8080/
88
current-context: webhook
99
contexts:

test/e2e/authorizer/authzorder.kubeconfig renamed to test/e2e/authorizer/testdata/webhook2.kubeconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ kind: Config
33
clusters:
44
- name: httest
55
cluster:
6-
certificate-authority: .TestAuthorizationOrder/ca.crt
6+
certificate-authority: .TestAuthorizationOrder/Authorization_order_2/ca.crt
77
server: https://localhost:8081/
88
current-context: webhook
99
contexts:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: v1
2+
kind: Config
3+
clusters:
4+
- name: httest
5+
cluster:
6+
certificate-authority: .TestAuthorizationOrder/Default_authorization_order/ca.crt
7+
server: https://localhost:8082/
8+
current-context: webhook
9+
contexts:
10+
- name: webhook
11+
context:
12+
cluster: httest

test/e2e/authorizer/utils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func RunWebhook(ctx context.Context, t *testing.T, port string, response string)
3535
address := fmt.Sprintf("localhost:%s", port)
3636

3737
ctx, cancel := context.WithCancel(ctx)
38-
pkiDir := fmt.Sprintf(".%s", t.Name())
38+
pkiDir := fmt.Sprintf("testdata/.%s", t.Name())
3939
args := []string{
4040
"--tls",
4141
"--response", response,

0 commit comments

Comments
 (0)