Skip to content

Commit d94ea4d

Browse files
committed
Add webhook for Kubernetes components to validate permissions
Add check on create/update DevWorkspaces to verify that user performing the action has RBAC permissions to create/update/delete objects in that Kubernetes type. If not, the operator is rejected. For now, only inlined Kubernetes components are processed to avoid having to always fetch from URI. Signed-off-by: Angel Misevski <[email protected]>
1 parent 4bb7a56 commit d94ea4d

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright (c) 2019-2022 Red Hat, Inc.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package handler
15+
16+
import (
17+
"context"
18+
"fmt"
19+
"strings"
20+
21+
dwv2 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
22+
authv1 "k8s.io/api/authorization/v1"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
25+
"sigs.k8s.io/yaml"
26+
)
27+
28+
func (h *WebhookHandler) validateKubernetesObjectPermissionsOnCreate(ctx context.Context, req admission.Request, wksp *dwv2.DevWorkspace) error {
29+
kubeComponents := getKubeComponentsFromWorkspace(wksp)
30+
for componentName, component := range kubeComponents {
31+
if component.Uri != "" {
32+
return fmt.Errorf("kubenetes components specified via URI are unsupported")
33+
}
34+
if component.Inlined == "" {
35+
return fmt.Errorf("kubernetes component does not define inlined content")
36+
}
37+
if err := h.validatePermissionsOnObject(ctx, req, componentName, component.Inlined); err != nil {
38+
return err
39+
}
40+
}
41+
return nil
42+
}
43+
44+
func (h *WebhookHandler) validateKubernetesObjectPermissionsOnUpdate(ctx context.Context, req admission.Request, newWksp, oldWksp *dwv2.DevWorkspace) error {
45+
newKubeComponents := getKubeComponentsFromWorkspace(newWksp)
46+
oldKubeComponents := getKubeComponentsFromWorkspace(oldWksp)
47+
48+
for componentName, newComponent := range newKubeComponents {
49+
if newComponent.Uri != "" {
50+
return fmt.Errorf("kubenetes components specified via URI are unsupported")
51+
}
52+
if newComponent.Inlined == "" {
53+
return fmt.Errorf("kubernetes component does not define inlined content")
54+
}
55+
56+
oldComponent, ok := oldKubeComponents[componentName]
57+
if !ok || oldComponent.Inlined != newComponent.Inlined {
58+
// Review new components
59+
if err := h.validatePermissionsOnObject(ctx, req, componentName, newComponent.Inlined); err != nil {
60+
return err
61+
}
62+
}
63+
}
64+
return nil
65+
}
66+
67+
func (h *WebhookHandler) validatePermissionsOnObject(ctx context.Context, req admission.Request, componentName, component string) error {
68+
69+
typeMeta := &metav1.TypeMeta{}
70+
if err := yaml.Unmarshal([]byte(component), typeMeta); err != nil {
71+
return fmt.Errorf("failed to read content for component %s", componentName)
72+
}
73+
if typeMeta.Kind == "List" {
74+
return fmt.Errorf("lists are not supported in Kubernetes or OpenShift components")
75+
}
76+
77+
// Workaround to get the correct resource type for a given kind -- probably fragile
78+
// Convert e.g. Pod -> pods, Deployment -> deployments
79+
resourceType := fmt.Sprintf("%ss", strings.ToLower(typeMeta.Kind))
80+
81+
sar := &authv1.LocalSubjectAccessReview{
82+
ObjectMeta: metav1.ObjectMeta{
83+
Namespace: req.Namespace,
84+
},
85+
Spec: authv1.SubjectAccessReviewSpec{
86+
ResourceAttributes: &authv1.ResourceAttributes{
87+
Namespace: req.Namespace,
88+
Verb: "*",
89+
Group: typeMeta.GroupVersionKind().Group,
90+
Version: typeMeta.GroupVersionKind().Version,
91+
Resource: resourceType,
92+
},
93+
User: req.UserInfo.Username,
94+
Groups: req.UserInfo.Groups,
95+
UID: req.UserInfo.UID,
96+
},
97+
}
98+
99+
err := h.Client.Create(ctx, sar)
100+
if err != nil {
101+
return fmt.Errorf("failed to create subjectaccessreview for request: %w", err)
102+
}
103+
104+
username := req.UserInfo.Username
105+
if username == "" {
106+
username = req.UserInfo.UID
107+
}
108+
109+
if !sar.Status.Allowed {
110+
return fmt.Errorf("user %s does not have permissions to work with objects of kind %s defined in component %s", username, typeMeta.GroupVersionKind().String(), componentName)
111+
}
112+
113+
return nil
114+
}
115+
116+
// getKubeComponentsFromWorkspace returns the Kubernetes (and OpenShift) components in a workspace
117+
// in a map with component names as keys.
118+
func getKubeComponentsFromWorkspace(wksp *dwv2.DevWorkspace) map[string]dwv2.K8sLikeComponent {
119+
kubeComponents := map[string]dwv2.K8sLikeComponent{}
120+
for _, component := range wksp.Spec.Template.Components {
121+
kubeComponent, err := getKubeLikeComponent(&component)
122+
if err != nil {
123+
continue
124+
}
125+
kubeComponents[component.Name] = *kubeComponent
126+
}
127+
return kubeComponents
128+
}
129+
130+
// getKubeLikeComponent returns the definition of the Kubernetes or OpenShift
131+
// component defined by a general DevWorkspace Component. If the component does
132+
// not specify the Kubernetes or OpenShift field, an error is returned.
133+
func getKubeLikeComponent(component *dwv2.Component) (*dwv2.K8sLikeComponent, error) {
134+
if component.Kubernetes != nil {
135+
return &component.Kubernetes.K8sLikeComponent, nil
136+
}
137+
if component.Openshift != nil {
138+
return &component.Openshift.K8sLikeComponent, nil
139+
}
140+
return nil, fmt.Errorf("component does not specify kubernetes or openshift fields")
141+
}

webhook/workspace/handler/workspace.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ func (h *WebhookHandler) MutateWorkspaceV1alpha2OnCreate(ctx context.Context, re
5454
return admission.Denied(err.Error())
5555
}
5656

57+
if err := h.validateKubernetesObjectPermissionsOnCreate(ctx, req, wksp); err != nil {
58+
return admission.Denied(err.Error())
59+
}
60+
5761
if warnings := checkUnsupportedFeatures(wksp.Spec.Template); unsupportedWarningsPresent(warnings) {
5862
return h.returnPatched(req, wksp).WithWarnings(formatUnsupportedFeaturesWarning(warnings))
5963
}
@@ -154,6 +158,10 @@ func (h *WebhookHandler) MutateWorkspaceV1alpha2OnUpdate(ctx context.Context, re
154158
return admission.Denied(err.Error())
155159
}
156160

161+
if err := h.validateKubernetesObjectPermissionsOnUpdate(ctx, req, newWksp, oldWksp); err != nil {
162+
return admission.Denied(err.Error())
163+
}
164+
157165
oldCreator, found := oldWksp.Labels[constants.DevWorkspaceCreatorLabel]
158166
if !found {
159167
return admission.Denied(fmt.Sprintf("label '%s' is missing. Please recreate devworkspace to get it initialized", constants.DevWorkspaceCreatorLabel))

0 commit comments

Comments
 (0)