1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package webhook
17
18 import (
19 "context"
20 "fmt"
21 "net/http"
22 "strings"
23
24 "github.com/go-logr/logr"
25 "github.com/pkg/errors"
26 authnv1 "k8s.io/api/authentication/v1"
27 authzv1 "k8s.io/api/authorization/v1"
28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29 "k8s.io/apimachinery/pkg/runtime"
30 authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
31 "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
32
33 "github.com/chaos-mesh/chaos-mesh/api/v1alpha1"
34 )
35
36 var alwaysAllowedKind = []string{
37 v1alpha1.KindAWSChaos,
38 v1alpha1.KindPodNetworkChaos,
39 v1alpha1.KindPodIOChaos,
40 v1alpha1.KindGCPChaos,
41 v1alpha1.KindPodHttpChaos,
42 v1alpha1.KindPhysicalMachine,
43 v1alpha1.KindStatusCheck,
44 v1alpha1.KindRemoteCluster,
45
46 "WorkflowNode",
47 }
48
49
50
51
52 type AuthValidator struct {
53 enabled bool
54 authCli *authorizationv1.AuthorizationV1Client
55
56 decoder *admission.Decoder
57
58 clusterScoped bool
59 targetNamespace string
60 enableFilterNamespace bool
61 logger logr.Logger
62 }
63
64
65 func NewAuthValidator(enabled bool, authCli *authorizationv1.AuthorizationV1Client, decoderScheme *runtime.Scheme,
66 clusterScoped bool, targetNamespace string, enableFilterNamespace bool, logger logr.Logger) *AuthValidator {
67 return &AuthValidator{
68 enabled: enabled,
69 authCli: authCli,
70 decoder: admission.NewDecoder(decoderScheme),
71 clusterScoped: clusterScoped,
72 targetNamespace: targetNamespace,
73 enableFilterNamespace: enableFilterNamespace,
74 logger: logger,
75 }
76 }
77
78
79 func (v *AuthValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
80 if !v.enabled {
81 return admission.Allowed("")
82 }
83
84 username := req.UserInfo.Username
85 groups := req.UserInfo.Groups
86 requestKind := req.Kind.Kind
87
88 if contains(alwaysAllowedKind, requestKind) {
89 return admission.Allowed(fmt.Sprintf("skip the RBAC check for type %s", requestKind))
90 }
91
92 kind, ok := v1alpha1.AllKindsIncludeScheduleAndWorkflow()[requestKind]
93 if !ok {
94 err := errors.Wrapf(errInvalidValue, "kind %s is not support", requestKind)
95 return admission.Errored(http.StatusBadRequest, err)
96 }
97 chaos := kind.SpawnObject()
98
99 err := v.decoder.Decode(req, chaos)
100 if err != nil {
101 return admission.Errored(http.StatusBadRequest, err)
102 }
103
104 requireClusterPrivileges, affectedNamespaces := affectedNamespaces(chaos)
105
106 if requireClusterPrivileges {
107 allow, err := v.auth(req.UserInfo, "", requestKind)
108 if err != nil {
109 return admission.Errored(http.StatusBadRequest, err)
110 }
111
112 if !allow {
113 return admission.Denied(fmt.Sprintf("%s is forbidden on cluster", username))
114 }
115 v.logger.Info("user have the privileges on cluster, auth validate passed", "user", username, "groups", groups, "namespace", affectedNamespaces)
116 } else {
117 v.logger.Info("start validating user", "user", username, "groups", groups, "namespace", affectedNamespaces)
118
119 for namespace := range affectedNamespaces {
120 allow, err := v.auth(req.UserInfo, namespace, requestKind)
121 if err != nil {
122 return admission.Errored(http.StatusBadRequest, err)
123 }
124
125 if !allow {
126 return admission.Denied(fmt.Sprintf("%s is forbidden on namespace %s", username, namespace))
127 }
128 }
129
130 v.logger.Info("user have the privileges on namespace, auth validate passed", "user", username, "groups", groups, "namespace", affectedNamespaces)
131 }
132
133 return admission.Allowed("")
134 }
135
136 func (v *AuthValidator) auth(userInfo authnv1.UserInfo, namespace string, chaosKind string) (bool, error) {
137 resourceName, err := v.resourceFor(chaosKind)
138 if err != nil {
139 return false, err
140 }
141
142 sar := authzv1.SubjectAccessReview{
143 Spec: authzv1.SubjectAccessReviewSpec{
144 ResourceAttributes: &authzv1.ResourceAttributes{
145 Namespace: namespace,
146 Verb: "create",
147 Group: "chaos-mesh.org",
148 Resource: resourceName,
149 },
150 User: userInfo.Username,
151 UID: userInfo.UID,
152 Groups: userInfo.Groups,
153 Extra: convertExtra(userInfo.Extra),
154 },
155 }
156
157
158 response, err := v.authCli.SubjectAccessReviews().Create(context.TODO(), &sar, metav1.CreateOptions{})
159 if err != nil {
160 return false, err
161 }
162
163 return response.Status.Allowed, nil
164 }
165
166 func (v *AuthValidator) resourceFor(name string) (string, error) {
167
168 return strings.ToLower(name), nil
169 }
170
171 func contains(arr []string, target string) bool {
172 for _, item := range arr {
173 if item == target {
174 return true
175 }
176 }
177 return false
178 }
179
180 func convertExtra(in map[string]authnv1.ExtraValue) map[string]authzv1.ExtraValue {
181 if in == nil {
182 return nil
183 }
184
185 extra := make(map[string]authzv1.ExtraValue)
186 for key, value := range in {
187 extra[key] = authzv1.ExtraValue(value)
188 }
189 return extra
190 }
191